Skip to content

Content: catalogue, procedural, LLM, artifact

"Content" is everything a run needs to invent an invoice that isn't computed on the spot: the roster of companies, and for each the product line it sells. It comes in layers — a procedural baseline, an optional LLM overlay, published as an artifact — all sitting behind the catalogue.

The catalogue model (catalog.py)

Catalogue / CompanyRoster / Company / ProductTemplate are the shape: a company (with its business type, jurisdiction, product category and niche) and the products it can bill. SeedCatalogue is the built-in one shipped in the pack, so a run works with no build at all.

Identity is derived, not stored. A company's address, phones, emails, and tax registrations come from derive_identity(company_id) at generation — deterministic and format-valid, but never written to the artifact (① no PII).

Procedural (procedural.py)

The key-free baseline: a combinatorial pool (~7,900 base descriptions) expands into each company and its products deterministically per index (generate_company / generate_company_range). This is always present — the skeleton the LLM overlays, and the fallback for anything the LLM can't fill.

LLM overlay (llm_build.py)

When a provider mix is configured, an LLM rewrites product descriptions (and co-generates a price band) in chunks — the semantic long tail a combinatorial pool can't reach. Structural fields, the roster, and the fallback stay procedural: any slot the LLM can't fill keeps its procedural product, so a build is always complete. The price band is a sampling input, never a golden number — a bad one is dropped for the procedural band.

Validation (validation.py)

Every generated description is screened before it is written — quality (empty, assistant preamble, markdown, marketing prose, too long/short) and PII (emails, URLs, long digit runs, honorific+name, entity suffixes), plus cross-item dedup. A flagged description is rejected and falls back to procedural text; too high a rejection rate fails the build. The same gates run over procedural content (that is how they're tested without a key), and the results are recorded in the manifest as an audit.

Artifact (artifact.py)

The published form: per-company Parquet, decimal128 money, sharded read/write, a sha256 per shard, and a root manifest carrying the catalogue_version (stamped on every golden row a run produces), created_at, and the build provenance (models, fill rate, cost, rounds). Reading it needs no credential (file:// / gs:// / s3://), so a pack with a rich catalogue is as local-first as one without.

flowchart LR
    Proc["procedural skeleton"] --> Merge
    LLM["LLM overlay<br/>(optional)"] --> Gate["validation.py<br/>quality + PII"]
    Gate -->|accept| Merge
    Gate -->|reject| Proc
    Merge["per-company records"] --> Art["artifact.py<br/>parquet · sha256 · manifest"]

See Catalogue building for the build flow (a sharded run under a budget) and Providers for the mix mechanics.