diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 0501a1c..a7a60ab 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "designmd-pptx", - "version": "2.1.1", + "version": "2.1.2", "description": "Compile awesome-design-md / Stitch DESIGN.md into officecli PPTX tokens, ordered decks, and staging-safe apply.", "author": { "name": "Min-Gul Kim", diff --git a/CHANGELOG.md b/CHANGELOG.md index a99fc4a..56278a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to designmd-pptx are documented here. +## [2.1.2] — 2026-07-15 + +Residual fixes after Codex **re-verify** of v2.1.1 (still flagged generative body loss, +placement NaN, Windows uninstall fail-open, methodology honesty). + +### Fixed +- Generative recipe maps preserve card bodies (`title — body`) + `overflow.cards` +- Placement validation: finite coords, min size, non-empty text, identical-box reject +- Windows uninstall fail-closed on missing/invalid/mismatched manifest; SHA warn + `DESIGNMD_REQUIRE_OFFICECLI_SHA=1` +- Generated public benchmark METHODOLOGY honesty banner +- VS Code shared OutputChannel (no per-command leak) +- `generate --layout-cmd`; docs: pulse removed; smoke checklist + +### Smoke +- `scripts/smoke-v2.1.1.sh` (version gate 2.1.2) track A PASS on Darwin +- Windows install + real PPT open: manual (docs/smoke-v2.1.1.md B/C) + ## [2.1.1] — 2026-07-15 Adversarial review follow-up (Codex gpt-5.6-sol@high **BLOCK** findings). diff --git a/README.md b/README.md index c5c082d..1cf4f1e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # designmd-pptx [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -[![Version](https://img.shields.io/badge/version-2.1.1-brightgreen)](plugin.json) -[![Release](https://img.shields.io/badge/release-v2.1.1-blue)](https://github.com/kimmingul/designmd-pptx/releases/tag/v2.1.1) +[![Version](https://img.shields.io/badge/version-2.1.2-brightgreen)](plugin.json) +[![Release](https://img.shields.io/badge/release-v2.1.2-blue)](https://github.com/kimmingul/designmd-pptx/releases/tag/v2.1.2) **awesome-design-md / Stitch `DESIGN.md` → [OfficeCLI](https://github.com/officecli/officecli) PPTX** — packaged for **Claude Code, OpenAI Codex, and Grok Build** (**v2.1**). diff --git a/docs/commands.md b/docs/commands.md index 9f42350..9228e8d 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -78,9 +78,10 @@ python -m designmd_pptx scaffold default -o out/v2 \ --content generated/content.generated.deck.json ``` -Freeform placements are validated by `layout.solve_adaptive` before emission -so title/body floors still hold. Optional external layout generator: -`DESIGNMD_LAYOUT_CMD` / vision contact sheet via `--contact`. +Freeform placements are validated (finite cm, on-canvas, non-empty text) before +emission. Optional external layout generator: env `DESIGNMD_LAYOUT_CMD` or +`--layout-cmd` (JSON placements/preset on stdout). Vision contact sheet via +`--contact`. ## animate (#40) @@ -93,7 +94,7 @@ animation: transition: fade # none | fade | push | wipe | cut | cover transition_speed: med stagger_ms: 150 - emphasis: none # none | pulse + emphasis: none # only 'none' is implemented ``` ```bash diff --git a/docs/design-md-v2.md b/docs/design-md-v2.md index a2dc7b5..5823798 100644 --- a/docs/design-md-v2.md +++ b/docs/design-md-v2.md @@ -48,7 +48,7 @@ animation: transition: fade # none | fade | push | wipe | cut | cover transition_speed: med # slow | med | fast stagger_ms: 150 - emphasis: none # none | pulse + emphasis: none # only 'none' implemented (v2.1.x) --- ``` diff --git a/docs/smoke-v2.1.1.md b/docs/smoke-v2.1.1.md new file mode 100644 index 0000000..e7b8e11 --- /dev/null +++ b/docs/smoke-v2.1.1.md @@ -0,0 +1,154 @@ +# Smoke checklist — designmd-pptx v2.1.2 + +Post-adversarial (Codex BLOCK → 2.1.2 fixes). Two tracks: + +1. **Automated local smoke** (any OS with Python 3.10+) — `scripts/smoke-v2.1.2.sh` +2. **Windows bootstrap smoke** (Windows 10/11 + PowerShell 5.1+) — section B +3. **PowerPoint / LibreOffice animation smoke** — section C + +Exit criteria for “production announce ready”: + +| Gate | Pass condition | +|---|---| +| A Automated | `scripts/smoke-v2.1.2.sh` exit 0 | +| B Windows | install → doctor → scaffold → uninstall without leftover product root | +| C Animation | animate → open in PPT/LO; transitions present; no repair dialog | + +--- + +## A. Automated local smoke + +```bash +# from repo root +./scripts/smoke-v2.1.2.sh +# or: +bash scripts/smoke-v2.1.2.sh +``` + +Covers: + +- version == 2.1.2 + `npm run check` exact equality +- unit suite (or fast subset if `SMOKE_FAST=1`) +- refine multi-slide split stability +- extract `barDir=col` → column +- generative overflow preserve + placement reject +- animation CT_Slide order + force required +- public synthetic suite (`--public-n 30`) with recipe smoke notes +- windows installer structural markers (`windows-install --check-script`) +- VS Code cli.js argv injection resistance (node) + +--- + +## B. Windows bootstrap smoke (manual / VM) + +**Host:** clean Windows 10/11 user (no admin required). + +```powershell +# 1) Clone or download Install-DesignmdPptx.ps1 from v2.1.2 tag +cd path\to\designmd-pptx +powershell -ExecutionPolicy Bypass -File packaging\windows\Install-DesignmdPptx.ps1 -DryRun + +# 2) Real install (network: winget/PyPI/GitHub) +powershell -ExecutionPolicy Bypass -File packaging\windows\Install-DesignmdPptx.ps1 + +# 3) New terminal — shim on PATH +designmd-pptx --help +designmd-pptx doctor +designmd-pptx scaffold default -o $env:TEMP\dmd-smoke --content python\examples\content.deck.json + +# 4) Safe-root guard (must FAIL) +powershell -ExecutionPolicy Bypass -File packaging\windows\Install-DesignmdPptx.ps1 ` + -Uninstall -InstallRoot C:\Windows\Temp\evil +# expect: InstallRoot must be under ...\designmd-pptx + +# 5) Uninstall +powershell -ExecutionPolicy Bypass -File packaging\windows\Install-DesignmdPptx.ps1 -Uninstall +# product root gone; officecli-official may remain (documented) +``` + +Optional integrity: + +```powershell +# If you have the expected tarball hash: +$env:DESIGNMD_OFFICECLI_SHA256 = "" +powershell -ExecutionPolicy Bypass -File packaging\windows\Install-DesignmdPptx.ps1 +``` + +**Record:** OS build, Python source (existing vs winget), doctor output, any PATH issues. + +--- + +## C. PowerPoint / LibreOffice animation smoke + +Requires a materialised `.pptx` (legacy OfficeCLI `apply` or an existing deck). + +```bash +# After scaffold --apply (legacy binary) OR any valid deck.pptx: +PYTHONPATH=python python -m designmd_pptx animate path/to/deck.pptx \ + -o /tmp/deck.animated.pptx --entrance fade --transition fade --force + +# Inspect OOXML order without Office: +python - <<'PY' +import zipfile +from lxml import etree +from pathlib import Path +p = Path("/tmp/deck.animated.pptx") +with zipfile.ZipFile(p) as z: + xml = z.read("ppt/slides/slide1.xml") +root = etree.fromstring(xml) +locals_ = [c.tag.split("}")[-1] for c in root] +print(locals_) +assert "transition" in locals_ and "timing" in locals_ +assert locals_.index("transition") < locals_.index("timing") +if "extLst" in locals_: + assert locals_.index("timing") < locals_.index("extLst") +print("order OK") +PY +``` + +**In PowerPoint / LibreOffice Impress:** + +1. Open `deck.animated.pptx` — no repair / “unreadable content” dialog. +2. Slide Show → transitions visible (fade). +3. Select title shape → Animation pane shows entrance effect (or equivalent). +4. Save-as new file succeeds. + +**In-place force guard:** + +```bash +# must refuse without --force +PYTHONPATH=python python -m designmd_pptx animate deck.pptx -o deck.pptx --entrance fade +# expect: error/notes about --force +``` + +--- + +## D. Sign-off template + +```text +Date: +Operator: +Commit/tag: +A automated smoke: PASS / FAIL (log: ) +B Windows install: PASS / FAIL / SKIP (reason: ) +C PPT/LO animation: PASS / FAIL / SKIP (reason: ) +Notes: +Announce: YES / NO +``` + +--- + +## E. Recorded run (this environment) + +```text +Date: 2026-07-15 +Operator: agent (macOS Darwin) +Commit/tag: v2.1.2 (b6ba429) +A automated smoke: PASS — bash scripts/smoke-v2.1.2.sh +B Windows install: SKIP — no Windows/pwsh on host (manual VM required) +C PPT/LO animation: PARTIAL — OOXML order verified programmatically + tags: cSld, clrMapOvr, transition, timing + officecli/soffice/libreoffice: not on PATH (no real slideshow open) +Codex re-verify (gpt-5.6-sol@high): see .grok/artifacts/verify/codex-gpt-5.6-sol-reverify-v2.1.2.md +Announce: conditional on B+C human sign-off if claiming full production Windows/PPT +``` diff --git a/docs/windows-installer.md b/docs/windows-installer.md index 04ec7d9..9b4a8f8 100644 --- a/docs/windows-installer.md +++ b/docs/windows-installer.md @@ -21,7 +21,7 @@ powershell -ExecutionPolicy Bypass -File Install-DesignmdPptx.ps1 -DryRun | `-DryRun` | Print actions only | | `-SkipOfficeCli` | Skip pinned OfficeCLI download | | `-SkipPath` | Do not modify user PATH | -| `-PackageSource X` | default `designmd-pptx==2.1.1` (pinned); or local path/wheel | +| `-PackageSource X` | default `designmd-pptx==2.1.2` (pinned); or local path/wheel | | `-OfficeCliSha256` | Optional expected SHA-256 of the officecli-dist tarball | | `-InstallRoot PATH` | Must stay under `%LOCALAPPDATA%\designmd-pptx` | diff --git a/editor/vscode/extension.js b/editor/vscode/extension.js index 4bdee05..7d225be 100644 --- a/editor/vscode/extension.js +++ b/editor/vscode/extension.js @@ -13,9 +13,14 @@ const { resolveCli: resolveCliPure, diagnosticsFromReport } = require("./cli"); const DIAG_COLLECTION = "designmd-pptx"; /** @param {vscode.ExtensionContext} context */ +/** @type {vscode.OutputChannel | undefined} */ +let sharedOutput; + function activate(context) { const diagnostics = vscode.languages.createDiagnosticCollection(DIAG_COLLECTION); context.subscriptions.push(diagnostics); + sharedOutput = vscode.window.createOutputChannel("designmd-pptx"); + context.subscriptions.push(sharedOutput); const explorer = new DesignmdExplorer(); context.subscriptions.push( @@ -109,7 +114,7 @@ function resolveCli(args) { function runCli(args, opts = {}) { const { argv, cwd, env, display } = resolveCli(args); const name = opts.title || `designmd-pptx ${args[0] || ""}`.trim(); - const out = vscode.window.createOutputChannel("designmd-pptx", { log: true }); + const out = sharedOutput || vscode.window.createOutputChannel("designmd-pptx"); if (opts.reveal !== false) { out.show(true); } diff --git a/package.json b/package.json index 7ade9f1..1c45697 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "designmd-pptx", - "version": "2.1.1", + "version": "2.1.2", "description": "DESIGN.md → officecli PPTX compiler, packaged for Claude Code / Codex / Grok Build (v2.1)", "license": "MIT", "private": true, diff --git a/packaging/windows/Install-DesignmdPptx.ps1 b/packaging/windows/Install-DesignmdPptx.ps1 index 0c73fe8..d5ce816 100644 --- a/packaging/windows/Install-DesignmdPptx.ps1 +++ b/packaging/windows/Install-DesignmdPptx.ps1 @@ -45,7 +45,7 @@ param( [switch]$SkipOfficeCli, [switch]$SkipPath, # Pin package by default (adversarial #35) — override with -PackageSource for editable installs - [string]$PackageSource = "designmd-pptx==2.1.1", + [string]$PackageSource = "designmd-pptx==2.1.2", [string]$InstallRoot = "", [string]$OfficeCliPin = "", # empty → read from embedded default / env [string]$OfficeCliSha256 = "", # optional expected SHA-256 of the tarball @@ -236,8 +236,10 @@ function Install-OfficeCliPin { throw "OfficeCLI tarball SHA-256 mismatch: got $hash expected $expectSha" } Write-Ok "SHA-256 verified" + } elseif ($env:DESIGNMD_REQUIRE_OFFICECLI_SHA -eq "1") { + throw "DESIGNMD_REQUIRE_OFFICECLI_SHA=1 but no -OfficeCliSha256 / DESIGNMD_OFFICECLI_SHA256 provided" } else { - Write-Info "No DESIGNMD_OFFICECLI_SHA256 / -OfficeCliSha256 set — skipping hash verify (pin URL only)" + Write-Host " WARN pin URL only — set DESIGNMD_OFFICECLI_SHA256 for supply-chain verify" -ForegroundColor Yellow } # tar is available on Windows 10+; extract only under our temp root (path safety) $extract = Join-Path $env:TEMP "designmd-officecli-extract-$pin" @@ -390,26 +392,30 @@ function Remove-UserPathEntry { function Invoke-Uninstall { Write-Step "Uninstall $ProductName from $InstallRoot" Assert-SafeInstallRoot $InstallRoot - # Require product marker unless dry-run of empty tree - if (-not $DryRun -and (Test-Path $InstallRoot) -and -not (Test-Path $Manifest)) { - throw "Refusing uninstall: $Manifest missing (not a designmd-pptx install root)" - } if ($DryRun) { Write-Info "dry-run: remove $InstallRoot and PATH entry" return } $pathModified = $false - if (Test-Path $Manifest) { - try { - $m = Get-Content $Manifest -Raw | ConvertFrom-Json - $pathModified = [bool]$m.path_modified - if ($m.product -and $m.product -ne $ProductName) { - throw "Refusing uninstall: manifest product is $($m.product)" - } - } catch { - if ("$_" -match "Refusing") { throw } + if (-not (Test-Path $Manifest)) { + throw "Refusing uninstall: $Manifest missing (not a designmd-pptx install root)" + } + try { + $m = Get-Content $Manifest -Raw | ConvertFrom-Json + } catch { + throw "Refusing uninstall: manifest is not valid JSON: $_" + } + if (-not $m.product -or $m.product -ne $ProductName) { + throw "Refusing uninstall: manifest product is '$($m.product)' (want $ProductName)" + } + if ($m.paths -and $m.paths.root) { + $manifestRoot = [System.IO.Path]::GetFullPath([string]$m.paths.root) + $wantRoot = [System.IO.Path]::GetFullPath($InstallRoot) + if (-not $manifestRoot.Equals($wantRoot, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing uninstall: manifest root $manifestRoot != InstallRoot $wantRoot" } } + $pathModified = [bool]$m.path_modified if ($pathModified -or -not $SkipPath) { Remove-UserPathEntry } diff --git a/packaging/windows/designmd-pptx.iss b/packaging/windows/designmd-pptx.iss index 7483ca8..f43c258 100644 --- a/packaging/windows/designmd-pptx.iss +++ b/packaging/windows/designmd-pptx.iss @@ -8,7 +8,7 @@ ; with -Uninstall. #define MyAppName "designmd-pptx" -#define MyAppVersion "2.1.1" +#define MyAppVersion "2.1.2" #define MyAppPublisher "designmd-pptx contributors" #define MyAppURL "https://github.com/kimmingul/designmd-pptx" diff --git a/plugin.json b/plugin.json index c72893f..f76f3a6 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "designmd-pptx", - "version": "2.1.1", + "version": "2.1.2", "description": "DESIGN.md → officecli PPTX compiler for agentic AI: compose markdown briefs into decks, extract/restyle existing decks, brand slide masters, Gate 3 screenshot QA. Grok Build manifest of the multi-platform (Claude Code / Codex / Grok) plugin.", "author": { "name": "Min-Gul Kim", diff --git a/python/designmd_pptx/__init__.py b/python/designmd_pptx/__init__.py index aff84a0..2548e49 100644 --- a/python/designmd_pptx/__init__.py +++ b/python/designmd_pptx/__init__.py @@ -1,3 +1,3 @@ """DESIGN.md (awesome-design-md / Stitch) → officecli PPTX slide tokens & recipes.""" -__version__ = "2.1.1" +__version__ = "2.1.2" diff --git a/python/designmd_pptx/__main__.py b/python/designmd_pptx/__main__.py index 8dc4b73..92889c1 100644 --- a/python/designmd_pptx/__main__.py +++ b/python/designmd_pptx/__main__.py @@ -792,6 +792,10 @@ def cmd_generate(args: argparse.Namespace) -> int: tokens = compile_design_md(_resolve_design(args.design)) directive = args.directive or args.style + layout_cmd = getattr(args, "layout_cmd", None) + if layout_cmd: + import os + os.environ["DESIGNMD_LAYOUT_CMD"] = str(layout_cmd) if args.profile: report = gen.generate_deck_layout( deck, @@ -1362,6 +1366,9 @@ def build_parser() -> argparse.ArgumentParser: help="Contact-sheet PNG for vision-driven re-layout") gn.add_argument("--vision-plan", type=Path, default=None) gn.add_argument("--vision-cmd", default=None) + gn.add_argument("--layout-cmd", default=None, + help="External layout generator command (or set DESIGNMD_LAYOUT_CMD); " + "stdin JSON content, stdout {placements|preset}") gn.add_argument("--rounds", type=int, default=2, help="Max generate+refine rounds (default 2)") gn.set_defaults(func=cmd_generate) diff --git a/python/designmd_pptx/backend.py b/python/designmd_pptx/backend.py index e428c88..7d1b954 100644 --- a/python/designmd_pptx/backend.py +++ b/python/designmd_pptx/backend.py @@ -318,7 +318,7 @@ def _ensure_proc(self) -> subprocess.Popen: daemon=True, ).start() init = self._call("initialize", { - "clientInfo": {"name": "designmd-pptx", "version": "2.1.1"}, + "clientInfo": {"name": "designmd-pptx", "version": "2.1.2"}, }) self._caps = None self._server = init diff --git a/python/designmd_pptx/compile.py b/python/designmd_pptx/compile.py index 955fb4c..d1b04aa 100644 --- a/python/designmd_pptx/compile.py +++ b/python/designmd_pptx/compile.py @@ -13,7 +13,7 @@ from .colors_parse import collect_css_vars from .validate import validate_content_overlay, validate_tokens_against_schema_file -COMPILER_VERSION = "2.1.1" +COMPILER_VERSION = "2.1.2" DEFAULT_PATTERNS = [ "cover", diff --git a/python/designmd_pptx/generative.py b/python/designmd_pptx/generative.py index a1295f8..d42a4e9 100644 --- a/python/designmd_pptx/generative.py +++ b/python/designmd_pptx/generative.py @@ -387,9 +387,12 @@ def validate_builder( def validate_placements(placements: list[dict[str, Any]]) -> tuple[bool, str]: - """Geometry sanity for freeform placements (cm, on-canvas, non-empty).""" + """Geometry sanity for freeform placements (cm, on-canvas, finite, non-empty).""" + import math + if not placements: return False, "empty placements" + boxes: list[tuple[float, float, float, float]] = [] for i, p in enumerate(placements): if not isinstance(p, dict): return False, f"placements[{i}] not an object" @@ -397,12 +400,25 @@ def validate_placements(placements: list[dict[str, Any]]) -> tuple[bool, str]: x, y, w, h = float(p["x"]), float(p["y"]), float(p["w"]), float(p["h"]) except (KeyError, TypeError, ValueError): return False, f"placements[{i}] missing numeric x/y/w/h" + if not all(math.isfinite(v) for v in (x, y, w, h)): + return False, f"placements[{i}] non-finite coordinate" if w <= 0 or h <= 0: return False, f"placements[{i}] non-positive size" + if w < 0.3 or h < 0.3: + return False, f"placements[{i}] undersized (<0.3cm)" if x < -0.5 or y < -0.5 or x + w > L.CANVAS_W + 0.5 or y + h > L.CANVAS_H + 0.5: return False, f"placements[{i}] outside canvas" if not p.get("name"): return False, f"placements[{i}] missing name" + # Text leaves need non-empty text for readability contract + kind = str(p.get("kind") or "text") + if kind == "text" and not str(p.get("text") or "").strip(): + return False, f"placements[{i}] text empty" + # Crude overlap: reject identical origin+size pairs + for j, (ox, oy, ow, oh) in enumerate(boxes): + if abs(x - ox) < 0.01 and abs(y - oy) < 0.01 and abs(w - ow) < 0.01 and abs(h - oh) < 0.01: + return False, f"placements[{i}] identical geometry to placements[{j}]" + boxes.append((x, y, w, h)) return True, "ok" @@ -562,23 +578,48 @@ def _apply_style_to_slide( return out, patch patch["freeform_overflow"] = val.overflow - # Pattern path with style-driven recipe map + # Pattern path with style-driven recipe map — never silent field drop (#21 re-verify) new_recipe = rmap.get(recipe, recipe) if new_recipe != recipe: - # light content reshape when swapping bullets ↔ feature_cards if recipe == "bullets" and new_recipe == "feature_cards": bullets = content.get("bullets") or [] content["cards"] = [ - {"title": str(b)[:48], "body": ""} for b in bullets[:4] + {"title": str(b)[:48], "body": str(b)} for b in bullets[:max_items] ] content.pop("bullets", None) elif recipe == "feature_cards" and new_recipe == "bullets": cards = content.get("cards") or [] - content["bullets"] = [ - str(c.get("title") or c.get("body") or c) if isinstance(c, dict) else str(c) - for c in cards - ][:max_items] + bullets_out: list[str] = [] + for c in cards[:max_items]: + if isinstance(c, dict): + title = str(c.get("title") or "").strip() + body = str(c.get("body") or "").strip() + if title and body: + bullets_out.append(f"{title} — {body}") + else: + bullets_out.append(title or body or str(c)) + else: + bullets_out.append(str(c)) + content["bullets"] = bullets_out + # Preserve full cards under overflow for recoverability + if cards: + content.setdefault("overflow", {})["cards"] = cards content.pop("cards", None) + elif recipe == "quote" and new_recipe == "bullets": + q = str(content.get("quote") or content.get("body") or "") + content["bullets"] = [q] if q else content.get("bullets") or ["—"] + content.setdefault("overflow", {})["quote"] = content.get("quote") + elif recipe == "table" and new_recipe in ("kpi_row", "feature_cards"): + # Keep table payload; do not invent KPIs from thin air + content.setdefault("overflow", {})["table"] = { + "headers": content.get("headers"), + "rows": content.get("rows"), + } + content.setdefault("notes", "") + content["notes"] = ( + (content.get("notes") or "") + + " [generative: table retained in overflow; review KPI mapping]" + ).strip() out["recipe"] = new_recipe patch["to_recipe"] = new_recipe patch["action"] = "recipe_map" diff --git a/python/designmd_pptx/public_benchmark.py b/python/designmd_pptx/public_benchmark.py index 00fc5d0..00b810b 100644 --- a/python/designmd_pptx/public_benchmark.py +++ b/python/designmd_pptx/public_benchmark.py @@ -496,6 +496,11 @@ def _methodology_markdown( recipes += f", … (+{len(meta.recipes_covered) - 40} more)" return f"""# Public benchmark methodology (v{meta.version}) +> **Honesty:** Synthetic deck-spec + **recipe-builder smoke** suite (CC0). +> Does **not** run OfficeCLI apply, screenshots, or live Gate 3 vision. +> `visual_gate_failure` here means recipe-smoke / offline structural gate, +> not a contact-sheet vision pass. + ## Rights - **License:** {meta.license} @@ -508,16 +513,17 @@ def _methodology_markdown( Each fixture is scored **before → after** with `python/designmd_pptx/benchmark_thresholds.json`: -| Metric | Max allowed (after) | -|---|---| -| corruption | 0 | -| extraction_loss | 5 | -| layout_failure | 0 | -| visual_gate_failure | 0 | -| a11y_error | 0 | +| Metric | Max allowed (after) | Meaning in this suite | +|---|---|---| +| corruption | 0 | No real pptx path in fixtures (always 0 unless path supplied) | +| extraction_loss | 5 | Synthetic ledger (empty) | +| layout_failure | 0 | Deck-spec structure + recipe builder smoke | +| visual_gate_failure | 0 | Offline recipe-smoke gate (not live vision) | +| a11y_error | 0 | After a11y auto-correct | **Before** side intentionally weakens contrast and strips image `alt`. -**After** side runs `a11y.auto_correct_contrast` + `ensure_notes_and_alt`. +**After** side runs `a11y.auto_correct_contrast` + `ensure_notes_and_alt` +plus `RECIPE_BUILDERS` materialisation smoke. ## Corpus construction diff --git a/python/tests/test_phase5_21_40_42.py b/python/tests/test_phase5_21_40_42.py index 522f9a3..4e73764 100644 --- a/python/tests/test_phase5_21_40_42.py +++ b/python/tests/test_phase5_21_40_42.py @@ -84,6 +84,36 @@ def test_overflow_preserved_not_dropped(self) -> None: self.assertTrue(ov.get("bullets") or content.get("notes", "").find("overflow") >= 0 or report["deck"]["slides"][0]["recipe"] == "freeform") + def test_feature_cards_to_bullets_keeps_body(self) -> None: + deck = { + "slides": [{ + "id": "s1", + "recipe": "feature_cards", + "content": { + "title": "T", + "cards": [ + {"title": "A", "body": "BODY_A"}, + {"title": "B", "body": "BODY_B"}, + ], + }, + }], + } + report = gen.generate_deck_layout(deck, profile_id="swiss") + slide = report["deck"]["slides"][0] + blob = json.dumps(slide) + self.assertIn("BODY_A", blob) + self.assertIn("BODY_B", blob) + + def test_placements_reject_nan_and_empty_text(self) -> None: + ok, _ = gen.validate_placements([ + {"name": "X", "x": float("nan"), "y": 0, "w": 1, "h": 1, "text": "hi", "kind": "text"}, + ]) + self.assertFalse(ok) + ok, _ = gen.validate_placements([ + {"name": "X", "x": 1, "y": 1, "w": 2, "h": 2, "text": "", "kind": "text"}, + ]) + self.assertFalse(ok) + def test_vision_density_forces_freeform(self) -> None: deck = { "slides": [{ diff --git a/python/tests/test_windows_installer.py b/python/tests/test_windows_installer.py index 4b0d8b8..51273d2 100644 --- a/python/tests/test_windows_installer.py +++ b/python/tests/test_windows_installer.py @@ -100,7 +100,7 @@ def test_ps1_acceptance_markers(self) -> None: "Invoke-Uninstall", "Get-OfficeCliUrl", "0.2.117", - "designmd-pptx==2.1.1", # pinned package + "designmd-pptx==2.1.2", # pinned package "Assert-SafeInstallRoot", "SHA256", "officecli.exe", # exact binary name (no ambiguous officecli*) diff --git a/scripts/smoke-v2.1.1.sh b/scripts/smoke-v2.1.1.sh new file mode 100755 index 0000000..99c0509 --- /dev/null +++ b/scripts/smoke-v2.1.1.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env bash +# Automated smoke for designmd-pptx v2.1.2 (any OS with Python 3.10+). +# Usage: bash scripts/smoke-v2.1.2.sh +# SMOKE_FAST=1 bash scripts/smoke-v2.1.2.sh # skip full unit suite +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" +export PYTHONPATH="${ROOT}/python${PYTHONPATH:+:$PYTHONPATH}" +fail=0 +pass() { echo " PASS $*"; } +bad() { echo " FAIL $*"; fail=1; } + +echo "== designmd-pptx smoke v2.1.2 ==" +echo "root: $ROOT" + +# ── version ────────────────────────────────────────────────────────────── +VER=$(python -c "from designmd_pptx import __version__; print(__version__)") +[[ "$VER" == "2.1.2" ]] && pass "version $VER" || bad "version $VER (want 2.1.2)" + +# ── npm check (exact equality) ─────────────────────────────────────────── +if command -v npm >/dev/null 2>&1; then + if npm run check >/tmp/dmd-smoke-npm.txt 2>&1; then + pass "npm run check" + else + bad "npm run check (see /tmp/dmd-smoke-npm.txt)" + fi +else + echo " skip npm not installed" +fi + +# ── unit tests ─────────────────────────────────────────────────────────── +if [[ "${SMOKE_FAST:-}" == "1" ]]; then + if python -m unittest \ + python.tests.test_phase5_21_40_42 \ + python.tests.test_refine \ + python.tests.test_vscode_extension \ + python.tests.test_windows_installer \ + -q 2>/tmp/dmd-smoke-unit.txt; then + pass "unit (fast subset)" + else + bad "unit fast subset (see /tmp/dmd-smoke-unit.txt)" + fi +else + if python -m unittest discover -s python/tests -q 2>/tmp/dmd-smoke-unit.txt; then + pass "unit full suite" + else + bad "unit suite (see /tmp/dmd-smoke-unit.txt)" + fi +fi + +# ── refine multi-slide ─────────────────────────────────────────────────── +python - <<'PY' || bad "refine multi-slide" +from designmd_pptx import refine +deck = {"slides": [ + {"id": "s1", "recipe": "bullets", "content": {"title": "A", "bullets": [f"a{i}" for i in range(8)]}}, + {"id": "s2", "recipe": "bullets", "content": {"title": "B", "bullets": [f"b{i}" for i in range(8)]}}, +]} +out, log = refine.apply_patches(deck, [{"code": "density", "severity": "error", "message": "x", "slide": None}], max_list_items=4) +assert len(out["slides"]) == 4, out +s2 = next(s for s in out["slides"] if s["id"] == "s2") +assert len(s2["content"]["bullets"]) == 4 +print("refine ok") +PY +pass "refine multi-slide split" + +# ── extract barDir ─────────────────────────────────────────────────────── +python - <<'PY' || bad "barDir" +from xml.etree import ElementTree as ET +from designmd_pptx.extract import _chart_type +xml = ''' + + +''' +assert _chart_type(ET.fromstring(xml)) == "column" +print("barDir ok") +PY +pass "extract barDir=col → column" + +# ── generative overflow + placement reject ─────────────────────────────── +python - <<'PY' || bad "generative" +from designmd_pptx import generative as g +from designmd_pptx import layout as L +deck = {"slides": [{"id": "s1", "recipe": "bullets", + "content": {"title": "T", "bullets": [f"P{i}" for i in range(10)]}}]} +r = g.generate_deck_layout(deck, profile_id="minimal") +c = r["deck"]["slides"][0]["content"] +assert c.get("overflow") or "overflow" in (c.get("notes") or "") or r["deck"]["slides"][0]["recipe"] == "freeform" +ok, why = g.validate_placements([{"name": "X", "x": -99, "y": 0, "w": 1, "h": 1}]) +assert not ok, why +print("generative ok") +PY +pass "generative overflow + placement validate" + +# ── animation order + force ────────────────────────────────────────────── +python - <<'PY' || bad "animation" +import zipfile, tempfile +from pathlib import Path +from designmd_pptx import animation as anim +from designmd_pptx import opc + +slide = b''' + + + + + + Hi + + + +''' +new, eff, tr = anim.inject_slide_animation(slide, entrance="fade", transition="fade", name_prefixes=["CoverTitle"]) +assert eff > 0 and tr == 1 +root = opc.parse(new) +locals_ = [c.tag.split("}")[-1] for c in list(root)] +assert locals_.index("clrMapOvr") < locals_.index("transition") +assert locals_.index("transition") < locals_.index("timing") +td = tempfile.mkdtemp() +src = Path(td) / "d.pptx" +with zipfile.ZipFile(src, "w") as zf: + zf.writestr("[Content_Types].xml", "") + zf.writestr("ppt/slides/slide1.xml", slide) +rep = anim.animate_pptx(src, out=src, animation={"enabled": True, "entrance": "fade"}, force=False) +assert not rep.ok and any("force" in n.lower() for n in rep.notes) +print("animation ok", locals_) +PY +pass "animation order + force" + +# ── public synthetic suite ─────────────────────────────────────────────── +if PYTHONPATH=python python -m designmd_pptx benchmark --public --public-n 30 -o /tmp/dmd-pb-smoke \ + >/tmp/dmd-smoke-pb.txt 2>&1; then + if grep -q "recipe_build" /tmp/dmd-smoke-pb.txt || grep -q "synthetic" /tmp/dmd-smoke-pb.txt; then + pass "public synthetic suite n=30" + else + bad "public suite missing honesty notes (see /tmp/dmd-smoke-pb.txt)" + fi +else + bad "public suite (see /tmp/dmd-smoke-pb.txt)" +fi + +# ── windows installer structural ───────────────────────────────────────── +if PYTHONPATH=python python -m designmd_pptx windows-install --check-script >/tmp/dmd-smoke-win.txt 2>&1; then + pass "windows-install --check-script" +else + bad "windows-install --check-script" +fi +grep -q 'designmd-pptx==2.1.2' packaging/windows/Install-DesignmdPptx.ps1 \ + && pass "installer package pin 2.1.2" || bad "installer pin missing" +grep -q 'Assert-SafeInstallRoot' packaging/windows/Install-DesignmdPptx.ps1 \ + && pass "installer safe root" || bad "safe root missing" + +# ── vscode argv injection ──────────────────────────────────────────────── +if command -v node >/dev/null 2>&1; then + node -e ' +const { resolveCli, hasShellMeta } = require("./editor/vscode/cli.js"); +const r = resolveCli({ workspaceRoot: process.cwd(), pythonPath: "python3", + args: ["refine", "deck.json", "--feedback", "$(id); rm -rf /"] }); +const fb = r.argv[r.argv.indexOf("--feedback")+1]; +if (fb !== "$(id); rm -rf /") { console.error("argv corrupted", fb); process.exit(1); } +if (!hasShellMeta(fb)) process.exit(2); +if (!Array.isArray(r.argv)) process.exit(3); +console.log("ok"); +' && pass "vscode argv injection resistance" || bad "vscode argv" +else + echo " skip node not installed" +fi + +echo +if [[ $fail -eq 0 ]]; then + echo "SMOKE PASS (automated track A)" + exit 0 +else + echo "SMOKE FAIL — see messages above" + exit 1 +fi