Skip to content

Releases: addsumtech/slides_maker

v4.4.0

Choose a tag to compare

@dong845 dong845 released this 03 Aug 00:14

Two behaviour changes to know about before upgrading.

(1) Section fan-out now starts at ~9 slides, not 15+. A 9–14 slide deck that used to be written
by one author is now authored by parallel section agents against a locked design plan and a shared
style.py. The old threshold was reasoned on TOKEN cost, where one author genuinely wins because
nothing is duplicated — but wall clock was never on the scale, and measured across five real build
sessions the build step is 40–71% of all model-active minutes, generated serially by one agent
whose context reaches ~500k by mid-build. Fan-out spends the same tokens concurrently and hands each
author a fresh ~60k context. Fanning out the BUILD deliberately does not escalate the REVIEW
shape: the sectioned critic panel still belongs to a genuinely large deck (~15+), because one
reviewer reading a 40-slide document reviews it worse — coupling the two thresholds would add
critics to every mid-sized deck and spend the wall clock the fan-out just saved. If you are
optimising for tokens rather than wall clock, the old threshold was correct and is documented as
such.

(2) render_deck.py rejects an unrecognised flag instead of resolving it to the output
directory. render_deck.py deck.pptx --gate-check --briefing used to run the hand-off gate at the
presented legibility floor — a briefing deck, whose budget is ~150 words, held to ~40 — and print
nothing either way.


Why this release exists

A user reported that one deck took ~5 hours to build. Measured across six real build
transcripts, the cause was not the critics, not the scripts and not the user:

  • 83–90% of all output tokens are reasoning, not deliverables (tool payloads 9–15%, visible text ~1.5%)
  • median context 440–575k per turn, p90 ~900k, hitting the 1M cap and forcing compaction
  • ~1,200 tool calls per session at ~1,400 output tokens each
  • every deterministic script is irrelevant to this: a full 14-slide render is 5.4s, the whole gate suite ~8s

SKILL.md already stated the governing equation — the cost of a deck is round-trips × context
and its own measurement of a 12-page build: 122 calls averaging 1.00 tool per round-trip. The
rule was stated once, in the preamble, and never at the two places it binds. It was advisory, and it
was skipped.

Verified by rebuilding the same 14-slide deck on the new pipeline: hard lint findings 11 → 0,
and a real 2-round × 2-lens critic panel ran where the baseline had waived the critic entirely.
Round-trips 512 → 152, output tokens 1,358,258 → 154,154.

Honest bound: the critical path went from ~2.4h to roughly 2h for a 14-slide deck with the full
two-round critic. Most of what remains is subagent stages (planner, art director, section authors,
two critic rounds), which these changes do not touch. Getting under an hour requires cutting a
critic round or reducing scope — both quality trades, and neither is done here.

Fixed — three measurements that could not see what they were measuring

  • deckkit.measure_text gained font=. It never passed a face to the metric, so it always
    measured in the deck default. The same command string is 4.04in in Helvetica and 5.44in in Courier
    New, so a monospace line came back 26% narrow — enough to report a 9.2in line as fitting an
    8.25in box. Nothing downstream can catch that: the box is then BUILT to the wrong size and every
    later geometry check agrees with the box. fit_text_size had always taken font=; the asymmetry
    was the bug. It cost one measured build a full rebuild.
  • lint_deck had two definitions of "the title". _find_title answers it as "a TITLE
    placeholder, else the first text ≥14.5pt in the top 28%". The deck-stats scan answered it
    independently: top 20% of the canvas, biggest short box, no size floor at all. A title sitting
    between the two bands was invisible to the stats scan, so whatever chrome sat above it won —
    reproduced with a 30pt title at 1.30in and a 9.5pt margin word at 0.52in. That is not cosmetic:
    title_txt feeds the title spine, the titles-only test that the coordinator and both critic
    lenses read as the deck's argument. One measured deck presented its argument as three pieces of
    margin chrome. It also fed INVERTED TYPE HIERARCHY, which then advised that a correct 30pt title
    was smaller than its body tier. Zero test coverage before this release.
  • lint_deck measured text in a face it refused to look at. It imports deckkit, then dropped
    face and weight on the very line that reads r.font, after which every Latin character was charged
    a flat 0.52 em. Measured on Helvetica Neue at 26pt: uppercase runs +33% over the estimate and
    narrow glyphs and spaces −44% — so it both missed real collisions and invented others. The
    invented ones are the expensive half: an author rewrote two correct titles to satisfy a wrong
    measurement, which is rework the gate itself caused.
  • code_block says when a line will clip. It sets word_wrap=False so indentation survives, and
    its own comment nominated the docstring's "keep snippets short" as the enforcement. A clip is
    invisible to every gate — the height model stays right, the shape stays on canvas, and the end of
    the line is simply not on the slide.
  • preflight_check's mono-wrap check stopped crying wolf. It charged the whole paragraph the
    monospace advance whenever any run was monospace (~20% over on a label: <command> line), and
    announced "word_wrap is off, so they run off it silently" without having looked at word_wrap.
    Three of its findings on one real build were false, and separating them from the true one cost real
    time. This precision is load-bearing, not tidiness: the check FAILS a build, and a builder who
    learns it cries wolf routes around the one defect it exists for — a shipped install command that
    copied as a 404 path.

Added — the cost of a build is measurable, and the repetition is gone

  • scripts/roundtrip_budget.py measures a build from its session transcript: round-trips, the
    batching ratio (tool calls per round-trip), median context re-sent, and whether the render
    self-check read the slide PNGs in one message or one at a time. Step 6 fills the hand-off cost:
    line from it. It refuses to guess when it cannot find the right transcript — it used to fall
    back to the newest transcript anywhere on the machine and reported a stranger's session as yours,
    including "image reads 0" for a run that made 28.
  • scripts/dispatch_brief.py writes the deck brief once and prints a pointer prompt for every
    dispatch. Measured: nine dispatches cost 41,203 output tokens at ~4,600 each, and almost all of
    it was the same interview answers, paths, search cap and CONTRACT CARD retyped nine times. The
    generated prompt is ~220 tokens. Second reason: it makes the contract card one artifact instead
    of nine reconstructions, which critic-panel.md asks for and has no way to check. It refuses to
    emit a prompt while any required section of the brief is unfilled.
  • scripts/slide_index.py prints slide N -> file:line function plus each slide's plan-row
    docstring. Section fan-out means the coordinator did not write the code, so a finding on slide 7
    otherwise begins by grepping three modules it has never read.
  • Icons render ~300× faster. rasterize() had no output cache, and cairocffi cannot find a
    Homebrew libcairo, so every icon fell through to headless Chrome: 5.05s → 0.021s cold, 0.003s
    warm
    , output equivalent (ink coverage within 0.0002). That also exposed a latent quality bug —
    only the Chrome backend supersampled, so on any machine where cairosvg did load, icons silently
    shipped at ⅓ resolution. All three backends now share one supersample constant, and check_env
    stops reporting the slowest backend as a plain [ok].
  • Codex image concurrency scales with the machine (cores//3, clamped 2–4) instead of a flat 2,
    against work that spends its time waiting on a hosted model rather than on a local core. A 4-core
    CI box keeps exactly the old behaviour.
  • Batching restated where it binds. Step 5 now says to read every slide PNG in ONE message and to
    apply the whole promoted fix list in ONE message, each with its reason — and the render self-check
    gained a required per-slide verdict line, because batching the reads must not blur fourteen
    judgments into one impression.

Not done, deliberately

SKILL.md is not slimmed. It is ~44.8k tokens of the ~302k per-call context — the smallest
term — and this repo has a recorded measured regression from exactly that change: a "lossless" split
that scored a perfect content-preservation pass, passed every automated gate, and still shipped a
deck with zero icons and a hand-rolled chart. Context size was also tested directly and dropped as a
lever: latency against context on cache-hit turns regressed at R² = 0.009, and deleting the
entire skill from context has a ceiling of ~2.4 minutes.

v4.3.0

Choose a tag to compare

@dong845 dong845 released this 31 Jul 00:58
v4.3.0

v4.2.0

Choose a tag to compare

@dong845 dong845 released this 28 Jul 08:13

One behaviour change to know about before upgrading. render_deck.py --gate-check /
--deliverables now require a critic waiver to be CLASSIFIED, not merely written. A
.deck-gates.json carrying {"critic": {"waived": "<any string>"}} used to print one line and
pass; it now needs waived_category from a fixed set (no-dispatch-on-host /
already-reviewed-minor-edit / user-waived / external-deck), a reason of real substance, and
— for no-dispatch-on-host — an explicit inline_ran boolean. Existing scripts that waive the
critic will start failing until they say which kind of skip it is. The verdict: consent path is
untouched. Why: the Codex delivery gate had required a distinct schema-valid review artifact per
lens for a while, and the shared path accepted any sentence — measured, a hand-typed waiver
carried a whole 10-slide deck through all hand-off gates pass with no independent critic ever
involved.

Added — the checklist now has something that reads it

  • scripts/preflight_check.py decides the mechanical half of PRE-FLIGHT and prints the other
    half as explicitly NOT covered. Eleven of the twelve ticks were self-attested: the model wrote
    twelve checkmarks and nothing anywhere read them, which is the exact silent-skip class the
    checklist exists to prevent. It decides items 1 (speaker-notes coverage), 2 (build timing vs
    --static), 3b (every build: docstring having matching Build.step calls, by AST), 4 (native
    charts + equation_native), 7 (an as-of date), 8 (meta-annotations and unfilled
    <slot>/{slot} text leaked onto a slide
    ) and 10 (fonts that may not resolve elsewhere) —
    and prints 5 / 6 / 6b / 9 / 11 as judgment calls that are still yours, so a tick never looks
    covered. Items 2, 7 and 10 are advisory rather than failures by construction: whether builds
    were opted in, whether any claim is time-bound, and whether a font exists on the presenter's
    machine are all facts absent from the file, and a checker that fails on what it cannot know is
    one people learn to ignore. (Item 10 learned this the hard way — it shipped as a failure, and CI
    on Ubuntu promptly declared every macOS-authored deck "not ready to render". item10_fonts can
    now return PASS / ADVISORY / NOT CHECKABLE and never FAIL, verified by AST over its own return
    statements and by stubbing the installed-font list empty.)
  • scripts/check_reference_code.py resolves every deckkit.* call taught in the skill's own
    prose against the real module — unknown helper, bad keyword, dead references/*.md pointer, and
    the silent .fore_color.alpha = ... no-op. Prose that teaches an API is still an API contract,
    and nothing used to check it.
  • CI now runs both, plus the Codex gate's own 437 lines of tests, which shipped wired to nothing
    (grep -c codex ci.yml was 0). Every new step asserts the suite RAN rather than merely exited 0.
  • tests/test_critic_waiver_gate.py (7 cases) and tests/test_preflight_check.py (11 cases). The
    latter asserts both halves: that mechanical defects are caught, and that a clean run still says
    the five judgment items are unticked.

Fixed — four wrong API facts were shipping in the shared references

  • A scrim recipe that erased the image it was protecting. Two unfenced shared references taught
    scrim.fill.fore_color.alpha = int(0.6 * 255). python-pptx's ColorFormat has no alpha setter
    and no __slots__, so the assignment raises nothing, writes nothing, and yields a 100% opaque
    rectangle. Measured on a real deck: the full-bleed cover plate rendered pure black (brightest
    pixel 0/765) and every lint passed green — text is painted last so TEXT NOT VISIBLE /
    OCCLUSION cannot fire, and white-on-pure-black scores ~21:1 so TEXT-ON-IMAGE CONTRAST reported
    the best number in the deck. Now deckkit.scrim_overlay(), whose gradient carries a real
    <a:alpha> on DrawingML's 0–100000 scale (not 0–255, the other half of the original bug).
  • dk.card(...) — no such helper, and it was the checkmarked "Do" example while the crossed-out
    "Don't" example was the one that actually worked. icon_tile(tile_color=...) — the parameter is
    fill=. references/deckkit-component-guide.md — no such file. role=content-critic dispatch —
    appears nowhere in the skill; the real contract is lens-based via agents/critic.md +
    validate_review.py.
  • image-generation.md's "Planning workflow" list was split in half. A 90-line section had been
    inserted between steps 2 and 3, orphaning steps 3–6 — including the emphatic "Do NOT pass
    --count" rule — under an unrelated heading. Moved verbatim; the list reads 1–6 again.
  • codex-runtime.md never named review_effort / fast_opt_in / thorough_panel / arbiters,
    the keys its own delivery gate enforces — zero matches across every .md in the skill. Scoped
    honestly: review_effort defaults to standard, so a standard-tier run was never blocked, and
    --init already emitted most of them. The one real deadlock was fast, whose required
    fast_opt_in appeared in no doc and was missing from the --init skeleton. Both closed.
  • build_example_generic.py promised a spot-check that each build: docstring has matching
    Build.step calls, while its own slide_pipeline docstring declared build: and implemented
    none — the example was teaching a docstring that lies.
  • file-inventory.md was missing three scripts and every test suite. An inventory that omits a
    script is a navigation failure: the model cannot reach for a tool it cannot see.

Changed

  • The Codex / OpenAI adapter now lives on main rather than a side branch. One tree serves both
    runtimes, separated by Codex only: fences and the references/runtime-routing.md profiles
    (shared = Claude Code / Kimi; codex + openai-gpt-bridged = OpenAI). Routing is
    capabilities-first, not provider-inferred. The pre-merge tree is preserved on the cc_4.1.0
    branch.
  • Local install artifacts (.agents/, .claude/, skills-lock.json, OPTIMIZATION_SUMMARY.md)
    are now ignored; a copy of the skill that drifts from skills/ should not ship in the repo.

v4.1.0

Choose a tag to compare

@dong845 dong845 released this 27 Jul 03:26

One behaviour change to know about before upgrading. render_deck.py --deliverables now also
refuses on DENSITY — more than a third of slides over the text budget stops the hand-off unless
.deck-gates.json carries a written "density": {"waived": "<reason>"}. Scripts that call
--deliverables on text-heavy decks will start failing. Two escapes, both intended: pass the
delivery mode (--selfread / --textheavy / --surface, the same flags lint_deck.py takes) so
the deck is held to its own budget, or record the waiver. Everything else below is additive.

Added — the render decides, not a list of shape types

  • TEXT NOT VISIBLE asks the one question with a bounded answer — does this line render any
    glyphs at all? — straight from the pixels. Every previous occlusion rule enumerated causes (this
    shape type, painted then, covering that much), and causes are unbounded: pictures were skipped
    because alpha is unknowable from the file, groups and gradients for their own reasons, and anything
    assembled from many small parts slipped a per-shape threshold. Three real decks shipped through
    those holes with the gate reporting clean.
  • OCCLUSION / RULE THROUGH TEXT now measure the UNION of everything painted over a text
    block, so a 150-tile field erasing a caption and a rule assembled from 40 dashes are caught like
    the single shapes they look like.
  • CAPTION NOT ALIGNED — a label must sit on the thing it labels. The panels of a composite
    figure have no shape geometry, so captions get laid out on the text grid while the panels sit
    where the plotting library put them, at widths that differ whenever the panels keep their own
    aspect ratios. Read out of the pixels, so it works whether the panels are one composite or a row
    of separate pictures. Ground is estimated per figure crop, not from the page, because a 4% grey
    step between a figure's own paper and the page was enough to make the check disqualify itself.
  • A skipped check is audible. With no renders beside the deck the pixel-backed families disable
    themselves and the run says so ([skipped] … NOT checked: …, plus pixel_checks in --json).
    0 findings with that line present is a different sentence from 0 findings without it.
  • A CRASHED check is audible too. The per-slide statistics were wrapped in
    except Exception: pass; one refactor took TEXT WALL, LAYOUT SAMENESS, UNDERFILLED, FLAT RHYTHM
    and the body-size floor off every deck while the report still printed ✓ clean. It now prints
    [BROKEN] … NOT checked: … and the suite asserts that line is absent on every fixture deck.

Added — hand-off gates that do not depend on what the user feels like accepting

  • DENSITY (above). Calibrated against the skill's own reference deck, which runs at a median of
    27 words a slide — the budget was never the problem. It exists because the per-slide TEXT WALL warning was correct and ignored on two consecutive decks (8/12, then 12/12 over budget).
  • render_deck.py <deck>.pptx --gate-check runs every hand-off gate, renders nothing, finishes
    in under a second. The gates used to be reachable only through --deliverables, which Step 6
    deliberately makes a decline-able offer — so on every deck where the user said "no PDF", the
    strongest gate in the skill never ran, and nothing showed that it hadn't.
  • The design checkpoint now carries a density: line as two numbers the gate can be compared
    against (planned median load, planned count over 70), so density is decided at plan time instead
    of discovered at lint time.

Fixed — the measurement everything else is built on

  • Bold text in font COLLECTIONS (.ttc/.otc) measured at regular width, 3.9% short. Every guard
    built on measure_text silently passed while the text wrapped anyway: a caption sized for one
    line put its second on top of a footer, and the lint agreed with the build because both were
    computed from the same wrong number. Fixed by selecting the right face index inside the
    collection — and CI now renders real strings and compares the ink against the prediction,
    one-sided: the measurement may be conservative, never optimistic.

Changed

  • The critic's second round is scoped, and the cost is published. Round 2 was a full fresh
    whole-deck re-review; the measured A/B is in references/critic-panel.md — including a claim from
    an earlier draft that the measurement did not support and that was removed rather than softened.
  • A third-party assessment does not wear its subject's livery. The LOGO PRINCIPLE situation
    table gained the row for decks that evaluate an entity rather than speak for one.
  • SEARCH BUDGET. Web search is capped per SESSION and shared with every subagent; one research
    fan-out (12 agents + 7 verifiers, none told a cap existed) spent all 200, and the bill arrived
    hours later when a single lookup for a company's official logo could not run. Small NAMED lookups
    go first, each dispatch states a per-agent cap, the round stays under half of what REMAINS (not
    half the cap — the budget does not reset between decks), and searches: planned/spent reaches the
    hand-off cost: line. Stated where the fan-out is DISPATCHED, not 1000 lines later.

CI / repository

  • The credential scanner was excluded from the repo by the very pattern it scans for (*_secret*);
    check_repo_integrity.py now fails the build on any ignored source file or untracked CI path.
  • The regression suite ran ABOVE pip install, so every run died on a bare ModuleNotFoundError
    that reads exactly like a lint regression. Moved after the installs, with an audible-skip backstop
    so a silently skipped suite cannot pass.
  • Both READMEs realigned with the shipped skill.

Verification

22 regression assertions (from 18), two-sided: a PASS corpus that must stay clean, a FAIL corpus
where every defect must be caught, and the skill's own reference deck held at baseline. The lint
changes in this release were then attacked by an adversarial audit that required a runnable repro
for every claim — 20 raised, 5 confirmed, all 5 fixed here, including two claims in earlier commit
messages that the audit proved false.

v4.0.0

Choose a tag to compare

@dong845 dong845 released this 26 Jul 01:05

Why a major bump. render_deck.py --deliverables now REFUSES to run until the deck carries a
.deck-gates.json recording that its quality gates actually ran. Anything scripting --deliverables
breaks until it writes that file (or a written waiver). That is the breaking change; everything else
below is additive.

Added — review effort is a tier the user picks, not one the skill infers

  • The rule that scales the loop ("scale the critic to stakes") always existed but was never offered.
    Measured on one low-stakes deck: ~32 subagents and ~2M tokens across research + build + two
    review rounds, with the user given no visibility and no choice. Step 0 now collects a one-word
    review: tierfast | standard | thorough — sizing BOTH cost centres, the research
    fan-out and the review panel (they measured comparable: ~1.02M tokens of research against ~0.95M
    of review).
  • standard and thorough are pure ALIASES for the low-stakes and high-stakes behaviour that
    already existed — every cell of the tier table either restates the stakes section or defers to it.
    A deck whose user says nothing behaves exactly as before. fast is the one genuinely new band
    and is opt-in only, never derived: one generalist critic and one round is a real recall drop,
    so it has to be asked for. Purpose decides the tier; SIZE never lowers it.
  • Floors no tier may move: the build-time geometry gate, the render lint, PRE-FLIGHT 12, at least one
    independent critic, the EXISTENCE of the primary-source gate, and .deck-gates.json. Per-section
    panels on large decks and corroborated consent on high-stakes decks are shapes, not weight, and are
    unaffected by the tier. fast may not silently ship over a revise — it returns to the user.
  • The hand-off now carries review: and cost: lines. A dial whose bill is never shown builds no
    intuition, so the next deck's choice is as blind as the last one's.
  • Pass the approved claim ledger to critics WHOLE, never a summary retyped for the dispatch. The
    largest measured saving here: 7 of 8 "unsourced" findings in one round-2 review were briefing
    artifacts, and the round they consumed was pure waste.
  • An earlier draft was rejected by a six-dimension adversarial audit (9 blockers, 27 majors) and
    largely rewritten. Two invented optimisations were dropped rather than patched — "triggered
    arbitration" at standard (it contradicted four surviving statements that low-stakes dispatches no
    arbiter, including agents/arbiter.md's own brief) and narrowing round 2 to the changed slides (the
    review-validation gate's scope buckets are whole-deck and per-section only, so a scoped round 2
    bounces and costs an extra round). Both are recorded in critic-panel.md as deliberate reversions
    with their reasons.

Added — the hand-off gate, and a consent record that is evidence rather than a claim

  • --deliverables refuses without .deck-gates.json: a critic verdict, the design plan's
    boldness / signature_move / carried_by / form_ledger, and the provenance pass's per-claim
    claims list (a summary tally is rejected on purpose — a tally is written by the same pass that
    would have skipped the refutation). Any gate deliberately skipped is waived in writing, and the
    tool prints the reason, so a skip is visible instead of invisible.
  • validate_review.py --record <deck-dir> writes the critic block from the validated review
    itself
    — verdict, blocker/major counts, the review file's path and sha256 — and --deliverables
    re-reads that artifact. A record the model types at hand-off is self-certification: the model that
    skipped the loop writes the same JSON as the model that ran it. Verified by execution across five
    adversarial paths (review edited after recording → sha256 mismatch; file moved; record says consent
    while the review says revise → "the artifact wins"; a review consenting with a blocker still open;
    hand-written record with no source → still passes, labelled SELF-REPORTED). Backward compatible by
    design: the old shape is not broken, it is made distinguishable.

Security — credentials are scanned by SHAPE, and the recurring sk-* false positive is documented

  • A third-party multi-engine scan reported a possible hardcoded sk- API key. Audited the full tree
    and the full history (1,645 blobs, all refs): no credential exposure and nothing to revoke.
    Every hit is a CSS class name — the direction-gate preview names its classes after slide skeletons
    (sk-body, sk-split, sk-island, sk-band, sk-rail, sk-statement), and sk- is short for
    skeleton. A real key is sk- + ~48 high-entropy characters; these are sk- + four to nine
    lowercase letters.
  • This was the second time that report had been filed and dismissed by hand (v3.9.0 was the first).
    The false positive is not the expensive part — a report that is always wrong teaches everyone to
    ignore the scanner, and then the one true positive is ignored too.
    So the dismissal is now a
    command: scripts/scan_secrets.py discriminates by minimum length AND a Shannon-entropy floor
    rather than by prefix, covering OpenAI, Anthropic, GitHub classic + fine-grained, AWS, GCP, Slack,
    private-key blocks and generic secret = "..." assignments. It never prints a matched value —
    only its location and a masked head — because a scanner that echoes what it found has published it
    into your CI logs.
  • It self-tests both directions before it scans (real shapes must be caught, this repo's own
    strings must not be), so a rule that silently stops matching fails the build instead of reporting
    clean. Wired into CI on every push and PR: selftest → working tree → every blob in every commit
    (a key deleted in a later commit is still leaked, so the tree alone is not an answer).
  • .gitignore now covers .env*, *.key, *.pem, *.p12, id_rsa*, *_secret* and
    credentials.json — staying out of version control becomes a mechanism rather than a habit.
    image-generation.md no longer shows any literal after OPENAI_API_KEY=; the documented pattern
    reads from a local key file, because a scanner cannot tell a placeholder from a credential and
    neither can a reader skimming a diff. New SECURITY.md records the audit and the ten-second check.

Added — a CI guard that a SKILL.md slimming layers content instead of deleting it

  • scripts/check_skill_lossless.py + a CI step prove every substantive line of a baseline SKILL.md is
    still findable somewhere in the skill tree after a refactor. Deliberate deletions go in an allowlist
    keyed on a hash of the normalised line, so editing a line revokes its waiver.

Changed — SKILL.md layered into step-scoped references (228KB → 134KB), losslessly

  • Working detail moved into eight new step-scoped reference files, each with a live trigger at the
    pipeline moment that needs it. Two files were deliberately pulled BACK inline — the deckkit
    component catalogue and the render self-check — because their absence makes no noise: no lint fires
    when you hand-roll a form the library already has or skip the scan entirely, so a rule whose omission
    is silent cannot live behind a read. Verified 2298/2298 lines accounted for.

Fixed — native_chart(value_fmt=...) now rejects the wrong format dialect

  • value_fmt is written straight into the chart's EXCEL number-format code, while iso_bars takes the
    Python dialect, and neither docstring said so — a {:,.0f} handed to native_chart was printed raw
    onto a shipped slide. It now raises a named ValueError. Pre-existing, unrelated to the refactor.

Added — TITLE-RULE MONOCULTURE lint (the title-chrome rotation rule is now GATED, not prose-only)

  • The render self-check has long said "the title CHROME is not one fixed template repeated on every
    slide — rotate 2-3 treatments", but it lived only in prose and got missed (a head()-style build
    helper that stamps one eyebrow+rule on all 12 content slides is exactly how it regresses; a critic
    pass didn't catch it either). scripts/lint_deck.py now detects it deterministically: a per-slide
    title_rule_y feature + a deck-level check that fires TITLE-RULE MONOCULTURE when the same thin
    rule sits under the title at the same height on >60% of content slides (rules must CLUSTER at one
    height, so a deck that rotates rule/tab/rail/ordinal never trips it; filled eyebrow tabs and vertical
    accent rails are excluded by geometry). Mirrors the existing BOTTOM-STRIP MONOCULTURE gate. SKILL.md
    render self-check "Titles" now references the backstop. Verified: fires 10/10 on an all-rule deck,
    clean on a rotated one.

v3.9.0

Choose a tag to compare

@dong845 dong845 released this 22 Jul 04:33

Since v3.8.0. This release makes the direction gate show real styles and carry the chosen look through the whole deck — and keeps bold, bespoke design first-class.

Design capability, verified: a picked preset fixes only the register; the boldness/signature-move gate still fires, and a bespoke register invented for the content is a first-class peer of a preset. Confirmed by a 12-agent integration workflow (pass) and a security scan.

Changed — the direction gate is PRESET-DRIVEN (real styles, not colour schemes)

  • The gate now shows the best-fit named presets rendered with their own DNA (signature motif), not three synthesised palette/font combos — fixing "the options were just different colours". archetypes_html.preset_directions([names]) + _dna_cover() render each preset's motif (Swiss ghost numeral, Ink-wash 印 seal, Blueprint grid, Bauhaus triad, …).
  • 4 new presets → 18 total: bauhaus, midcentury, terminal, synthwave (all code-only, full guard/when/image_prompt parity), chosen from a design-movement web sweep.

Added — the chosen style runs through EVERY slide, not just the cover (的风格要走所有页)

  • _dna_ambient() renders a quiet register signature (corner mark / edge rule / faint grid-scanline / recurring numeral-seal) on every interior slide, behind content. Self-verify item (q) enforces it end-to-end via a new interior register: contract field + a register_interiors critic check + a PRE-FLIGHT tick — a deck styled only on the bookends is now a finding.
  • The quiet register (repeats every slide = SYSTEM) is distinguished from the loud signature motif (dosed 2–3, never stamped) across every gate, so the new behaviour doesn't trip the block-sameness rules.

Changed — branch-aware gate count + bespoke registers are first-class

  • No image tool → 4 rendered directions (3 DNA presets + 1 pure colour-scheme direction); with image tool → 3. build_directions_html auto-letters "describe your own" (D on a 3-up, E on a 4-up).
  • A direction dict may carry cover_motif + ambient_motif so a bespoke register renders its OWN real DNA in the preview — presets are the floor you beat, not the menu you satisfy.

Changed — generated imagery must FUSE style AND stay topical

  • New 🔴 FUSION gate: every generated hero/plate must be both unmistakably on-style and depict the deck's subject; on-style-but-generic filler fails and is regenerated.

Security

  • Hardened a docs API-key placeholder (sk-...$(cat ~/.openai_key) / DUMMY_KEY). Investigation confirmed no real key was ever hardcoded or committed — scripts read from env vars; re-scan clean.

v3.8.0

Choose a tag to compare

@dong845 dong845 released this 22 Jul 00:59

Since v3.7.0. Every claimed symbol was verified to exist in the shipped scripts.

One behaviour change to know: at boldness: bold/experimental, a surviving timid/sanded distinctiveness verdict is now blocking-until-the-user-waives-it — the waiver moves from the agent to the person who set the dial. conservative/balanced+ unchanged.

Added — native 2.5D isometric components (no generated image)

  • deckkit.iso_bars — a faithful 2.5D bar chart: extrusion height is linear in the value and
    zero-based, so the depth never distorts the data (this is why the projection is parallel, not
    perspective — a perspective chart would foreshorten the far bars and lie). Rejects negatives and

    9 bars with an actionable message; highlight= pops one bar in hi_color= (default the deck's
    emphasis hue).

  • deckkit.iso_stack — a layered architecture / disclosure ladder / decision hierarchy: floating
    isometric slabs with a label aligned beside each. Caps at ~6 layers (raises, like org_tree).
  • deckkit.iso_prism — one extruded isometric block as a hero; returns a cleared apex anchor
    above the whole top face so a caller can seat a label without landing it on the rhombus.
  • Fixed projection (true 30° isometric) and one-light-source face shading (top = base, right ×0.80,
    left ×0.55) so every 2.5D element in a deck reads as one system. Text sits beside the geometry —
    python-pptx cannot shear text onto a tilted face. Scenario gates live in form-selection.md:
    2.5D trades precision for presence (the constant top-face pedestal compresses ratios, an 8×
    reads ~4.8×), so it is a pitch/launch/keynote move, never a research/defense results comparison, and
    the more the room rewards rigor the worse the trade. Dose like generated imagery — one 2.5D moment
    per deck. Complementary to the generated-image branch: native = crisp, editable, data-bearing;
    generated = soft, organic, atmospheric.

Added — component_audit.py: did this deck hand-roll a form the library implements?

  • Reads the build script (which components were called, via dk.x(), an alias, or
    from deckkit import x) + the finished pptx (geometry signatures: bar row · abutting 100% band ·
    tile row · marker row) and names the component whose geometry a hand-roll matches. Advisory,
    never a blocker
    — a bespoke composition is the signature move; what the tool states as fact is
    the usage ratio and the specific match. Motivation, measured: across three delivered decks the
    build scripts called 3 of 59 form components; everything else was raw box+text, re-inheriting the
    geometry bugs the components fix. Run at PRE-FLIGHT 12.
  • Suppression is derived from deckkit's source (which functions draw rects) intersected with the
    form catalogue — a hand-kept list was wrong twice in two edits (columns() returns rects and draws
    nothing; table() emits a GraphicFrame), each time silencing real detections deck-wide. A
    deck that was never opened now prints NOT CHECKED and exits 1 rather than reporting clean.

Added — deck-level gates: RULE_THROUGH_TEXT · composition axis · signature proof

  • RULE_THROUGH_TEXT (build-time lint CRITICAL) — a decorative rule/divider drawn through a
    text block's ink, always caused by a hand-picked y the text later grew into. Neither existing
    lint caught it (a thin box over text is not text-on-text, not overflow, not invisible text); it
    shipped twice in one delivered deck and was caught by the user. Derive the rule from the block's
    measured end, never a coordinate.
  • The direction gate diverges on COMPOSITION, not just palette — the token set gained cover
    and skeleton (rendered faithfully to the canonical skeleton vocabulary), divergence is now a
    pairwise ≥2 of {palette · type · density · composition} rule with lock-and-redirect, and a new
    directions_diversity.py measures it (never auto-kills — rediverge or justify on the gate line).
    The picked composition is carried into the built deck (the cover token becomes the cover's
    layout, skeleton becomes the rhythm map's plurality), not discarded at style.py.
  • The SIGNATURE PROOF opens Step 4 — author the signature slide first and render just that page
    (render_deck.py --slides N) before authoring the rest, so the pixels that honour or sand the
    boldness:/signature move: contract arrive when the decision is cheap to change, not after the
    whole deck is built. signature move: now carries a carried_by: clause (2–3 slides where the
    idea does structural work), verified through to the critic.

Changed — blandness can block, at the user's own dial

  • The critic's distinctiveness axis was MAJOR-at-most (a deck asked to be bold could ship
    forgettable with a footnote while a 4pt overflow blocked). At boldness: bold/experimental a
    surviving timid/sanded verdict is now blocking-until-the user waives it — the waiver moves
    from the agent to the person who set the dial. conservative/balanced+ unchanged.

Fixed

  • render_deck.py --slides N[,M] renders only the named pages (the signature-proof path);
    byte-identical to those pages from a full render, leaves no cache, mutually exclusive with --fast
    and --deliverables.
  • lint_deck.py is iso-aware — two textless freeform polygons that overlap are the faces of one
    vector drawing (an iso solid, any freeform illustration), not a card collision; a pure-2.5D slide
    no longer drowns the linter in self-overlap noise. A text card on a prism still flags.
  • A long chain of self-inflicted defects found by adversarial review across these features — an
    over-suppressing audit list, a green pre-flight for an unopened deck, a composition axis that
    stopped at the preview, an iso_prism return that produced invisible labels, iso_stack labels
    drifting into the trough beside the next slab — each fixed and locked with a regression.
  • Whole-skill maturity audit (six lenses, adversarially verified) closed the last integration
    gaps from the recent gate additions: a composition skeleton token that could crash the
    direction gate
    (a 5-value hard-error set feeding an 8-value rhythm-map vocabulary — now accepts
    all eight); a dead rule where the composition tokens were produced and routed to the design
    lens but the critic brief never consumed them (now wired producer→consumer→arbiter with a verdict
    slot); and reference drift behind the code (small_multiples told the agent to hand-compose what its
    own index forbids; the design-gallery catalogue omitted four real components; modbox had no
    when-to-use trigger). Plus disambiguation tie-breakers (hub_spoke/hub_spokes, scorecard/
    kpi_card) and every emitted lint code now documented.

5 sections · Added · Added · Added · Changed · Fixed

v3.7.0

Choose a tag to compare

@dong845 dong845 released this 21 Jul 08:58

Since v3.6.0. Full detail below; every claimed symbol was verified to exist in the shipped scripts.

Two behaviour changes to know before upgrading

  • render_deck.py no longer writes <deck>.pdf / viewer.html by default — pass --deliverables (alias --final) at hand-off. They went stale the moment the .pptx changed, and a stale PDF is worse than an absent one.
  • OLDSTYLE_FIGURES no longer blocks a build; it warns, and the defect is prevented inside the components instead. A build script that wants it fatal asserts over its own finished file.

Added — design components the form catalogue kept prescribing and could not draw

  • deckkit.small_multiples — a grid of identical mini native charts on ONE SHARED value axis,
    with an optional hero panel. The cheapest correctness win in the set: data-viz.md and
    form-selection.md both prescribe shared scales, but composing native_chart per panel by hand
    let PowerPoint auto-scale each one, so a small bump and a huge bump rendered identically — a
    silent misreading no geometry lint could see. The regression asserts the axis contract itself.
  • deckkit.position_map — N labelled items on two continuous axes, with greedy label
    anti-collision and a ValueError naming any pair it cannot separate. quadrant() returns four
    cells and discards the within-cell position that is usually the whole argument; native_bubble
    drops the labels.
  • deckkit.annotated_figure — a real figure + numbered fractional-coordinate markers + a
    numbered caption rail + an optional magnified inset (via Picture.crop_*, no image processing).
    The inset picks the first corner that covers no marker. This is the form the skill's own
    integral-figure philosophy most insists on and least supported.
  • deckkit.org_tree — tidy hierarchy layout: post-order centroid placement (the part that stops
    being hand-placeable at depth 3), horizontal bus connectors, and a hard ValueError when it cannot
    fit legibly rather than silently squeezing.
  • image_fx.quiet_region(path) — grid luminance-variance scan returning the image's calmest
    single-ink region plus its mean luminance, so title placement and ink colour are measured
    rather than eyeballed. Two fixes found by running it on real photographs: the growth threshold
    anchors on the image's own variance distribution (one flat cell otherwise made every gradient
    neighbour look "busy"), and growth requires luminance coherence — a full-height column spanning
    dark sky and light ground averages to a mid-luminance where neither ink is safe.
  • deckkit.design_intent(slide, envelope=, rhyme=, reason=) — the declared-register channel that
    three lint messages already promised ("record the quiet-register exception") with nowhere to record
    it. Stored as an invisible zero-ink tag shape, so intent travels inside the .pptx to the
    render-time lint with no side-channel file. Abuse is audited (INTENT INFLATION).
  • deckkit.pic_alpha(picture, pct) — native picture opacity via a:alphaModFix. Against the
    scrim-overlay approach the A/B is unambiguous: the image keeps its own hues instead of being tinted
    toward the scrim colour, and there is no second full-bleed shape.

Added — --fast incremental render

  • render_deck.py --fast re-renders only the slides that changed. Profiling an 18-slide deck:
    build 1.8s, LibreOffice→PDF 9.1s, rasterize 24.4s cold. Rasterization, not the PDF export, is the
    larger cost. Measured: 12.3s → 4.7s for a one-slide change, 0.07s when nothing changed;
    outputs byte-identical to a full render.
  • Each slide is fingerprinted (its XML + rels + the bytes of the media it references) and mixed with
    a deck-global digest (presentation.xml, theme, masters, layouts). That digest closes a hole
    found by testing: flipping the canvas 16:9→4:3 changes no slide XML, so --fast previously
    reported "no slide changed" and left every PNG at the wrong aspect ratio.
  • Correctness over speed throughout — a stale PNG is worse than a slow render, because the lint
    and the visual critic both trust render/*.png to BE the deck. Full-render fallbacks (each with a
    printed reason) for: changed slide count, hidden slides, auto slide-number fields, or no cache.
    Cache writes are atomic and delete the cache on failure.
  • Hardened against an adversarial audit (27 candidates → 5 must-fix, all reproduced): hidden slides
    broke the page↔slide mapping on the pre-existing full path too; an unresolvable part hashed to a
    constant so real edits stopped registering; the subset PDF collided between concurrent runs;
    --fast --deliverables exited 0 having produced no PDF (now rejected at parse time); and media
    owned by a layout/master/theme was invisible to the digest.

Added — deck-level design gates (the second design-capability tranche)

  • ENVELOPE MONOCULTURE — fires when >60% of interior slides end their content at the same
    height. A deck can vary every form and still read as one template because every page fills the
    same rectangle; this is judged as a distribution, not per slide.
  • REGISTRATION DRIFT — consecutive title tops drifting 0.02–0.12in. Identical is calm and a
    deliberate jump is a decision; a sub-visible wobble is neither, and reads as a twitch when
    advancing.
  • INHERITED_EFFECT — warns when a shape bypasses the theme-shadow strip (below).
  • DEAD BOTTOM's per-slide floor drops 0.62 → 0.45, so only the genuine accident fires; the
    0.45–0.62 band is a legitimate upper envelope and is now judged by the distribution check instead.

Changed — OLDSTYLE_FIGURES is a WARN; the defect is prevented in the components

  • v3.6.0 shipped this as a hard build blocker. It is now a WARN, and the defect is prevented at
    the source instead: numeral_run_face() resolves a lining face inside every component that emits a
    figure, so correct output is the default rather than a rule authors must satisfy.
  • Why the architecture changed rather than the threshold: a fourth audit round found more
    defects than the third (12 non-minor vs 5), three self-inflicted by the previous fix round. As a
    blocker the rule has an unbounded false-positive surface — every component × every font × every
    string shape — while being structurally blind to where numbers most often live: tables and native
    charts report has_text_frame=False, so their numeric cells and tick labels were never checked at
    all. "7" (a single digit, which cannot bob), "10x" and "H1 2026" were blocked; a cover whose
    title IS a year had no fix available to its author.
  • A project that wants it fatal asserts over its own finished file — which is what a build script can
    now do in three lines.

Changed — numeral faces resolve from the deck's design (visible change)

  • big_numeral's default face changed. It was hard-coded serif="Georgia"; it now resolves
    through deckkit.numeral_face(), which keeps a lining SERIF (Times New Roman) by default. The
    register is preserved — an oversized italic serif figure, as before — but the digits no longer
    sit at mixed heights. A deck rebuilt from an older build script will render its marker numerals
    in a different face.
    serif= still overrides, except that an old-style face is substituted
    (lint_layout would reject it).
  • stat_row, scorecard, kpi_card and change_stat route their FIGURE runs through the same
    resolver, keeping each component's own default (body font) while guaranteeing lining digits.
    Previously these inherited a body/display face and could hard-fail their own output on a deck
    whose font is Georgia.
  • Which faces count as old-style is measured from the installed font rather than read off a
    list; the curated set is only a fallback for fonts the machine does not have.

Changed — the PDF and viewer.html are reserved hand-off deliverables

  • They were produced on every render and parked at the deck root. A deck is rebuilt each critic
    round and then usually hand-edited in PowerPoint, so both went stale the moment the .pptx changed —
    and a stale PDF is worse than an absent one, because someone opens it and reviews the wrong deck.
  • render_deck.py now takes --deliverables (alias --final), off by default. The PDF still
    exists as a render intermediate inside render/; the flag is what promotes it to the deck root and
    writes the viewer. The default run prints a one-line reminder of how to produce them at hand-off.
    --fast and --deliverables are mutually exclusive, enforced at flag-parse time.

Security — the metered image rung asks before it bills

  • The generated-template branch had three rungs (native imagegen → codex CLI → OPENAI_API_KEY)
    governed by one rule: never block on a choice when a working path is present. Right for the two
    subscription rungs, wrong for the third — it treats a present API key as authorisation. On a
    machine with no codex CLI but an exported key, the skill would have started billing per image
    without asking.
  • BILLING GATE: rung 3 is metered, an available key is NOT consent, ask before the first paid
    call — a red stop explicitly not waived by a per-deck auto directive (delegation covers
    preferences, never the user's money) — and on a decline fall back to a native look rather than
    spending. agents/asset-prep.md (a subagent brief, which could have spent money with no way to
    ask) and generate_images_codex.py's own failure message are corrected to match.
  • Claude Code's real cost is now documented: CC has no native image tool, so the codex CLI IS the
    path there — free on the subscription, one codex login. Q1 probes for a free path before
    offering the branch, so a user cannot pick a look nothing can generate.

Fixed — the inherited theme shadow under every generated shape

  • python-pptx stamps `<p:style><a:effectRef idx=...
Read more

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.

v3.5.1

Choose a tag to compare

@dong845 dong845 released this 20 Jul 01:29

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.

Full changelog: https://github.com/addsumtech/slides_maker/blob/main/CHANGELOG.md