Metadata-Version: 2.4
Name: charter-extract
Version: 0.1.1
Summary: Charter extraction into DTCG (charter-dtcg v1). Office/OOXML deterministic primary, URL inferred secondary.
Author: ElPi Corp
License: FSL-1.1-Apache-2.0
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: lxml>=5
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Requires-Dist: jsonschema>=4; extra == "test"
Requires-Dist: python-pptx>=1; extra == "test"
Requires-Dist: python-docx>=1; extra == "test"

# charter-extract

Reads a brand charter out of a source document and emits DTCG conforming to the **charter-dtcg v1** contract. Owned by Rho; consumed by Hephaistos's Railway extraction service (this package is the logic, not the deployment).

## Two routes, and why one is the default

| Route | Provenance | Default? |
|---|---|---|
| Office `.pptx` / `.docx` | `ooxml-theme` | **yes, whenever a document exists** |
| URL | `firecrawl-branding` | only when there is no document |

`theme1.xml` carries a colour scheme (`dk1`/`lt1`/`accent1-6`) and a font scheme whose entries are **labelled by role**. Reading them is a lookup. Every other source infers the role, and an inferred charter costs more to correct by hand than a read one costs to obtain.

## The trap this module exists for

The theme says which colours exist. The **colour map** says which role each one plays. `bg1="dk1"` is perfectly legal and swaps surface and text.

A reader that parses `clrScheme` and skips `clrMap` returns a plausible charter with the background and the text inverted — and nothing about the document looks broken. `fixtures/inverted.pptx` is that document: byte-identical theme to `fixtures/normal.pptx`, one different map. The test suite asserts the two fixtures' themes are identical first, so the control cannot pass for the wrong reason.

Word says the same thing three ways: `w:`-prefixed attributes, `t1` instead of `tx1`, and `light1`/`dark1` where the theme itself says `lt1`/`dk1`. All three are normalised before resolving. That divergence was found by the `.docx` test, not by reading the spec.

`python-pptx` and `python-docx` do not expose the theme, hence lxml directly. **PyMuPDF is excluded — AGPL.**

## What the Office route refuses

`accent1` is where OOXML puts the brand's leading colour. If a theme does not declare it, the document never named one — and the extractor **raises** rather than promoting the next accent into `primary`.

That refusal exists because of where the value would have gone, not because a fallback is unthinkable. Promoting accent2 would ship a colour the document never mentioned, carrying `provenance: ooxml-theme` and `confidence: 1`, through the only route that reaches a consumer with no human in it. An honest `source` field naming `a:accent2` does not redeem a provenance claiming determinism for a substitution.

Marking it inferred instead is not available: the provenance enum is closed and v1 is frozen, so "guessed from an OOXML theme" is not a value that exists. A fallback that cannot declare itself does not get to travel.

A gap **further down** the ladder is legal and passes: `accent3` missing does not stop the document from naming its leading colour. A guard that refused there would punish a legal theme, and a guard that punishes the legitimate gets removed.

## The confirmation gate

Office fields are deterministic: `confidence: 1`, and they carry **no** `confirmed` flag. Asking a human to confirm a value read verbatim from a theme teaches them to click through everything, and then the gate guards nothing.

URL fields are guesses. They are emitted with `confirmed: false`, which makes the charter **deliberately contract-invalid** until a human passes the gate — an inferred charter must not reach a consumer unreviewed.

## Reuse, not rewrite

The URL route runs the command the existing `brand-kit-extractor` agent already uses:

```
firecrawl scrape <url> --format branding,markdown
```

A test asserts that command string verbatim, so a future bespoke scraper fails loudly instead of quietly replacing it. That agent also documents the failure mode worth knowing: Firecrawl reads CSS variables, which do not always match what a visitor sees.

## Running

```
python -m venv .venv && .venv/bin/pip install -e ".[test]"
.venv/bin/python fixtures/build_fixtures.py
.venv/bin/python -m pytest -q
```

---

