Skip to content

v3.6.0

Choose a tag to compare

@dong845 dong845 released this 20 Jul 11:02

Four things ship in this release: a security fix for a real vulnerability, a new chart
form, a new component family, and a rule that finally became a gate.

Security — this release fixes an SVG-rasterizer flaw where an attacker-supplied icon could read
local files, or run JS/SSRF, through the headless-Chrome fallback. If you pass local or third-party
SVGs to icon_png(), upgrade.


Added — OLDSTYLE_FIGURES: a documented rule becomes a deterministic gate

  • deckkit.lint_layout now hard-fails on display numerals set in an OLD-STYLE (text) figure
    face
    — Georgia, Palatino, Baskerville, Book Antiqua, Constantia, Hoefler Text, Calluna, Candara.
    Those faces set 0/1/2 at x-height, push 6/8 up and drop 3/4/5/7/9 below the baseline, so a hero
    number visibly bobs and misaligns with adjacent CJK/Latin. The check names the face, the offending
    run and its size, and points at the lining-figure fix.
  • Threshold-aware, so it is right rather than merely strict: fires at ≥20pt only. Old-style
    figures inside running prose are a legitimate typographic choice and stay quiet; the gate targets
    display numerals, where the wobble is a defect.
  • Why this shipped as code and not more prose: the rule was already written in five places —
    SKILL.md, design-principles.md, font-guidance.md, multilingual.md and critic.md — and was
    still missed on a real deck, twice, across two skills. That is precisely the failure mode SKILL.md's
    own enforcement invariant warns about: a MUST that lives only in reference prose is advisory in
    practice
    . Geometry linters cannot see it (it is a font property, not a layout fault) and render
    thumbnails are too small to reveal it, so nothing downstream caught it either.
  • Wired end-to-end: the CRITICAL in lint_layout, a plain-language row in troubleshooting-faq.md §4,
    the hard-fail list in SKILL.md updated from four items to five, and a four-case regression test
    in smoke_deckkit.py covering both directions (Georgia/Palatino display numerals fail; Georgia body
    prose and a lining-face numeral pass) — so the gate itself is verified to fire, not merely present.

Added — composition / target / range components (native, editable)

  • native_chart stacked & area kindscolumn_stacked · column_stacked_100 · bar_stacked ·
    bar_stacked_100 · area · area_stacked · area_stacked_100: real editable PowerPoint charts for
    composition over time (a total AND its component mix — the most common exec chart the roster
    couldn't draw). Series-fill themed, zero_base extended to stacked column/bar, CJK <a:ea> labels
    intact; a printed notice fires on negative segments (a stack's height only reads as a sum for
    same-sign parts).
  • deckkit.bullet_graph — Stephen Few's 'actual vs TARGET, in context' KPI bar (poor/ok/good bands +
    a target tick), one row per metric. Each row scales to its OWN max, so a mixed-unit dashboard reads
    right; higher_better=False reverses the bands for churn/latency. The dashboard bar scorecard/
    meter_bar can't give.
  • deckkit.range_bars — a 'football field' (floating min–max ranges per row on a shared axis_scale
    • optional base-case tick); closes form-selection's recipe-only note.
  • Wired end-to-end from a 4-lane self-check (integration · decision-taste · agent-workflow cooperation
    · correctness): a Composition + Range row in the Concept→Visualization dictionary (§3), a
    'stacked chart that misleads' anti-pattern (100%-hides-collapsing-total · negative segments · spaghetti)
    in data-viz.md + critic hooks in review-rubrics.md/critic.md, a single-highlight carve-out so a
    correct stacked chart isn't flagged, and roster/catalogue entries (form-selection.md, data-viz.md,
    design-gallery.md, SKILL.md). Correctness fixes: range_bars includes base-case points in its axis;
    bullet_graph sorts bands; smoke test added.

Added — choropleth map (value per country / province)

  • deckkit.choropleth(slide, x, y, w, h, data, mapname) + scripts/maps.py — the high-payoff
    form the skill was missing: shade a real map by a value per region. mapname = europe · world ·
    china (provinces). Built on public-domain geometry (Natural Earth 110m countries · DataV China
    provinces — nothing fabricated), rendered with matplotlib (no geopandas): Albers equal-area conic for a
    region, equirectangular for the world; light→accent ramp (or scale='div'); neutral no-data fill;
    thin borders. Key data by ISO-3166 alpha-2/alpha-3 or English name (countries), or province name/adcode
    (china); the Natural Earth ISO_A2 = -99 quirk is patched (France/Norway…) and unmatched keys are
    reported, not dropped.
  • The map PNG is language-agnostic; the title + gradient legend are native deckkit text, so units
    and titles render in any language (CJK included) instead of matplotlib tofu. Geometry is fetched once and
    cached like icons (network only on first use; SLIDE_MAKER_CACHE override).
  • Hardened by a 3-lane self-check (code-alignment · decision-taste · adversarial correctness): scale='div'
    is zero-centred (neutral == 0, distinct poles via accent2) and the native legend now renders the
    diverging ramp with a 0 tick so legend and map agree; NaN/None values are treated as no-data (no
    crash); the unmatched-key notice reports the actual bad keys; China's nine-dash line renders as a
    visible dashed element. Decision-taste wired everywhere the model looks: a Geography/"where" row in the
    Concept→Visualization dictionary (design-intelligence-addendum.md §3 + form-selection.md), a
    counts-not-rates anti-pattern in data-viz.md + a named critic check in review-rubrics.md, and the
    "shade a rate, blank = no data" guidance in the docstring. Smoke covers projection, region matching
    (incl. the -99 patch), NaN, div, and a real render.

Security — hardening pass (public-skill audit)

  • SVG icon rasterizer no longer executes untrusted content (was the one real vuln). icon_png()
    accepts a local .svg, and when cairosvg/rsvg-convert are absent the headless-Chrome fallback
    used to render it with JS + file:// access — an attacker-supplied icon could read local files
    (exfiltrated into the deck) or run JS/SSRF. Fixed with sanitize_svg() (a backend-agnostic control
    run before every rasterizer): strips <script>/<foreignObject>/<iframe>/<image>/<audio>/
    <video>/<set>/<animate> (incl. namespaced forms), on* handlers, any non-internal
    href/xlink:href/src, and external url(...) — while preserving paths, <use href="#..">, and
    the gradient refs recolor injects. Chrome also runs --disable-remote-fonts. Comments are stripped
    first (no comment-split reassembly), and the input is size/complexity-bounded (>100KB or >600 elements
    is refused in ~1ms) so the sanitizer itself can't be turned into a quadratic-time DoS. Verified across
    all 13 icon libraries: real icons (flat + gradient + duotone) still render; a 16-payload adversarial
    battery is fully neutralized; a ReDoS regression test guards the bound (smoke_deckkit.py).
  • Icon name is validated (fetch_svg) as a strict slug ([A-Za-z0-9._-], no ..), closing a
    path-traversal into the icon cache and a CDN-URL traversal to arbitrary jsDelivr/npm packages.
  • LibreOffice render is now time-bounded (render_deck.py, timeout=300) so a malformed/hostile
    .pptx can't hang the pipeline — matching the timeouts every other soffice/ffmpeg call already had.