
Brand Xlsx
- 19 installs
- 238 repo stars
- Updated July 10, 2026
- ferdinandobons/brand-docs
Extract a company's brand from an .xlsx template into a reusable profile, then generate new on-brand Excel workbooks from a fill manifest.
About
Extracts a brand profile from an .xlsx template and generates new on-brand Excel workbooks by mapping data to named cells and regions, via a deterministic Python engine. A developer uses it to fill branded workbook templates with variable data.
- Seven-verb contract: extract, comprehend, verify, generate, learn, propose-overrides, refine
- Resolves each named cell/region through profile.json and preserves the template shell
Brand Xlsx by the numbers
- 19 all-time installs (skills.sh)
- Ranked #439 of 688 Office & Documents skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ferdinandobons/brand-docs --skill brand-xlsxAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 19 |
|---|---|
| repo stars | ★ 238 |
| Last updated | July 10, 2026 |
| Repository | ferdinandobons/brand-docs ↗ |
What it does
Extract a company's brand from an .xlsx template into a reusable profile, then generate new on-brand Excel workbooks from a fill manifest.
Files
brand-xlsx
Use this skill when the user wants reusable branded Excel/workbook generation from a company .xlsx template and variable user-provided data.
This is an AI-agent skill for Codex and Claude Code. The user should describe the workbook/model they want filled; the agent maps that request to named cells and named regions, invokes the internal Python engine, verifies the output, and returns the generated .xlsx.
The seven verbs: three deterministic + four model-assisted
Every brand skill (brand-docx, brand-pptx, brand-xlsx) implements the same contract. The deterministic core is extract / verify / generate; on top of it sit the optional learning verbs comprehend / learn / propose-overrides / refine, each fail-closed (the engine validates every proposal and authors every value).
| Verb | Input | Output |
|---|---|---|
| extract | a company .xlsx template | a reusable Brand Profile |
| comprehend (optional, model-driven) | a saved profile + a model-authored comprehension.json | the profile with a validated, cached comprehension block |
| verify | a saved Brand Profile | QA findings + a verdict |
| generate | data (a GridDocument) + a profile | a new on-brand .xlsx |
| learn (deterministic distillation) | the profile's cross-run generation history | recurring QA findings distilled into shell-frozen overrides, advisory until --accept |
| propose-overrides (model-driven) | the recurring remainder learn could not bind + a model-authored proposal | shell-backed corrections through the same fail-closed sink, advisory until --accept |
| refine | end-of-generation user feedback (text or a screenshot) as a refinement.json delta | the existing comprehension overlaid for FUTURE generations, advisory until --accept |
comprehend is optional: generate works on the deterministic profile alone. See reference/comprehension.md for the full step.
Hard Rules
- Treat
python scripts/cli.py ...as an internal engine command, not the user-facing workflow. scripts/cli.pyis a LAUNCHER that locates the engine root by itself: it works from this skill folder AND from the repo/plugin root (setBRAND_DOCS_ROOTto override). Never guess deeper paths likescripts/brandkit/....- Run the dependency preflight before starting extract / comprehend / verify / generate, and report missing or unusable dependencies before proceeding.
- Extract opens the source template read-only and saves
brand-kit/<name>/template/shell.xlsxbyte-for-byte. - Generate opens the saved shell and resolves every named cell/region through
profile.json. - Do not put style names, colors, fonts, or brand identifiers in a GridDocument.
- If the user did not provide a template or enough data, ask for the missing input.
- Return the generated file path plus a QA summary.
- Consult
profile.json.artifact_catalogbefore generation when the user asks to mimic a specific piece of the template.
Preflight (always first)
Before doing any work, run:
python scripts/cli.py doctorUse its output to decide the run mode:
- If a required Python dependency is missing, install/repair it before extraction
or generation; the core engine is not ready.
- If only visual renderers are missing or unusable (
sofficepluspdftoppmor
optional PyMuPDF/fitz), the core L0 workflow can still run, but a full visual audit cannot be claimed. Tell the user what is missing, include the install/repair hint printed by doctor, and either proceed with degraded QA or install the renderer first.
- If optional OCR (
tesseract) is missing, the visual audit can still run, but
rendered residual-text proof is incomplete. Report that limitation when judging stale placeholders or field caches.
- For
--qa deepor--qa strict, prefer repairing/installing renderers before
generation. If the environment cannot run them, deep generates a degraded manifest and strict fails with a visual proof blocker.
Agent Workflow
1. Run the dependency preflight above and report any degraded capability. 2. Determine the brand name and locate the user-provided .xlsx template. 3. If no matching brand-kit/<name> exists, extract one. 4. Comprehend the template (optional, model-driven; see below). Skip when a current comprehension is already cached or no model is available. 5. Convert the user's tabular/model data into GridDocument JSON. 6. Generate the .xlsx with the internal engine. 7. Run QA and report any warnings honestly. 8. Feedback (only after returning the file): invite a refinement of the understanding for future workbooks (see below).
Before generation, inspect profile.json.artifact_catalog when the user asks to mimic a specific workbook piece. It records OOXML parts, named ranges, formulas, sheet dimensions, table names, merged cells, row/column sizing, cell styles, and number formats.
Authoring the GridDocument
The Grid is where "correct workbook" becomes "great workbook". Author it region-first, against the profile, never cell-address-first:
1. Read `brand-kit/<name>/PROFILE.md` before writing a fill. It lists the named-region roles, the brand cell styles, and the palette tokens. Address content to NAMED regions from that table; the engine fills the template's real ranges. 2. Respect region bounds. Size data to the region; when real data overflows, split it or ask the user to grow the template range instead of spilling into unnamed cells. 3. Formulas are content, not output. Never paste a computed value where the template keeps a formula: the engine preserves formulas, and QA fails the build if one is lost. 4. Numbers carry the brand too. Use the captured number.<family> roles (currency, percent, date, ...) for masks; never invent a format string. 5. Color discipline. Brand cell styles already carry the look: the default is NO direct color. For true emphasis, reference a palette role or a theme slot, never a hex. 6. Never name a style, font, or hex. If a fill needs something the role table cannot express, say so in the QA summary instead of inventing formatting: the resolver is the only author of values.
Feedback (end of generation)
Ask for feedback only after you have returned the generated .xlsx and its QA summary - never before or during generation. Invite the user to reply with text or a screenshot of the workbook, and name the roles, palette colors, and sections you actually used so the answer is concrete. A screenshot is your own multimodal read; the engine only ever ingests the structured JSON delta you distil from it.
Turn the answer into a small refinement delta of verbatim ids and merge it with the refine verb (see reference/comprehension.md):
python scripts/cli.py refine --name <brand> --input refinement.json --acceptA refinement improves FUTURE generations of this brand only - it mutates the saved profile, never the .xlsx you just produced. To apply it, generate again.
When the SAME QA finding recurs across runs, you can also propose a shell-bound correction with propose-overrides: the comprehend-input bundle surfaces the recurring generation_history, and you NAME a shell-backed re-point (a stub role to an existing healthy role, a number_format mask the shell uses, or a captured demo value) that the engine binds fail-closed (see reference/comprehension.md). It is advisory until --accept, improves FUTURE generations only, and every live correction surfaces as an INFO override_applied finding in QA.
Internal Extract
python scripts/cli.py extract --name <brand> --template <template.xlsx> --scope projectInternal Comprehend (optional, model-driven)
Read reference/comprehension.md for the full guidance, the six questions, and the anti-overfitting directive. In short:
python scripts/cli.py comprehend-input --name <brand> # prints {facts, excerpt} for the model
python scripts/cli.py comprehend --name <brand> --input comprehension.json # the ONLY writerSkip this verb when comprehension.status is present and its source_shell_sha256 equals the live provenance.shell.sha256. Never re-run it at generate time.
xlsx readiness. The Excel extractor surfaces named-region cover anchors and
sample-data regions, while fields is intentionally empty because workbooks donot have a TOC-style derived index. A current comprehension can therefore steer
cover fills/clears and demo-region cleanup, but it must not invent an index ref;
a ref into the empty field inventory is fail-closed and will be rejected.
Internal Verify
python scripts/cli.py verify --name <brand> --scope auto --qa auto--qa selects the QA depth (see reference/visual-audit.md):
fast: deterministic L0 only.auto: L0 + L1 visual pixel proxies when renderers (sofficepluspdftoppmor optional PyMuPDF/fitz) are present; otherwise L0 plus a single INFOvisual.unavailable.deep: L0 + L1 + a `visual_manifest.json` and per-page PNGs; iftesseractis installed the manifest also includes OCR text/hits. The orchestrator must then run the L2 step (see below).strict: deep visual audit plus gate errors when full render proof is unavailable or L1/OCR evidence is not clean.
Verify has no output to render, so all modes behave as L0 at verify time; the visual stages run at generate time.
Internal Generate
python scripts/cli.py generate --name <brand> --input <grid-document.json> --output <output.xlsx> --scope auto --qa autoSee reference/comprehension.md and reference/visual-audit.md.
Visual audit (two-stage)
The engine renders the output and runs deterministic pixel proxies, but the qualitative visual judgement is yours (the orchestrator), never the engine's - the Python engine never calls a model. To run the full two-stage audit:
1. Generate with --qa deep. The engine renders each printed page to a PNG, runs the L1 proxies, and writes visual_manifest.json next to the output in an <output-file>.visual/ dir, such as workbook.xlsx.visual/ (a side artifact; the .xlsx bytes never change). 2. Read the manifest path from stdout (visual manifest: <path>). 3. Open the PNGs listed in pages[*].png. For every entry in checklist, judge PASS/FAIL against the rendered pages, taking l1_findings and ocr.hits into account. 4. If any checklist item FAILS (or an L1 WARNING is confirmed visually as a real defect, or a visual.ocr_residual_text hit is confirmed as stale visible template text): repair the grid/content or the generated composition, regenerate, then re-run the audit. Loop until the checklist is clean, or until no further targeted repair can be justified without user input.
L1 findings are WARNING-only and never fail the gate by themselves; the real qualitative gate is your L2 judgement.
During repair, treat the template as a source of reusable workbook affordances, not a rule to preserve blindly. If inherited print areas, hidden rows/columns, frozen regions, named-region geometry, or other template structures create blank printed pages, overflow, clipped tables, or stale visible content, diagnose the structure as the cause and make the smallest targeted composition change. It is acceptable to adjust or collapse inherited scaffolding when preserving it damages the final workbook. After every repair, regenerate and rerun --qa deep or --qa strict.
Current Guarantees and Limits
M2 fills named cells and named regions while preserving formulas and workbook topology in the shell. Region fills that exceed the named range are refused before saving. When a current comprehension block is present, generation can clear corroborated cover/demo regions while preserving formulas; derived indexes remain out of scope for XLSX because the field inventory is intentionally empty.
The two-stage visual audit closes the "L0-only" gap: L1 deterministic pixel proxies catch rendered-layout defects L0 cannot see (blank/broken printed pages, content bleeding past the printable margins), and the L2 manifest drives the orchestrator's qualitative judgement and repair loop. See reference/visual-audit.md. When soffice and both PDF rasterizers (pdftoppm, optional PyMuPDF/fitz) are absent (e.g. CI), the audit degrades cleanly to L0 plus a single INFO visual.unavailable; exit codes are unchanged.
<!-- SPDX-License-Identifier: MIT --> <!-- SHARED BODY: skills/{brand-docx,brand-pptx,brand-xlsx}/reference/comprehension.md are BYTE-IDENTICAL. The comprehension guidance is format-neutral by design (a cover slot is a cover slot whether the file is a Word document, a PowerPoint deck, or an Excel workbook), so the same prompt drives all three formats and cannot drift. A CI guard (tests/test_reference_sync.py) asserts byte-identity. Edit all three together, or run the guard's --write helper. -->
Comprehension (optional, model-driven)
comprehend is the third of the four verbs (extract / comprehend / verify / generate). It is optional and model-driven: it is the one step where the model (you) reads a bounded bundle of deterministic facts about the template and writes down what each structure is for, so that generate can reconcile the template's preserved cover/index structures with the new content instead of leaving a cover unfilled, appending a duplicate title, or carrying stale index entries from removed demo content.
Everything load-bearing the model writes is a verbatim id copied from the facts bundle; the deterministic validator rejects anything else. The model proposes, the engine disposes. generate still works with no comprehension at all (the deterministic path), so skip this verb whenever a current comprehension already exists or no model is available.
When to run it
Run comprehend once per template shell, right after extract, before generate. Skip it when a current comprehension is already cached, that is, when the profile's comprehension.status is present and its source_shell_sha256 equals the live provenance.shell.sha256. A re-extract produces a new shell and resets comprehension.status to absent, so re-run comprehend only then. Never re-run it at generate time: the understanding is frozen into profile.json at merge and reused byte-for-byte.
The two CLI steps
comprehend is realized as two internal engine steps. Treat them as engine plumbing, not a user-facing workflow.
# 1. Print the bounded {facts, excerpt} bundle for the model to read.
python scripts/cli.py comprehend-input --name <brand>
# 2. Merge + validate + cache the model-authored comprehension.json (THE ONLY WRITER).
python scripts/cli.py comprehend --name <brand> --input comprehension.jsoncomprehend-input prints {facts, excerpt}: facts is the surfaced, format-uniform inventory the model reasons over and binds to (inventories: cover_anchors, fields, regions, roles, each entry an {"id": ...}), plus structure/anchors/styles slices; excerpt is an ordered, length-capped sample of text the extractor already collected. Read this bundle. Never parse the raw template or its OOXML.
comprehend is the single writer of the comprehension block. It re-runs the full schema validation and a fail-closed membership check, and only on a clean pass freezes the block into profile.json with status='present', stamping source_shell_sha256 from the live shell hash. On any finding it writes status='rejected' with the findings and exits non-zero; read the findings, fix the offending refs, and retry. A clean merge is idempotent: running comprehend twice yields a byte-identical profile.json.
The six questions (the SAME for every format)
Reasoning over the structure in the bundle, answer exactly these six questions. They are format-neutral: a cover slot is a cover slot, a derived index is a derived index, demo content is demo content, a brand color is a brand color, whether the file is a Word document, a PowerPoint deck, or an Excel workbook.
1. What is each element FOR? For each surfaced role and cover anchor, what is its purpose in this template? Annotate roles in role_annotations and name each cover slot's semantic_role/purpose. 2. What is demo vs real? Which surfaced regions hold placeholder/sample content the author is meant to replace, and which hold real, structural content to keep? Record each in demo_classification with a verdict. 3. Which artifacts feed which index? For each surfaced derived index (a table of contents, a list of tables/figures, an agenda/section list, anything the document regenerates from its own content), which role's items feed it, and should it be regenerated, preserved, or cleared? Record it in conventions.indexes. For a caption index (a list of tables or figures, identified by a non-null seq_id), also set caption_target (table or figure): it tells generate which captionable kind feeds the index, so it can emit the matching numbered SEQ field on each caption and regenerate the index from the new content. Without it a caption index can only be preserved or cleared (never repopulated), so its cache would keep the template's stale entries. 4. Which cover slot is which? For each surfaced cover anchor, which content slot fills it (binds_to), and should it be filled in place, cleared, or left alone (fill_rule)? Record it in cover_slots. 5. Which content shapes recur? Do the same few primitive blocks repeat as a unit across the template (a callout pattern, a titled card, a standard section opener)? fragment_candidates in the bundle hints at recurrences; trust your own reading of the excerpt too. For each genuine recurrence, propose a reusable fragment in fragments: a ref, a kind (component for a single inline fragment, section for a multi-block unit), a purpose, and a blocks template built ONLY from primitive block types. Put a {{slot}} token wherever the text varies per use; the author fills it via the referencing block's slots. Propose nothing when no shape genuinely recurs. 6. What is each brand color FOR / where does the template use it? The bundle's palette lists every brand color the extractor observed (a theme slot like accent1, or an off-theme hex:RRGGBB), each with its captured ref, provenance (where it was seen), and a coarse frequency. For each color, what role does it play in this template (a name, a purpose, a use_when, an optional semantic_role)? Record it in palette_annotations, keyed by the palette id. You name a color; you never author one - the deterministic capture owns the actual ref/hex, and a key into an empty/absent palette inventory is fail-closed. Annotate nothing when the template carried no color. An optional alias field on an annotation names a dotted-token alias for the captured color entry, minted at merge time with the captured ref byte-copied - so an off-theme hex:RRGGBB accent becomes addressable as a clean run-color token. The alias must be a syntactically-legal dotted token (the same lowercase family.qualifier grammar as a role id) that does not collide with an existing palette key or role id; you name it, the engine copies the captured ref.
The anti-overfitting directive (state and obey verbatim)
A title slot is a title slot whether its placeholder reads "Titolo", "Title",
or "Titre". Quote a literal only as evidence, never as a matching rule.
Reason over structure (builtin style ids, field codes, placeholder types, named
regions, geometry), not over the words a particular template happens to use.
Concretely, in the comprehension JSON:
- Every load-bearing reference - a
cover_slotskey (anchor_ref), an
index_ref, a region_ref, a feeds_from_role_id, a role_annotations key, a palette_annotations key (a palette id) - must be a verbatim id copied from the facts bundle. If an id is not in the surfaced inventory, do not invent it; the merge is fail-closed and will reject it (a ref into an empty inventory is itself an error).
- Eight fields are closed enums, and each maps to a real engine branch:
status (present|absent|rejected), fill_rule (in_place|clear|leave), reconcile (regenerate|preserve|clear), verdict (demo|real|mixed), a fragments entry's kind (component|section), a caption index's caption_target (table|figure), an audit row's verdict (PASS|FAIL|NA), and a triage entry's disposition (expected|defect). Use exactly those values.
- Every other field (
semantic_role, an index'skind,purpose,
generation_rules, evidence, region names) is an open advisory token. The generator never pattern-matches on it, so write it honestly for a human reader; never bend it to fit a fixed vocabulary.
Worked example (frozen role-ids + non-language placeholders only)
The example uses frozen role-ids (heading.1, the kind of id that appears in feeds_from_role_id and role_annotations) and non-language placeholder ids (<slot-1>, <index-A>, <region-1>, standing in for the surfaced cover-anchor / field / region ids) on purpose: it must carry zero plausible word from any human language, so it can never become a matching rule. Replace each <...> with a verbatim id from your own bundle.
{
"confidence": 0.86,
"cover_slots": {
"<slot-1>": { "semantic_role": "title", "purpose": "main cover title",
"binds_to": "title", "demo_value": "<captured placeholder text>",
"fill_rule": "in_place" }
},
"conventions": {
"indexes": [
{ "index_ref": "<index-A>", "kind": "outline", "seq_id": null,
"feeds_from_role_id": "heading.1", "reconcile": "regenerate" },
{ "index_ref": "<index-B>", "kind": "caption", "seq_id": "<seq>",
"feeds_from_role_id": "caption", "caption_target": "table",
"reconcile": "regenerate" }
],
"sections": [ { "region_ref": "<region-1>", "required": true, "repeatable": false } ]
},
"role_annotations": {
"heading.1": { "purpose": "top-level section heading",
"generation_rules": "one per major section" }
},
"demo_classification": {
"regions": [ { "region_ref": "<region-1>", "verdict": "real",
"evidence": "structural content region, not sample text" } ]
},
"fragments": [
{ "ref": "<fragment-1>", "kind": "component", "purpose": "recurring titled note",
"blocks": [ { "type": "callout", "intent": "note",
"runs": [ { "t": "{{body}}" } ] } ] }
],
"palette_annotations": {
"<color-1>": { "name": "primary brand color", "semantic_role": "accent",
"purpose": "headings and key emphasis",
"use_when": "drawing attention to a section title",
"alias": "accent.primary" }
}
}The <color-1> key is a verbatim palette id from the bundle's palette inventory (a theme slot like accent1, or hex:RRGGBB). The name / purpose / use_when / semantic_role are advisory free text you write for a human; you never author a color value - the deterministic capture owns the actual ref/hex, and the merge mirrors your names onto theme.palette without ever touching the captured color. The optional alias (here accent.primary) is a dotted-token alias for the captured color entry, minted at merge time with the captured ref byte-copied for reachability as a run-color token; it must be a syntactically-legal dotted token that collides with no existing palette key or role id, and again you only name it - the engine copies the captured ref.
Format readiness
Comprehension binds only to the ids a format's extractor actually surfaces. All three Office extractors expose cover anchors and regions when the template contains them. Derived-index fields are format-specific: DOCX can expose TOC/list fields, PPTX can expose an agenda/section-list field when present, and XLSX keeps fields intentionally empty because workbooks have no TOC-style field code. The model writes only the parts it has ids for; never force a cover, index, or region shape onto an empty inventory; a ref into an empty inventory is fail-closed and will be rejected.
Reusable fragments are format-neutral too: any format may carry a fragments proposal, and each lands in the profile's components / sections registry for generate to inline. A fragment's blocks must be primitive block types (never a style, color, or layout), so a proposed fragment resolves through the same brand chokepoint as inline content and cannot be off-brand. Propose fragments only when a shape genuinely recurs; an empty fragments list is the norm.
Persisting the visual-audit verdict (the audit map)
After a generate --qa deep|strict run, the engine writes a visual_manifest.json side artifact listing the rendered PNG pages, the exact-artifact shell_sha256 / content_sha256, and a profile-derived checklist (each item has an id). You (the model) open the pages and judge each checklist item. You MAY persist that judgement back into the comprehension as an audit map, keyed by the manifest's checklist[*].id, then merge it via comprehend:
"audit": {
"<checklist-id>": {
"verdict": "PASS", // PASS|FAIL|NA (closed enum)
"evidence": "cover title is bound, no residual demo prompt",
"shell_sha256": "<manifest.shell_sha256>",
"content_sha256": "<manifest.content_sha256>"
}
}The audit key MUST be a verbatim checklist[*].id from the manifest; a key that is not a current checklist id is fail-closed and rejected at merge (same rule as every other load-bearing ref). verdict is the closed PASS|FAIL|NA; evidence is advisory free text for a human; the two shas are copied verbatim from the manifest so the verdict is scoped to that exact artifact. You never write a brand value here, only a disposition against a structural checklist id the engine derived.
This makes the next generate cheaper: a same-shell / same-content generate short-circuits the L2 render+manifest round when every current checklist item PASSes at the matching (shell_sha256, content_sha256). Any FAIL, any NA, a newly-derived checklist id with no row, or a sha mismatch forces a full L2 round, so the short-circuit can never hide a regression. It is disabled under --qa strict (which always re-renders) and never fires at verify. An empty audit map is the norm until you have judged a render.
Triaging an ambiguous QA warning (the triage list)
Some QA checks emit a WARNING because the signal is genuinely ambiguous: a full-bleed cover that the edge-bleed proxy flags, a deliberately blank section page, or a native component family the new content legitimately has fewer of. When you (the model) have judged that such a WARNING is an EXPECTED property of this template - not a defect - you MAY persist that judgement as a triage list, merged via comprehend:
"triage": [
{
"check": "visual.edge_bleed", // expected|defect maps the disposition
"location": "page:1:bottom", // the finding's exact location (or null)
"disposition": "expected", // expected -> demote that WARNING to INFO
"evidence": "the cover is intentionally full-bleed in this brand"
}
]Only three checks are triage-eligible, and they are ALL WARNING-only: visual.blank_page, visual.edge_bleed, and component_survival. A triage entry naming any other check is fail-closed and rejected at merge, and two entries for the same (check, location) pair are rejected as ambiguous. disposition is the closed expected|defect; location is copied verbatim from the QA finding (it may be null); evidence is advisory free text for a human.
The effect is deliberately narrow and one-directional: an expected entry demotes exactly the matched WARNING to INFO (the message keeps a (triaged EXPECTED: ...) note); a defect entry, or any entry that does not match a finding, leaves the gate verbatim. A triage entry can NEVER turn an ERROR into a warning, nor raise a severity - the eligible checks are WARNING-only and the demotion only ever lowers a WARNING to INFO, so a real failure can never be silenced. An empty triage list is the norm.
Promoting a faked heading onto a real heading role (the promote_appearance list)
Some templates fake a heading: a line that LOOKS like a heading - visibly larger and/or in a brand color - is authored with the body paragraph style (no heading role, no named heading style), so the deterministic engine treats it as body and the brand heading look is lost. The extractor's pure-deterministic detector surfaces each such body-style run as a pseudo_heading fact in the bundle when its captured size or color is a clear outlier vs the dominant body appearance. Each fact carries a stable structural ref, the run's OWN captured outlier size_hp (half-points) and/or color, and coarse, brand-text-free evidence (e.g. "size 44hp vs dominant body 24hp"). The list is absent when the detector found no outlier.
When you judge that a candidate is a real heading the template faked, you MAY persist a promote_appearance list, merged via comprehend:
"promote_appearance": [
{
"pseudo_heading_ref": "body_run_3", // a verbatim ref from facts.pseudo_headings
"target_role_id": "heading.1" // a declared heading.* role to promote onto
}
]You name only the two ids; you never author a size or color. On a clean merge the engine COPIES the captured outlier size_hp/color from the detector fact onto roles[target_role_id].appearance, so the generated heading carries the size/color the template proved it uses. The pseudo_heading_ref MUST be a ref the detector surfaced (an empty/absent pseudo_headings inventory fails closed), target_role_id MUST be a declared heading.* role (a non-heading target is rejected), and a (ref, target) pair must be unique. The promoted size/color is re-validated shell-backed at QA exactly like any applied appearance: a value the shell does not carry is an ERROR, so the engine can never inject a size/color the template lacks. An empty promote_appearance list is the norm.
Refining the understanding from user feedback (the refine verb)
After a generation, the user may give qualitative feedback - in text or as a screenshot of the produced file. You turn that answer into a small, structured refinement delta of verbatim ids and merge it with the refine verb:
# Overlay the delta onto the EXISTING comprehension, then re-validate the whole block.
python scripts/cli.py refine --name <brand> --input refinement.json
# Add --accept to persist the refined comprehension (else the diff is previewed).
python scripts/cli.py refine --name <brand> --input refinement.json --acceptA refinement delta touches ONLY the existing qualitative-understanding sinks - role_annotations, palette_annotations, demo_classification, cover_slots, and conventions (indexes / sections). It is not a schema change and it never writes a brand value: a palette annotation carries only name / purpose / use_when / semantic_role (the ref / hex stay the deterministic capture's), and every ref is still a verbatim id from the surfaced inventories.
"palette_annotations": {
"<color-1>": { "name": "primary brand", "use_when": "section headings" }
},
"role_annotations": {
"heading.1": { "purpose": "section titles", "generation_rules": "keep terse" }
}The verb overlays the delta onto the present block (replacing or adding the named sink entries; list sinks merge by their ref, never naive concat), then routes the WHOLE combined block back through comprehend's single fail-closed writer: the full schema + membership validation re-runs and every ref re-binds to surface_inventories, so a delta naming an id that is not surfaced (or one in an empty inventory) is rejected and nothing is written. Without --accept the post-overlay diff is previewed and the prior block stays authoritative; --accept persists it and re-stamps source_shell_sha256.
A screenshot is YOUR multimodal read of the produced file; the engine only ever ingests the resulting structured JSON delta of verbatim ids - it never sees the image. The feedback ask happens only after the file and its QA summary are returned, never before or during generation, and a refinement improves FUTURE generations - it never re-emits or edits the file you just produced.
Proposing overrides corrections from recurring findings (the propose-overrides verb)
refine sharpens the qualitative understanding; propose-overrides is its peer for shell-bound corrections - the model-assisted sibling of the deterministic learn verb. When the same QA finding keeps recurring across runs, the engine's deterministic learn distils the ones it can bind to a brand-safe target on its own (a stub role with a healthy same-family sibling, a captured demo string). The ambiguous remainder - a stub role with no sibling, a finding whose right re-point needs judgement - is surfaced to you in the comprehend-input bundle under facts.generation_history: a bounded, message-free list of {check, location, severity, recurred_runs} (the universal (check, location) identity only, never the finding's message text). You reason over it and author a small overrides proposal, then merge it:
# Overlay the proposal onto any existing lesson, then re-validate the whole block.
python scripts/cli.py propose-overrides --name <brand> --input overrides.json
# Add --accept to make the correction LIVE (else it is written advisory-'absent').
python scripts/cli.py propose-overrides --name <brand> --input overrides.json --acceptA proposal may ONLY NAME a shell-backed pointer - never author a style, font, or color:
// reroute a role whose resolver is a dead stub to an EXISTING healthy role:
"reroute_roles": { "heading.9": "heading.1" },
// swap a number_format MASK the shell already uses (xlsx):
"number_format_swaps": { "<role-1>": "#,##0" },
// register a CAPTURED demo string for clearing:
"demo_clears": ["<captured-demo-value>"]The verb overlays the proposal onto any present lesson (additive: a deterministic learn lesson and your proposal coexist, your entry winning a key collision) and routes the WHOLE combined block through the single merge_overrides writer: the target role must be a declared, shell-backed role, the mask must be one the shell uses, and the demo value must have been captured - any unbound pointer rejects the WHOLE proposal (all-or-nothing), so an off-brand correction is impossible by construction. Without --accept the correction is written but kept OUT of the live resolver (status='absent', byte-identical generation); --accept makes it LIVE and re-stamps source_shell_sha256, so a re-extract (new shell) invalidates it.
Like the feedback ask, this happens only after generation and improves FUTURE generations only. Every LIVE correction is auditable: the gate emits an INFO override_applied finding for each one (in generate and verify alike), so a learned re-point is never silent.
<!-- SPDX-License-Identifier: MIT -->
Visual audit (two-stage)
The visual audit sits on top of the L0 deterministic gate (schema, resolver targets, residual text, structural diffs). L0 stays unchanged and authoritative. The audit adds two stages that see what L0 cannot, the rendered layout:
- L1: deterministic pixel proxies (engine). The engine renders the output to
per-page PNGs and runs pixel checks that flag rendered-layout defects. During --qa deep, optional Tesseract OCR also scans the rendered pages for captured template/demo text. Each defect is a Finding(check="visual.<name>"), severity WARNING (never ERROR, so the audit never fails a gate that L0 passes).
- L2: qualitative judgement (orchestrator, i.e. you). The engine emits a
structured visual_manifest.json (PNG paths + a profile-derived checklist + the L1 findings). You open the PNGs, judge each checklist item PASS/FAIL, and drive a repair loop. The engine never calls a model.
The PNGs and the manifest are side artifacts written to an <output-file>.visual/ dir next to the output, such as workbook.xlsx.visual/. The bytes of the generated document never change because of the audit.
When it runs (--qa)
--qa | renderers present (soffice + pdftoppm, or optional PyMuPDF/fitz fallback) | renderers absent (e.g. CI) |
|---|---|---|
fast | L0 only | L0 only (identical) |
auto | L0 + L1 | L0 + one INFO visual.unavailable |
deep | L0 + L1 + manifest (INFO visual.manifest with the path) -> triggers your L2 step | L0 + INFO visual.unavailable + a degraded manifest with the checklist; on macOS a first-page Quick Look fallback may be included |
strict | L0 + L1 + OCR/manifest + ERROR visual.strict if render/L1/OCR evidence is unclean | L0 + degraded manifest + ERROR visual.strict_unavailable |
Notes:
- At verify time there is no output to render, so every mode behaves as L0.
- Renderer absence degrades cleanly: INFO/WARNING findings only, no ERROR, **exit
code unchanged**. auto may create no .visual dir; deep writes a degraded .visual/visual_manifest.json so the orchestrator can still inspect what was skipped and which checklist items remain unproven.
strictis the release-quality mode: it writes the manifest likedeep, but
fails when full render proof is unavailable or when L1/OCR findings need repair.
- The renderer is env-detected via
doctor.probe()["visual_qa"], which
smoke-tests the DOCX, PPTX, and XLSX render paths end to end; run python scripts/cli.py doctor before starting the workflow and report missing/unusable dependencies before claiming a full visual audit.
pdftoppmremains the baseline PDF rasterizer. If it is missing or fails after
LibreOffice produced a PDF, optional PyMuPDF (fitz) can rasterize the PDF as a degraded fallback; the manifest records this in environment.optional_python.
- Optional OCR uses the external
tesseractbinary. Missing OCR does not block
the visual audit; it only means rendered residual-text proof is incomplete. The manifest records OCR availability in environment.ocr and results in ocr.
L1 proxies
| check id | catches (that L0 cannot see) | severity |
|---|---|---|
visual.blank_page | a blank/near-blank page: broken page, content not rendered, or overflow that pushed everything off the page | WARNING |
visual.edge_bleed | content touching/exceeding the printable margins (text/image clipping or overflow); the defect OverflowCapability.RENDER exists to intercept | WARNING |
visual.no_pages | an existing output that rendered zero pages | WARNING |
visual.ocr_residual_text | optional OCR saw captured template/demo text still visible in the render | WARNING |
The proxies are deterministic (aggregate luma/ink fractions, no randomness, no single-pixel font-hinting sensitivity) and conservative (tuned to render data: content pages mean-luma ~240-252, a blank page ~255) to minimize false positives. They accept either a PIL image or a PNG path and never raise on an unreadable image (they return no findings instead).
A WARNING is a signal for your L2 judgement, not a standalone failure: a legitimately near-empty page (a minimal cover/separator) or a deliberate full-bleed background exists, and only you can tell those apart from a real defect.
The manifest (visual_manifest.json)
Top-level fields:
{
"schema_version": "visual-manifest-1",
"kind": "xlsx",
"profile_name": "<identity.name>",
"document": "out.xlsx",
"renderers_available": true,
"qa_mode": "deep",
"dpi": 100,
"pages": [
{"index": 1, "png": "page-1.png", "width": 850, "height": 1100, "orientation": "portrait"}
],
"l1_findings": [
{"check": "visual.blank_page", "severity": "WARNING", "message": "...", "location": "page:2"}
],
"ocr": {
"engine": "tesseract",
"available": true,
"status": "ok",
"terms_checked": ["<captured template/demo text>"],
"pages": [{"index": 1, "text": "...", "text_truncated": false}],
"hits": [],
"errors": []
},
"environment": {
"platform": "macOS-...",
"python": "3.x",
"visual_qa": true,
"degraded": false,
"renderers": {
"soffice": {"available": true, "path": "/path/to/soffice"},
"pdftoppm": {"available": true, "path": "/path/to/pdftoppm"}
},
"optional_python": {
"fitz": {"available": false, "purpose": "PyMuPDF PDF raster fallback"}
},
"ocr": {
"tesseract": {"available": true, "path": "/path/to/tesseract", "purpose": "optional OCR visible-text audit"}
},
"install_hints": []
},
"checklist": [
{"id": "regions_present", "what": "...", "derived_from": "structure.skeleton[*].region + order", "severity_hint": "WARNING"}
],
"instructions": "Open each PNG. For each checklist item, judge PASS/FAIL ..."
}pages may be populated even when renderers_available is false: this means a degraded fallback image (for example a macOS Quick Look thumbnail) is available for inspection, not that full page-by-page render proof succeeded. Treat those PNGs as advisory evidence only and do not claim a clean visual audit from them.
png paths are relative to the `.visual` dir. When renderers are absent the manifest carries "degraded": true; pages is populated only if an advisory fallback image exists, l1_findings still records visual failure/degradation signals, and the checklist stays populated so you know what would have been inspected with full render proof.
Checklist items (derived from the profile, model-free)
Each item is included only when its backing data exists in the profile, so the checklist is tailored to the template:
| item id | inspect | derived from |
|---|---|---|
regions_present | expected regions appear in order | structure.skeleton[*].region + order |
cover_correct | bound title, no duplicate, no demo prompt | anchors.cover + comprehension.cover_slots |
no_residual_placeholder | no template demo text visible | surface.<kind>.demo_region + cover demo values + optional ocr.hits |
palette_on_brand | on-screen colors are brand colors | theme.colors + theme.palette_roles |
roles_styled | semantic blocks carry brand styles, not "Normal" | roles._index |
no_overlap | no overlapping/clipped text or shapes | constant; reinforced by visual.edge_bleed |
no_blank_pages | no unexpected blank/broken pages | constant; reinforced by visual.blank_page |
charts_rendered | charts drawn correctly, not empty boxes | present only when a chart role/component exists |
overflow_clean | nothing past the printable margins | qa.overflow_capability ∈ {render, estimator, cellfit} |
L2 repair loop (your protocol)
1. Generate with --qa deep. 2. Read the manifest path from stdout (visual manifest: <path>). 3. Open every pages[*].png. For each checklist item, judge PASS/FAIL against the rendered pages, taking l1_findings and ocr.hits into account (an L1 WARNING is a pointer to a page/side worth looking at, not a verdict). 4. If any item FAILS (or an L1/OCR WARNING is a real defect on inspection): repair the IntermediateDocument/content, regenerate, and re-run the audit. 5. Repeat until the checklist is clean (max 3 iterations by default), then return the file with an honest QA summary. 6. For release-quality validation, rerun with --qa strict; any visual.strict ERROR identifies the exact visual finding that still needs targeted repair.
The engine produces the evidence; the qualitative judgement and the decision to regenerate are yours.
# SPDX-License-Identifier: MIT
from __future__ import annotations
import os
import sys
from pathlib import Path
def _root(start: Path) -> Path:
env_root = os.environ.get("BRAND_DOCS_ROOT")
if env_root:
root = Path(env_root).expanduser().resolve()
if (root / "scripts" / "brandkit").is_dir():
return root
for parent in [start] + list(start.parents):
if (parent / ".claude-plugin").is_dir():
return parent
return start.parents[3]
ROOT = _root(Path(__file__).resolve())
sys.path.insert(0, str(ROOT / "scripts"))
from brandkit.cli import main # noqa: E402
if __name__ == "__main__":
raise SystemExit(main())