Authoring patterns.
Purpose
Manufacturers publish their products in wildly different shapes. One ships a separate cutsheet for every orderable SKU; another ships a single cutsheet that covers tens of thousands of SKUs through a configurator. A ULC record has to represent both ends of that range without duplicating data or losing it. This document describes the patterns for mapping real manufacturer documentation into ULC records.
It is not normative. It describes patterns, not requirements, and it informs the shape of ulc.schema.json and the reference validator rather than constraining them. The schema is the normative artifact.
What a ULC record is
A ULC record represents one attested photometric scenario for a fixture. That definition does three things:
- It pins each record to measurement evidence: an LM-79 test, or a declared derivative of one.
- It decouples records from orderable SKUs, so the thousands of SKUs a manufacturer produces do not force thousands of ULC files.
- It decouples records from IES files, so a manufacturer that ships many IES files derived from a smaller base-test set does not need a separate record for each one.
Two links carry the weight. A structured applicability block inside the record connects it to the orderable SKUs it describes. Provenance metadata on each value connects that value to the measurement evidence behind it.
The four authoring patterns
Four distinct patterns emerged across four manufacturers during the schema evaluation phase. Each is grounded in a real cutsheet.
Pattern A: one SKU per cutsheet
Example: Erco Quintessence Downlight 30416.023. One cutsheet, one order code, one IES file, one LDT, one ULD, no configurator.
Each orderable SKU gets its own cutsheet PDF and its own accompanying files. Nothing expands combinatorially inside a part number. Accessories are separately numbered SKUs that the cutsheet cross-references.
ULC mapping: one record per SKU. The applicability block is narrow, covering the single orderable configuration. Accessories are listed in compatible_accessories[] with catalog numbers and references; none gets its own photometric record.
Records per cutsheet: 1.
Pattern B: one record per photometric scenario
Example: Selux AYA Pole. A single cutsheet covers roughly 54,000 theoretical SKUs across optics, mounting, output tier, CCT, finish, voltage, power-cord length, and option combinations. Page 10 declares 21 photometric scenarios: three distributions × three outputs for the white light engine, plus 12 BioRed scenarios. CCT variance is declared through a multiplier table (2200K=0.86, 2700K=0.93, 3000K=1.00, 3500K=1.00, 4000K=1.07, 5000K=1.07) applied to a single tested 3000K baseline per optic × output combination.
ULC mapping: one record per tested photometric scenario. The applicability block is wide: it declares which SKU axes the record covers and how derived values are computed for each. A single Selux record covers all six white CCTs through the multiplier table, all six voltage variants through the regulated-driver rationale, all finish options because finish does not affect photometry, and so on.
Records per cutsheet: 21 (Selux AYA), not 54,000.
Pattern C: one record per IES, with provenance classes
Example: Lumenpulse Lumenfacade Inground Color Changing (LOI). One cutsheet publishes 1,728 IES files covering 4 lengths × 4 color modes × 14 optics × 2 tilts × 2 optical-option states. The IES headers reveal that those 1,728 files derive from a much smaller base-test set (distinct test IDs are visible, at least five in a random sample of five files), combined through optical simulation (Synopsys LightTools) and mathematical scaling. Every file carries the header note This file was scaled by Lumenpulse to reflect a different fixture configuration.
The manufacturer chose to materialize the full extended-photometry matrix as discrete IES files, so specifiers pick by configuration and get an IES that matches their exact order code.
ULC mapping: one record per IES file, preserving the 1:1 mapping specifiers expect. Each record declares its photometric provenance class explicitly:
- Values attested by a physical LM-79 carry
provenance.method: extractedwithvalue_type: measured. - Values generated by a simulation tool carry
provenance.method: optical_simulationwithvalue_type: ratedand a reference to the calibrating attestation. - Values produced by scaling a base test carry
provenance.method: extended_photometry(or the more generalscaled) withvalue_type: ratedand a reference to the base attestation’s test ID.
Consumers who need attestation fidelity (DLC applications, spec audits) filter to measured-only records; consumers who just need a photometric file for design work use whatever matches their configuration.
Records per cutsheet: 1,728, matching the IES count. Roughly 5 to 20 carry measured provenance; the rest carry simulation or extended-photometry provenance.
Pattern D: per-foot linear scaling
Example: Vode Nexa Suspended 807. Performance tables on pages 8 and 9 declare 48 photometric scenarios (2 optics × 3 outputs × 2 CRI tiers × 4 CCTs). Measurements are expressed per foot (lumens per foot, watts per foot, efficacy). Fixture lengths from 48 to 96 inches multiply the per-foot values linearly, and a single IES file at a reference length anchors the scaling.
The ordering configurator has 18 positions, most of them non-photometric (mounting type, canopy style, power-location cable length, sensor presence, finish, options). These multiply the SKU count without changing photometric output.
ULC mapping: one record per tested photometric scenario, the same principle as Pattern B. Photometric values are carried both as baseline-measured-at-length and as per-foot normalized. Length becomes an applicability axis with explicit linear-scaling rules. The non-photometric axes (mounting, canopy, finish, sensor, power location) live in applicability.covered_axes with a rationale that each is photometrically transparent.
Two Vode-specific attestation patterns also appear:
- Option-conditional attestations: Chicago Plenum compliance requires the
CPPorder-code option and Remote Power, so it is an attestation withapplicability.required_order_code_options: ["CPP"]plus a compatibility constraint. - Case-by-case attestations: BAA and BABA compliance depend on per-project manufacturer verification, so they are attestations with
value_type: nominalandverification: requires_manufacturer_confirmationplus acontact_reference.
Records per cutsheet: 48 (Vode Nexa), with per-foot scaling covering every length variant inside each record.
The primitives that support all four
product_family
A top-level block carrying the data that is true for every SKU in the cutsheet. The PIM populates it once and replicates it into each record emitted from the family.
Typical contents: family_id, family_display_name, manufacturer, catalog_line, catalog_model, cutsheet (filename, URL, sha256, revision), primary_category, shared_mechanical, shared_warranty, and shared_attestations (claims that apply to every SKU without qualification, such as UL Listing and Declare Red List status).
Consumers group records by family_id to reconstruct cutsheet-level views without forcing cutsheet-level JSON bundles.
configuration
A top-level block identifying the specific photometric scenario the record represents. Its fields describe the tested configuration, not the applicability range.
Typical contents: photometric_scenario_id, catalog_number (when the scenario is one specific SKU), scenario_label, tested_axes (distribution code, light-engine variant, output tier, CRI tier), tested_conditions (tested CCT, tested voltage, tested mounting, ambient temperature), and source_ies_ref (a string reference to the matching entry in the top-level source_files[] array).
applicability
A top-level block declaring the range of orderable SKUs the record’s measurements apply to. This is what keeps one record from being rewritten for every SKU variant.
applicable_catalog_pattern: the order-code skeleton, with fixed and variable axesfixed_axes: order-code segments that must match (for exampleOptics=SR,Output=HO)covered_axes: segments where several values share this record’s photometry, each with a rationale and an optional derivation rule (CCT multiplier table, per-foot linear scaling, regulated-driver voltage independence, and so on)excluded_combinations: combinations that are orderable for other records but not this one (for example “Max Output not available with 2200K, 2700K, 3500K, 5000K”)applicable_sku_count_estimate: informational
Provenance classes
Every photometric value declares a provenance method from a closed set. The values that matter for cutsheet authoring:
extracted: pulled directly from a source filevalidated: checked against a second sourceoptical_simulation: generated by an optical design tool calibrated against a base LM-79. Carriesprovenance.simulation_toolandprovenance.base_attestation_ref, and pairs withvalue_type: rated.extended_photometry: derived from a base LM-79 attestation by manufacturer-applied scaling rules. Carriesprovenance.base_attestation_refandprovenance.extension_method, and pairs withvalue_type: rated.scaled: general closed-form mathematical scaling (CCT multiplier, per-foot scaling, wattage-tier scaling). Pairs withvalue_type: rated.
Consumers filter records by these classes to prioritize direct attestations when they need to.
Conditional attestations
Two patterns from the Vode cutsheet do not fit the absolute-attestation shape.
An option-conditional attestation is true when a specific order-code option is selected and false otherwise:
{
"program": "chicago_plenum",
"value_type": "rated",
"applicability": {
"required_order_code_options": ["CPP"],
"required_constraints": { "Power Location": "remote_power" }
}
}
A case-by-case attestation is one the manufacturer supports but requires per-project verification before a consumer may represent the SKU as compliant:
{
"program": "baa",
"value_type": "nominal",
"verification": {
"type": "requires_manufacturer_confirmation",
"contact_reference": "compliance@manufacturer.example"
}
}
A validator must not propagate a case-by-case attestation downstream as if it were a measured or rated claim. The verification.type field is the signal.
Sustainability declaration
A Declare label (International Living Future Institute) carries structured data well beyond a “has a Declare label” boolean. ULC models it as a dedicated sustainability_declaration block that preserves the ingredient list, expiration date, LBC Red List tier, and document identifier.
Typical contents: declaration_type (for example ilfi_declare), document_id, expiration_date, original_issue_date, final_assembly_location, life_expectancy_years, end_of_life_options, recyclable_percent, ingredient_list[] (each with material_name and lbc_red_list_status), lbc_criteria_compliance (a boolean for overall Living Building Challenge compliance), voc_content, interior_performance, and responsible_sourcing. When the declaration is specifically a Red List statement rather than a full Declare label, the tier claim lives on the top-level declaration_type field (one of red_list_free, red_list_approved, red_list_declared).
Generated index
Every record carries a top-level index block: a flat, denormalized summary of the most commonly queried values (manufacturer, catalog, primary category, nominal CCT, total lumens, input power, BUG, attestation programs, search keywords). It exists to make AI scanning, search indexing, and filter UIs cheap, so a consumer can read the index without walking the deep blocks beneath it.
The index is generated, never hand-authored. The spec forbids manufacturers from typing index values. The canonical builder is the ulc build-index subcommand of the Go reference CLI at tools/validator/. It reads the deep blocks (product_family, configuration, electrical, photometry, colorimetry, outdoor_classification, attestations, sustainability_declaration) and writes the index deterministically. A manufacturer’s PIM or authoring tool runs the builder as the final step before emitting a record.
Two markers make the index self-describing:
x-ulc-generated: true: the builder produced the blockbuilder_version: the semver of the builder that produced it
A consumer that reads x-ulc-generated: true can treat the index as trustworthy. A missing marker or a stale builder version is the signal to rebuild.
Drift is prevented by construction, because the index is a pure function of the deep blocks. The builder holds the selection policies in one place: which CCT counts as nominal, how SI-authoritative scalars are extracted from dual-unit fields, which variant of a multi-CCT record supplies the baseline value. SI is always authoritative, and the dual-unit policy is fixed, not per-record. CI runs ulc build-index --check against every committed record as a second line of defense, and an optional local pre-commit hook with the same check ships at tools/hooks/pre-commit (see CONTRIBUTING.md for installation).
This mirrors precedent across the industry: DLC QPL, ETIM MC catalogs, and GLDF-authoring tools typically generate their scan surfaces from tooling rather than having manufacturers hand-author them.
Measured baseline with declared-by-axis scaling
Measurements that a manufacturer scales by a declared rule (Selux CCT multipliers, Vode per-foot rates) are modeled by pairing the measured baseline with a sibling array of declared values across the covered axis.
- The baseline field (for example
photometry.total_luminous_flux_lm) is a singleProvenancedNumberholding the tested value, withvalue_type: measuredandprovenance.attestation_refpointing at the LM-79 that produced it. - A sibling array (
declared_by_cct[], ordeclared_by_length[]for the length axis) carries one rated entry per covered value, each with its derivation method andvalue_type: rated.
A Selux record, for example:
"photometry": {
"total_luminous_flux_lm": {
"value": 5074,
"unit": "lm",
"value_type": "measured",
"provenance": {
"source": "ies",
"method": "extracted",
"attestation_ref": "lm79_selux_aya_sr_ho"
}
},
"declared_by_cct": [
{ "cct": 2200, "lumens": { "value": 4364, "unit": "lm", "value_type": "rated" } },
{ "cct": 3000, "lumens": { "value": 5074, "unit": "lm", "value_type": "measured" } },
{ "cct": 4000, "lumens": { "value": 5429, "unit": "lm", "value_type": "rated" } }
]
}
Choosing a pattern
Manufacturers do not choose a pattern in the abstract. They choose an authoring surface, and the pattern follows from their own data model.
- Tests each SKU individually and publishes per-SKU cutsheets: Pattern A.
- Publishes one cutsheet covering a configurator, with declared scaling tables inside the cutsheet: Pattern B.
- Publishes a dense pre-computed IES bundle, each file mapping to a configurator output: Pattern C.
- Normalizes per-unit-length measurements for a linear product family: Pattern D.
A single manufacturer may use different patterns for different product families. A single product family commits to one pattern, to avoid ambiguity.
How records reference each other
- Family grouping: every record in a cutsheet shares a
product_family.family_id. Consumers group on this key to reconstruct cutsheet-level views. - Attestation inheritance: derived records (Pattern C simulations and extended-photometry records) carry
provenance.base_attestation_refpointing at the measured base record’s test ID, so a consumer can trace any rated value back to the measurement it rests on. - Accessory references: mechanical accessories listed in
compatible_accessories[]are identified by their own catalog numbers. An accessory needs its own photometric record only when it changes fixture photometry.
Validation implications
The reference validator uses the patterns and primitives above as follows:
- Conformance level is computed, not declared. The builder grades each record against the rubric and writes the level it achieves into the generated index as
index.conformance_level, so the level a record carries is the level its data actually reaches (a hand-edited value fails the build-parity check, like any other index field).coreis the minimum identifying and photometric dataset;standardcovers what a typical LM-79 test report produces;fullrequires operating-point qualifiers (and, for outdoor products, a BUG rating). Records at full commonly also carry deeper comprehensive data (TM-30 hue bins, method-backed lumen-maintenance projections, measurement uncertainty, and instrumentation metadata), which the v1 rubric reports as INFO observations rather than hard requirements.ulc validatereports the achieved level and, belowfull, the specific fields that would raise it. The level is never a pass/fail gate.
The following integrity checks are defined by the specification but are not yet implemented in the reference validator. The v1 validator runs schema validation, builder parity, source-file hashes, and conformance reporting only; these are planned for a post-pilot release:
- Provenance integrity (planned): every
value_type: measuredfield should carry anattestation_ref. Avalue_type: ratedfield whosemethodis anything other thanextractedornormalizedshould carry anattestation_refor abase_attestation_refpointing at a measured record in the same family. - Cross-record consistency (planned):
family_idshould be stable within a cutsheet bundle, and every referencedbase_attestation_reftest ID should resolve to a record present in the bundle or cited with a manufacturer URL. - Conditional attestation handling (planned): a
verification.type: requires_manufacturer_confirmationattestation is nominal, and a downstream record that inherits one as validated should raise a WARNING. - Sustainability expiration (planned): a
sustainability_declaration.expiration_datein the past relative to the record’srecord_status_as_ofdate should raise a WARNING.