Releases: ferdinandobons/brand-docs
Release list
v0.10.1: reliability and security hardening
BrandDocs v0.10.1 is a reliability and security hardening release. It adds no new product features.
Security
- Validate OOXML packages at every live extract, generate, deterministic-QA, and visual-render boundary before third-party parsers process them.
- Enforce archive, inflated part/package, entry-count, compression-ratio, duplicate-member, unsafe-path, and content-types constraints centrally.
Fixed
- Preserve XLSX x14 sparkline groups through openpyxl saves and fail closed when worksheet extensions are lost.
- Publish generated files atomically only after schema, resolver, provenance, and QA checks pass; failed runs preserve any existing good output.
- Bound whole-row and whole-column Excel names to the used rectangle, reject discontiguous names explicitly, and keep sparse-sheet operations sparse.
- Make fixture-builder failures fail CI instead of silently becoming skips.
Changed
- Pin CI/development dependencies in
requirements-ci.txt. - Treat warnings as errors except for the explicitly handled openpyxl sparkline warning.
- Test and support Python 3.10 through 3.13 explicitly.
Verification
- 946 tests passed, 6 skipped, and 39 subtests passed locally on Python 3.11 and 3.13.
- Ruff lint and formatting checks passed.
- Real LibreOffice/Poppler rendering passed locally and in GitHub Actions.
- GitHub Actions passed across Python 3.10, 3.11, 3.12, and 3.13.
v0.10.0: hero polish
Hero-image polish release.
Changed
- The README hero's three blocks are now equidistant. The full hero refresh shipped inside v0.9.0 without a changelog entry: a redesigned one-to-many visual (one company template, extracted into a verified hexagonal Brand Profile core, fanning out into unlimited on-brand documents, with numbered extract / verify / generate steps), exported as the transparent
assets/hero.webpthe README embeds plus a white-flattenedassets/hero.jpg, withassets/hero.svgas the editable source. This release balances that composition: the template page moved 30 px right, the core 45 px left, and the flow curves, flying brand particles and step labels were re-fit so the three blocks sit at even ~250 px gaps (pixel-measured on the render).
Full changelog: CHANGELOG.md
v0.9.0 - the brand-intelligence release
[0.9.0] - 2026-06-10
The brand-intelligence release: derived indexes that never lie (rich TOC
cache, empty rebuilds), multi-template profiles (extract --blend), the
cross-template drift report (compare-profiles), a guided authoring surface,
a local-corpus fidelity benchmark, and a 31-finding general-review hardening
wave on top.
Fixed
-
General-review wave (multi-agent, 31 confirmed findings). Engine: the
heading-less generation now collapses a bare multi-paragraph outline TOC's
FULL span to an empty field (the plain writer used to leave the template's
demo entries and orphan the end fldChar - a regression of the rich-TOC
delta); pptx and xlsx extraction now parse the themea:fontSchemelatin
typefaces instead of hardcodinglatin=null(the docx extractor already
did);schema.validatereturns problems instead of raising on a
hand-edited blend ledger with mixed-type entries; profile JSON/sidecar
writes are atomic (same-directory temp +os.replace), making the blend's
byte-identity-on-failure claim true under ENOSPC/kill; OOXMLunpack/
read_partreject decompression bombs (per-part and per-package declared
inflated-size floors); the corpus runner exits 1 on verify exceptions, not
just clean failures. Examples: the three builders now consume ONE complete
12-slot brand clrScheme from_brandlib(docx/pptx/xlsx had drifted apart
on five supporting slots - caught live bycompare-profiles- and the
shipped pair now reports zero theme drift, pinned by a strengthened CLI
test that replaced a tautological assertion). Docs: README test count and
blending/compare teaching, CONVENTIONS gains the engine verb surface and a
§16 blend/compare vocabulary, the Pages site's broken README anchor and
stale install command fixed, a real brand word in ROADMAP replaced with a
placeholder. -
The refreshed outline TOC cache is now a real Word-shaped TOC (REFLECTIONS
P3). The visible cache of a preserved or authored outline table of contents
was rewritten as flat plain-text entries: one style for every level, no
links, no page-number fields. The rewrite now authors deterministic
bookmarks on the generated headings (_TocBD000001sequential,
collision-scanned ids), harvests a per-level paragraph-style map from the
template's OWN cached entries (trailing-digit TOC style convention,
indentation-rank fallback, nearest-lower-level chain), and emits each entry
as hyperlink-to-bookmark + tab + nested dirtyPAGEREF, so previews show
the template's real per-level TOC styling and Word fills page numbers on
open. The bare-paragraph TOC path converges to the same multi-paragraph
shape. Fail-closed: a malformed cache falls back to the previous simple
rewrite atomically; documents without an outline TOC author zero bookmarks
and stay byte-identical (now pinned by a dedicated TOC-free frozen anchor;
the historical anchor was deliberately recomputed because the committed
fixture itself contains an outline TOC, so its cached-TOC bytes necessarily
changed shape). -
Template demo entries no longer survive in derived indexes the content did
not feed. A kept caption index (table-of-tables / table-of-figures) whose
sequence received no captions, and an outline TOC in a heading-less
generation, used to keep the template's cached demo entries (the "stale
derived index" defect class, visible in every preview renderer). Both caches
are now rebuilt EMPTY: the field code survives, dirty, so Word recomputes it
on open, but fabricated entries never reach the generated document. The
destructive-action floor is untouched: structure is still never removed
without corroboration; only the visible cache is honest now.
Added
- Local-corpus fidelity benchmark (REFLECTIONS P3). New
scripts/corpus_benchmark.pywalks a corpus of REAL templates that lives
outside the repository (and refuses one inside it), runs
extract -> verify -> brand-agnostic probe generate -> QA per template in a
throwaway directory, and writes a datedreport.{md,json}next to the
corpus with the LibreOffice-vs-Word caveat stated in the header. The probe
input is typed blocks only (no styles, colors, fonts), so the corpus
measures the engine without ever tuning it;documentation/DEVELOPMENT.md
documents the layout. CI smokes the runner on the synthetic example
template. - Authoring intelligence for the IntermediateDocument/GridDocument
(REFLECTIONS P3). The least-guarded stage (what the authoring agent puts
IN the input) now has a guided surface: PROFILE.md gains three
format-uniform sections rendered by the new sharedcommon/profilemd.py
(the role table on all three formats - pptx/xlsx previously shipped a
two-line stub - a "Brand palette roles" table naming the semantic color
tokens an author may reference, and "Authoring hints" that advertise
comprehended fragments when present); each skill's SKILL.md gains an
"Authoring the IntermediateDocument/GridDocument" section with role-first
composition rules (skeleton order, heading hierarchy the TOC regenerates
from, captions so derived indexes stay real, native objects on pptx, region
bounds and formula preservation on xlsx, color discipline, fragments before
re-derivation). Advisory by construction: nothing here adds engine
authority, the resolver remains the only author of values. - Multi-template profile blending (REFLECTIONS P3, the quality-ceiling
lift).extract --name X --template second.docx --blendfolds a SECOND
same-format template into an existing profile at the VALUE-fact level: it
fills captured facts the primary left unset, corroborates agreeing facts
with a bounded deterministic confidence boost, and keeps the primary's value
on every conflict. Artifact POINTERS (style ids, layouts, anchors,
numbering) never cross shells: generation still opens the PRIMARY shell and
the resolver still membership-validates everything against it, so the brand
guarantee is untouched. Fail-closed all-or-nothing transaction (a rejected
blend leaves profile.json byte-identical), sha-deduped and idempotent
(re-blending the same file is a no-op), secondary shells stored
content-addressed next to the primary with provenance, guarded by the new
blend_shell_provenanceQA check (donor tamper/drift = ERROR). Single-
template profiles serialize without one new key and both frozen generation
anchors are unmoved. Same-format only by design; cross-format stays the job
ofcompare-profiles. compare-profiles: the cross-template drift report (REFLECTIONS P3).
New read-only CLI verb that compares the BRAND-level facts of two saved
profiles: theme colors per slot, theme/captured fonts, semantic palette
roles, and off-theme usage (a raw hex in one profile that is a theme slot in
the other - the sharpest "one template is off-theme" signal). Structural
facts (styles, layouts, anchors) are never compared as drift: they are
per-shell by design; role coverage is reported as information only. Writes
nothing; exits 1 on brand-level drift so it can gate brand coherence in CI.
First live run immediately caught a real inconsistency between the shipped
docx and pptx example templates (five theme slots disagree) - queued for the
next example-template refresh.- Contributor onramp (REFLECTIONS P1).
CONTRIBUTING.mdexpanded from a
10-line stub to the full gate (dev setup, the three test lanes, the seven
non-negotiable rules, PR checklist, sized first contributions);
scripts/brandkit/README.mdcreated as the engine internals map (module
layout, the ten verbs' call paths, the two invariants, the recipe for a new
capture axis); the release checklist written down in
documentation/DEVELOPMENT.md; three sized "good first issue"s opened. - PLUGIN_WORKFLOW drift guard.
documentation/PLUGIN_WORKFLOW.mdnow
documents the full ten-verb CLI surface and the learning loop
(generation_report.json->learn/propose-overrides->--accept) in the
end-to-end diagram;tests/test_doc_drift.pyfails the build if a CLI verb
or acommands/slash command is missing from the document.
v0.8.1: hardening patch
Highlights
A post-0.8.0 hardening patch. Generated bytes are unchanged for well-formed templates and profiles (frozen byte-identity anchor verified).
- Plugin onboarding fixed: every skill/command doc now teaches
python scripts/cli.py ...- a launcher that resolves the engine root from the skill folder AND the repo/plugin root (BRAND_DOCS_ROOToverrides). Fresh plugin sessions no longer trip on a wrong path. - Example pptx imports in Keynote: python-pptx omits
p:notesMasterIdLstwhen speaker notes are authored; strict importers rejected the package. The builder now repairs it deterministically at build time. - Engine hardening (with regression tests): caption-index refresh validates both field bounds (a corrupted inventory could corrupt the body via negative indexing);
role_idvalidates its own composition; border-apply refuses mislabeled sides and narrows its catches; set-only-when-unset guards treat empty attributes as unset. A frozenCHECK_REGISTRYpins the QA check-id vocabulary behind an AST-guarded test. - Example docx visual fixes from a page-by-page render audit: clean solid cover band, landscape appendix no longer inherits the two-column layout, readable white table headers, repeating header rows on page-spanning tables, balanced two-column synopsis.
- Honest speed expectation in the README: first run on a new template up to ~15 minutes end to end with an agent; every later document takes seconds - still a fraction of hand-formatting, with a faithful file instead of an approximate one.
Full detail: CHANGELOG.md
v0.8.0: the learning release
Highlights
The profile now learns. Three channels close the loop, every one fail-closed and advisory until an explicit --accept:
- From its own QA findings: every
generatepersists a cross-run report; thelearnverb distills recurring findings into shell-frozen overrides, and the newpropose-overridesverb lets the model correct the ambiguous remainder through the same single validated sink, with anoverride_appliedaudit finding for every live correction. - From the model's judgement: the L2 visual-audit verdict persists (and soundly short-circuits byte-identical re-renders), ambiguous WARNINGs get triaged, faked headings (a body-style line that visibly acts as a heading) are detected as
pseudo_headingsand can be promoted onto a real heading role, and off-theme brand accents become addressable run colors via palette aliases (the model names, the engine byte-copies the captured ref). - From the user: the
refineverb turns end-of-generation feedback (text or a screenshot) into a validated comprehension delta for future generations.
Fidelity below typography (docx): three new dominant-sampled, shell-verified appearance axes - paragraph geometry (spacing/indents/borders/shading), table conditional formats (tblLook, table-style reference, cell margins; band fills stay in the shell's style part) and list numbering (per-level numFmt/lvlText/indent, referencing/cloning the shell's own definitions, never synthesized). Run typography (font/size/color) now captures and applies on all three formats.
Covers on anchor-less templates: when extraction records anchors.cover.kind == NONE, the generator synthesizes a minimal cover from the profile's own resolvable cover.* roles through the resolver chokepoint - validated on a real-world template - with a cover_synthesized audit finding. A uniform appearance_apply_degraded parity ledger measures any captured axis a format cannot realize yet.
Measurably faster, byte-identical: single-launch render probe, per-pass artifact-load memo, lazy imports, shared scans. generate --qa auto ~8.5s -> ~5s per format; the 850-test suite 59s -> 24s; document bytes, profiles and QA reports byte-diffed identical.
Validated end-to-end on the enriched synthetic example templates and on real corporate templates (in-place SDT covers, synthesized covers, faked-heading promotion, alias minting, caption-index regeneration). Schema stays 1.2.0: profiles from 0.7.0 keep working unchanged.
Full detail: CHANGELOG.md
v0.7.0 - brand appearance, model-driven color, cover & caption-index fidelity
[0.7.0] - 2026-06-09
Brand-fidelity release: the engine now follows a template's real per-run typography
(font, size, color), applies model-named brand colors, fills content-control covers
faithfully, and regenerates caption indexes from the new content. Schema stays 1.2.0
(additive); profiles from earlier versions keep working unchanged.
Added
- Brand typography capture (font family + size + color). Extraction now captures
the template's dominant DIRECT run typography - the real visible font, font size
and run color a designed template applies per-run (e.g. Roboto 16pt) rather than
via the named styles or theme - into the reservedrole.appearancefield and the
additivetheme.fonts.body(font/size) andtheme.text.body(color). Generation
applies all three as direct run formatting through the resolver (paragraphs, list
items, captions, quotes, callouts, table cells, and hyperlink runs), so a generated
document renders in the template's real font, size and color instead of falling
back to thedocDefaults(typically Arial 11pt black)._extract_themealso reads
the realtheme1.xmlmajor/minor fonts anddocDefaults.- Universal and deterministic. Each axis is captured by dominant sampling over
ALL runs (a run with no explicit value votes "inherit"), so a value is applied
only when it dominates the whole document - an accent color present on a few runs
never becomes the body color. Nothing is hardcoded to any template; a document
with no dominant value (and every pre-capture profile) behaves exactly as before. - Brand guarantee, fail-closed. Every applied font/size/color is re-validated
against what the shell proves it contains (appearance_targets_exist): fonts vs
the fontTable + theme, sizes vs the template's ownw:szvalues, colors vs the
theme palette + the template's ownw:colorvalues. The body size/color default
never flows onto heading roles (their style's intrinsic size/color is preserved). - Deferred (see ROADMAP section 1): per-word accents,
cover-layout reconstruction, and heading typography when the template fakes
headings in the body style.
- Universal and deterministic. Each axis is captured by dominant sampling over
- Model-driven brand color (run color palette tokens). The profile now carries a
theme.paletteof the template's brand colors (theme slots likeaccent1, plus
off-themehex:RRGGBB) with usage provenance; an IntermediateDocument run can carry
a semanticcolortoken (a palette key, never a literal hex) that the resolver maps
to the brand color and the generator applies - rendering a theme color as
w:color@w:val=<hex>+w:themeColor=<slot>so it shows in both LibreOffice and
Word. The model NAMES each color's purpose viacomprehension.palette_annotations;
it never authors a color (fail-closed: a token into an empty/absent palette errors). - Caption-index regeneration (list of tables / figures). A kept caption index
(TOC \c "<seq>") is now repopulated from the new content: each captioned table or
figure emits a real WordSEQfield, and the index's visible cache is rebuilt from
the emitted captions (so a headless render shows the new entries, not the template's
stale ones). The model maps a caption kind to its index via the new closed
comprehension.conventions.indexes[*].caption_target(table|figure); brand- and
language-agnostic (the seq label is the template's own, read from the profile). - Cover fidelity for content-control covers. A cover content control bound to a
core property (e.g. the subject repeated in the page header) now has its cached run
text refreshed across the body and every header/footer, so a headless render shows
the filled value, not the template prompt. The cover role-style re-assertion is
gated to bare placeholder controls, so a real author-formatted slot keeps its own
formatting (a builtinSubtitlewhose color is a near-white tint no longer blanks a
filled cover subtitle).
Fixed
- Colored hyperlink runs now write
w:colorat the schema-correctCT_RPrposition
(beforew:u/w:sz/w:vertAlign) instead of appending it last, so an
underlined colored link is conformant OOXML; a theme-token link with no resolved hex
emitsthemeColoronly (no strayw:val="000000"that would paint it black).
v0.6.2
A robustness fix for real-world templates. No schema, profile or output change for well-formed templates; profiles from earlier versions keep working unchanged.
Fixed
- DOCX
extractandgenerateno longer crash on templates whose section measures are non-integer twips (e.g.1440.0000000000002inw:pgMar/w:pgSz, emitted by some editors). python-docx parses measures withint()and raises the moment any code touches the value, including its own internals (Document.add_tablederives the table width fromsection.left_margin). The shell's section measures are now sanitized in place (sub-twip rounding, page geometry visually identical) and every section-length read tolerates the malformed value via a raw-twips fallback.
Tests
MalformedSectionMeasureTest: the section-length helpers tolerate the bad value,sanitize_section_measuresrepairs it in place, and a generate with a table block (the exact crash path) survives a malformed margin.
v0.6.1
A clean finished checkpoint: the remaining LOW items from the v0.6.0 review are
closed and dead parameters removed. No behavior change beyond the fixes; profiles
from earlier versions keep working unchanged.
Fixed
- The QA gate now verifies a
number_formatrole's mask against the SHELL's
actually-used formats (the shell-backed peer of the schema's intra-profile
check), so a fabricated/hand-edited mask is caught at verify time, never applied. - A
table/KPI/SmartArtblock rendered against a profile with no matching role
no longer falls back to the default style SILENTLY: it surfaces an INFO
style_fallbackso the missing brand style is visible in QA (the content still
renders).
Changed
- Removed dead parameters (no behavior change):
run_qa's never-readmode, the
docx_write_blockprofilearg, and the xlsx_resolve_named_targetfindings
arg.
Tests
- Closed the review's remaining coverage gaps: strict-mode promotion of
visual.ocr_degradedto an error,_ocr_pngtimeout/OSError degradation, the
Quick-Look-absent render degrade, the QA-gate fabricated-mask rejection, and the
style_fallbackpath.
v0.6.0
Visual QA works by default, a whole-project code + quality review (multi-agent,
adversarially verified) hardens security/correctness/determinism, and the docs are
re-synced to the shipped engine. Brand Profiles from 0.1.x-0.5.0 keep working
unchanged.
Added
- Visual QA runs by default and installs in one step. The renderer probe is now
FUNCTIONAL-first: a LibreOffice that actually renders is usable even if its macOS
code signature was knocked loose by an update/quarantine removal (no re-sign
needed), so the visual gate is no longer falsely disabled.scripts/setup_visual_qa.sh
auto-detects the platform package manager and installs LibreOffice + Poppler
(+ optional Tesseract); the README/INSTALLATION reframe visual QA as a standard,
on-by-default part of the QA step. - An aggregate OCR time budget so a many-page document cannot turn a deep/strict
gate into anN x timeouthang.
Fixed
- Security - Excel formula injection: author content starting with
=became a
LIVE formula (=WEBSERVICE/=HYPERLINK/DDE) in the generated workbook, breaking
the "formulas live only in the shell" invariant. It is now neutralized to a TEXT
cell (verbatim, never executed) and surfaced; the QA gate also fails closed on any
output formula the shell did not have. - Security - hyperlink scheme allowlist: docx/pptx now refuse
file:/
javascript:/data:/smb:link targets (the text is kept), so untrusted content
cannot wire a hostile link into an on-brand file. - Robustness: component/section expansion has a node-count budget (runaway
fan-out fails loud, never hangs/OOMs); a pathologically deep list IID raises the
contractedIIDParseErrorinstead ofRecursionError; the OCR step tolerates
non-UTF-8 tesseract output instead of crashing the whole run. - Determinism: xlsx generation is byte-idempotent even when the shell's
core.xmllacksdcterms:created(openpyxl would otherwise fabricate a
wall-clock one); a number-format mask is no longer applied to a preserved-formula
/ merged cell;profile.jsonis written with sorted keys. - Number-format classification: elapsed-time masks (
[h]:mm) classify as time;
accounting masks with bracketed currency + padding idioms classify as accounting. - Visual QA: the Quick Look fallback accepts only the expected thumbnail name
(never stages a stale/unrelated PNG as a bogus render).
Changed
- Removed verified-dead helpers and corrected stale docstrings/docs: the docx TOC
and the pptx charts/SmartArt/KPI/images are native now (not "deferred"), and
number_format/named_rangeare first-class resolver types.
v0.5.0
Completes the block-type matrix: Word now renders every block type natively
(the toc block was the last to degrade), and Excel resolves a cell's semantic
number format to the template's own mask. Brand Profiles from 0.1.x-0.4.0 keep
working unchanged; an absent comprehension is still the deterministic path.
Added
- Word
tocblocks render natively. Thetocblock was the last block type
that degraded instead of rendering; it now authors a real, updateable outline
table-of-contents field. If the shell already carries an outline TOC the block
defers to it (refreshed in place, never a duplicate); otherwise it authors a
native outline TOC field at the block's position, with its visible cache filled
from the generated headings and the field marked dirty +updateFieldsso Word
rebuilds it on open. It defers only to an OUTLINE TOC, so a shell shipping only a
table-of-figures still gets its requested table of contents. Byte-idempotent.
Every Word block type now renders natively. - Excel resolves a cell's semantic number format. The
number_formatresolver
type is now wired end-to-end (previously reserved/staged).extractclassifies
the template's own number-format masks into brand-agnostic families
(currency/percent/date/datetime/time/accounting/decimal/integer/
text/scientific) and emits anumber.<family>role bound to the template's
VERBATIM mask. AGridDocumentnames the intent viaformats: {name: family}
(keyed by the same named-range/cell vocabulary ascells/regions); generation
resolvesnumber.<family>and applies the template's own mask to the filled
cell(s). A format is never fabricated: an intent the template does not carry
degrades loudly (number_format_degraded) and leaves the existing format, and a
resolver whose mask is not in the template's surfaced formats is rejected at
validation.