Development and Tooling
The development environment for ppt-craft relies on a standardized toolchain managed through pyproject.toml and hatchling, with strict quality gates enforced via vendored scripts and Git hooks. Configuration is centralized in the project root, while development utilities handle everything from local TLS certificate provisioning to prose hygiene and file-size ceilings. This section details the setup required to run the server, lint the codebase, and validate changes before they are committed.
Configuration and Build System
Section titled “Configuration and Build System”The project uses hatchling as the build backend, requiring Python 3.14 or higher 1. Dependencies are pinned to specific versions to ensure reproducibility, including fastapi, uvicorn, and python-pptx. Development dependencies such as ruff, mypy, and pytest are installed via the dev optional dependency group.
Ruff is configured to target Python 3.14 with a line length of 120 characters. It enforces a specific set of linting rules (E, F, W, I, UP, B) while ignoring specific codes like E501 (line too long) and F841 (unused variable). The isort tool is configured to recognize ppt_craft as a known first-party package.
Pytest is configured to look for tests in the tests directory and includes src in the Python path. The project exposes a CLI entry point ppt-craft mapped to ppt_craft.cli:cli.
Linting and Quality Gates
Section titled “Linting and Quality Gates”Quality is enforced through two primary vendored scripts and a Git pre-push hook.
File Size Ceiling
Section titled “File Size Ceiling”The script tools/check_file_ceiling.py enforces a maximum of 1,000 lines for hand-authored code files 2. It scans tracked files for languages including Python, Go, JavaScript, TypeScript, Astro, Shell, CSS, and HTML. Binary files, generated lockfiles, and common cache directories (e.g., __pycache__, .venv) are excluded from this check. The script exits with a non-zero status if any file exceeds the limit, printing the offenders sorted by line count.
Prose Hygiene
Section titled “Prose Hygiene”The script operates in two modes:
- Gate (Default): Reports offenders and exits non-zero if any are found 3.
- Fix (
--fix): Automatically replaces dashes with hyphens and removes decorative emoji. AI-speak phrases are reported for manual rewording but are not auto-fixed.
Files can opt out of these checks by including the marker prose-hygiene: allow.
Pre-Push Gate
Section titled “Pre-Push Gate”The .githooks/pre-push script runs automatically if core.hooksPath is set to .githooks 4. It executes two checks:
- Runs
tools/check_file_ceiling.pyto enforce the 1,000-line limit. - Runs the pytest suite (
python3 -m pytest -q) to ensure parity with local tests.
Bypassing these checks requires git push --no-verify.
Certificate Management
Section titled “Certificate Management”Local development requires HTTPS for the Office.js taskpane. The module src/ppt_craft/tls.py manages this using mkcert 5.
Workflow
Section titled “Workflow”- Cert Issuance: The system issues a SAN certificate covering
localhost,127.0.0.1,gb10.local, and the machine’s hostname (and.localalias). - Storage: Certificates are stored in
~/.local/share/ppt-craft/tls/with permissions0o700for the directory and0o600for the key.
Tooling
Section titled “Tooling”The mkcert binary is located via shutil.which or by checking common paths like mise shims, Homebrew, or /usr/local/bin. If mkcert is not found, a SddcError is raised with installation instructions.
Utility Scripts
Section titled “Utility Scripts”Smoke Test
Section titled “Smoke Test”scripts/smoke_live.py provides an end-to-end smoke test for the agent loop 6. It boots the ppt-craft server in a background thread, connects via WebSocket, and sends a draft request.
The script:
- Accepts arguments for
--host,--port,--intent,--slides, and--theme. - Waits for the server to start (up to 5 seconds).
- Listens for WebSocket events (
log,model_resolved,storyline_ready,slide_ready,preview_ready,deck_ready,error). - Times out after a configurable
--deadline(default 300 seconds).
Git Ignore
Section titled “Git Ignore”.gitignore excludes Python caches (__pycache__, .venv), build artifacts (dist, build), and generated theme assets (*.pptx, *.theme.json) 7. It also ignores HTTPS certificates in certs/ and generated wiki build outputs (wiki-site/node_modules, wiki-site/dist).
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "ppt-craft"
version = "0.1.0"
description = "Local Claude-style PowerPoint add-in (Office.js taskpane + GB10 Qwen3.6 backend)"
requires-python = ">=3.14"
authors = [{ name = "sddc.info" }]
license = { text = "MIT" }
readme = "README.md"
# Pinned to current latest as of 2026-05-08.
dependencies = [
"click==8.3.3",
"fastapi==0.136.1",
"uvicorn[standard]==0.46.0",
"websockets==16.0",
"httpx==0.28.1",
"jinja2==3.1.6",
"python-pptx==1.0.2",
"lxml==6.1.0",
"pillow==12.2.0",
# FastAPI's UploadFile / Form handling delegates here - the slides
# upload route would 500 with "must be installed" without it.
"python-multipart>=0.0.20",
# multipart parsing depends on email-validator etc; pulled by fastapi[standard]
]
[project.optional-dependencies]
dev = [
"pytest==9.0.3",
"ruff==0.15.10",
"mypy==1.13.0",
]
[project.scripts]
ppt-craft = "ppt_craft.cli:cli"
#!/usr/bin/env python3
"""House line-count ceiling check (vendored, stdlib-only, no AI).
VENDORED FILE - do not edit by hand. Regenerate with
``sddc repo audit vendor-ceiling`` from the sddcinfo monorepo. It is a
self-contained port of that monorepo's ``repo_audit`` ceiling logic, using the
identical file classification and ``n_lines = text.count("\n") + 1`` count, so
this gate agrees exactly with ``sddc repo audit ceiling``.
Hand-authored code only (generated lockfiles, binaries, data, config and docs
are excluded). No allowlist - any code file over the ceiling fails, with no
grandfathering. Run it from a repo root (or pass a path); exits non-zero and
prints offenders worst-first when any code file exceeds the ceiling.
"""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
MAX_FILE_LINES = 1000
# Hand-authored code languages this gate applies to.
CODE_LANGS = {"python", "go", "javascript", "typescript", "astro", "shell", "css", "html"}
_LANG_BY_SUFFIX = {
".py": "python",
".pyi": "python",
".go": "go",
".js": "javascript",
".mjs": "javascript",
".ts": "typescript",
".tsx": "typescript",
".jsx": "javascript",
".astro": "astro",
".sh": "shell",
".bash": "shell",
".css": "css",
".html": "html",
#!/usr/bin/env python3
"""Prose-hygiene gate (vendored, stdlib-only, no AI).
VENDORED FILE: do not edit by hand; regenerate from the upstream generator.
Self-contained: scans tracked text files for the AI-generated-prose tells this
project bans -- em/en-dashes, decorative emoji, and stock AI-speak phrases --
and fails if any remain. Run as a pre-commit / pre-push / CI gate so the cruft
can never accrete again.
Two modes:
(default) report offenders and exit non-zero if any are found (the GATE)
--fix deterministically repair the auto-fixable ones in place
(dashes -> hyphen, decorative emoji removed); AI-speak phrases are
reported for manual rewording, never auto-reworded.
A file may opt out of fixing/checking with a line containing the marker
``prose-hygiene: allow`` (for the rare file that legitimately carries the
characters -- e.g. the detector that defines these very patterns).
"""
from __future__ import annotations
import re
import subprocess
import sys
from pathlib import Path
_ALLOW_MARKER = "prose-hygiene: allow"
# Dash family that reads as AI-generated punctuation: em-dash, en-dash,
# horizontal bar, figure dash. All collapse to a plain hyphen.
_DASHES = "—–―‒"
_DASH_RE = re.compile(f"[{_DASHES}]")
# Decorative emoji ranges. Only flagged/stripped where DECORATIVE -- in a
# comment or a docs file -- never in UI markup (HTML/astro/jsx elements) or
# code strings, where emoji are functional (menu glyphs, status marks, icons)
# and removing them would break the product.
_EMOJI_RE = re.compile(
"[\U0001f300-\U0001faff\U00002600-\U000027bf\U0001f1e6-\U0001f1ff\U00002b00-\U00002bff]"
#!/bin/sh
# Pre-push gate. Enabled per-clone via `git config core.hooksPath .githooks`.
#
# Must-have: enforce the house 1000-line file ceiling (vendored, stdlib-only)
# so an oversized code file is caught BEFORE the push reaches a reviewer. The
# repo's pytest suite also covers this via tests/test_file_ceiling.py, so we run
# the suite too (it is fast) to keep the push gate in parity with local pytest.
# Bypass only in a genuine emergency with `git push --no-verify`.
set -e
root="$(git rev-parse --show-toplevel)"
python3 "${root}/tools/check_file_ceiling.py" "${root}"
exec python3 -m pytest -q "${root}/tests"
"""mkcert-backed local-CA HTTPS for the taskpane host.
Office add-ins require HTTPS, even for sideloaded testing. We use mkcert
because it provisions a per-machine local CA that is trusted by browsers
and by Office desktop hosts (Windows/Mac), so the taskpane loads without
"Not Secure" warnings.
This module is just file-system glue; mkcert itself must already be on
PATH. `sddc ppt cert init` runs `mkcert -install` once and then issues a
SAN cert covering localhost / 127.0.0.1 / gb10.local (extra hosts via
`--san`). Output keypair lands under ~/.local/share/sddc/ppt/tls/.
"""
from __future__ import annotations
import os
import shutil
import socket
import subprocess
from pathlib import Path
from ppt_craft.errors import SddcError
TLS_DIR = Path.home() / ".local" / "share" / "ppt-craft" / "tls"
def default_hosts() -> tuple[str, ...]:
"""Default SAN list for the local-CA cert.
Covers loopback plus the friendly `gb10.local` mDNS name, and adds
this machine's own hostname (and its `.local` mDNS alias) so a remote
browser hitting the box on its LAN name gets a clean cert. Add extra
hosts/IPs via `--san`.
"""
hosts = ["localhost", "127.0.0.1", "gb10.local"]
try:
host = socket.gethostname()
except OSError:
host = ""
if host and host not in hosts:
"""End-to-end live smoke against the running ppt-craft server + Qwen.
Boots the server in-process, opens a WS, sends a small `draft` request,
collects all events for a fixed deadline, prints a summary. Used to
validate the agent loop end-to-end without a browser.
Usage:
python3 scripts/smoke_live.py --intent 'Test deck' --slides 3 --port 13030
"""
from __future__ import annotations
import argparse
import asyncio
import json
import secrets
import sys
import threading
import time
from pathlib import Path
# Make the local src/ importable when run from the repo root.
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
def boot_server(host: str, port: int, token: str) -> threading.Thread:
import uvicorn
from ppt_craft.server import AppState, build_app
state = AppState(auth_token=token, host=host, port=port, qwen_url="http://localhost:8010")
app = build_app(state)
def _run() -> None:
uvicorn.run(app, host=host, port=port, log_level="warning")
t = threading.Thread(target=_run, daemon=True)
t.start()
# Wait for the server to come up
import socket
# Python
__pycache__/
*.py[cod]
*.egg-info/
.pytest_cache/
.mypy_cache/
.ruff_cache/
build/
dist/
.venv/
venv/
# Generated theme assets (regenerate via `ppt-craft themes build`)
src/ppt_craft/themes/*.pptx
src/ppt_craft/themes/*.theme.json
src/ppt_craft/static/taskpane/icon-*.png
# HTTPS certs (rotate via `ppt-craft cert init`)
certs/
# OS
.DS_Store
# Claude Code shared bash-guard log dir
.ai-hooks/
# Generated wiki: commit wiki-site/ (content + scaffold); ignore build output.
/wiki-site/node_modules/
/wiki-site/dist/
/wiki-site/.astro/
# Local fleet-wiki auto-regen hook (installed by sddc repo wiki deploy).
/.githooks/post-commit