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, not an orderable SKU and not an IES file. The reasoning behind that unit, and the two decouplings it buys, is in methodology.md; what matters for authoring is the two links that 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.

Lifecycle facts are monotonic. Beside record_status and record_status_as_of, a record may carry superseded_by (the successor pointer, by successor record_id, order code, or family model, with an optional record_sha256 pin of the exact successor record revision) and discontinued_at (the effective end-of-availability date). Both are recorded once, when the fact is settled: they can appear on a record, but they do not drift the way volatile commercial data does, which is the ULC admission rule that keeps lead times, stock, and pricing out of a point-in-time record. When you author a supersession, also set record_status to superseded. The guidance runs in that direction only: superseded_by stays optional on a superseded record, because a manufacturer may retire a product before naming its replacement, and because every record already carrying the superseded status predates the pointer and remains valid. Both facts speak for the whole record, the way record_status does: they describe the product across every orderable SKU the record’s applicability block covers, so neither is the place to record a fate that reaches only part of that range.

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: extracted with value_type: measured.
  • Values generated by a simulation tool carry provenance.method: optical_simulation with value_type: rated and a reference to the calibrating attestation.
  • Values produced by scaling a base test carry provenance.method: extended_photometry (or the more general scaled) with value_type: rated and 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 CPP order-code option and Remote Power, so it is an attestation with applicability.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: nominal and verification: requires_manufacturer_confirmation plus a contact_reference.

Records per cutsheet: 48 (Vode Nexa), with per-foot scaling covering every length variant inside each record.

Authoring exit-sign and emergency-class records

Product classes are an orthogonal dimension to the four manufacturer patterns above. A manufacturer pattern describes how a cutsheet’s SKUs map onto records; a product class describes which dataset the grader reads. The two compose: a product-class record is authored through whichever manufacturer pattern fits its cutsheet, then adds class-specific blocks. The class is selected by product_family.primary_category, and the two classes grade differently: an exit_sign swaps the architectural-photometry profile for the sign dataset (legend, illumination mode, battery, UL 924), while an emergency_luminaire keeps the normal profile minus luminaire efficacy and adds the emergency-battery gates. The worked examples are the three committed Cooper records, cooper-sure-lites-lpx7sd.ulc, cooper-sure-lites-es61src.ulc, and cooper-atlite-auxswhsd.ulc; all three are internally illuminated exit signs that also carry an emergency block. No committed record is a photoluminescent, self-luminous, or externally illuminated sign, nor a standalone emergency_luminaire; those modes and that class are gradable, but no committed example demonstrates them, so the field shapes below are drawn only from internally illuminated signs.

The exit_sign block. illumination_mode is the driver: it selects which sign rows the grader applies, so it is core-required for every sign (all three Cooper records carry internally_illuminated). legend_color is the other core sign field (red on all three). The universal standard-tier sign fields face_count and directional_indicator are populated on all three (single and ["none"]), and legend_height is the standard-tier letter-height figure (6 inches on the ES, 8 inches on the AUX). Mode-specific photometric fields gate by mode. At standard, sign-face luminance applies to photoluminescent and self-luminous signs, and face illuminance plus contrast ratio to externally illuminated signs, while an internally illuminated sign re-gates input_power_w instead (below). At full, the sign tier is anchored on a test-report-backed value: sign-face luminance with test_report provenance for every self-emitting mode (internally illuminated included), and face illuminance with test_report provenance for externally illuminated. illumination_technology is descriptive; the grader keys on illumination_mode.

The electrical re-gate. An internally illuminated sign re-gates electrical.input_power_w at standard, the running wattage a specifier compares. The ES publishes 3.8 W and the AUX 0.9 W; the LPX cutsheet publishes neither.

The emergency block. emergency_role and power_source are required inside the block. power_source decides the battery detail: an integral_battery sign gates the battery trio (battery_duration_min, battery_chemistry, self_test) at standard, and an ac_only sign carries none of it. The LPX and AUX are integral_battery and populate the trio (ni_cd, 90 minutes, self_diagnostic); the ES is ac_only and correctly omits it.

The UL 924 attestation. The emergency-lighting safety listing lives in product_family.shared_attestations as a ul_924 program token, never a boolean field, so listings have one home. Record its listing_number when the cutsheet prints one (the AUX carries E69466). Attaching the UL Certificate of Compliance as a compliance_documents source file and pointing the attestation’s source_document_ref at it is what raises the emergency achievement theme from claimed to documented: the AUX does this and is the corpus’s one documented case, while the LPX and ES list ul_924 without an attached certificate. UL 924 satisfies the core safety listing in every region, so a sign whose only listing is UL 924 still clears the core gate.

The workbook’s shared_attestations.listing_number column authors that listing number; see templates/workbook/README.md.

Which fields lift a sign from core to standard. legend_height (a sign field) and electrical.input_power_w (the internally-illuminated re-gate) are both standard-tier. The LPX illustrates the gap: it grades core, held below standard by exactly those two figures its cutsheet does not publish, one sign field and one electrical field. Frame the core-versus-standard gap as those two, not as letter geometry alone. For the full per-record grade catalogue, see examples/README.md; this section teaches the block shapes rather than what each record demonstrates.

Authoring achievement attestations

The Product Achievements axis is computed from the attestations a record already carries; authoring for it means recording the right attestation fields, not filling a separate block (the builder stamps index.achievements). Which program tokens feed which of the seven themes is the published, versioned map in the achievement-themes appendix of compliance-attestation.md; author against that map rather than restating it here.

Evidence separates claimed from documented. A theme reaches documented only when a qualifying attestation carries a source_document_ref, a file reference the schema guarantees to have a filename and a SHA-256 hash. Attach the evidence document (certificate, EPD, or declaration) as a compliance_documents source file and point the attestation’s source_document_ref at it; without an attached document the theme caps at claimed. The corpus shows both ends: the AUX exit sign’s ul_924 attestation carries an attached UL Certificate of Compliance and its emergency theme is documented, while the Selux pole and Vode pendant hold material_health (and, on Selux, dark_sky) at claimed, their Declare and DarkSky qualifications present but backed by no attached evidence document.

Withholding the evidence document. A manufacturer can document a theme while keeping the evidence document itself unpublished. The reference crosses the trust boundary; the bytes do not. Attach it exactly as the preceding paragraph describes, carrying the same filename and the same real SHA-256 hash of the withheld document in both the source_files entry and the attestation’s source_document_ref. Give it a neutral publication name (for example emergency-listing-evidence.pdf) rather than the internal document name. A default ulc validate run then reports one source-file/not-found-locally INFO at the source_files entry (/source_files/<i>/reference), the record validates cleanly, and the theme still reads documented, because documented records that an evidence document is attached, not that its bytes are reachable. Once the file does sit beside the record, that same default run byte-verifies it: a matching hash is silent, and a mismatch is an ERROR. ulc validate --verify-evidence extends the identical check to the attestation’s own source_document_ref pointer, where an absent document stays INFO and a hash mismatch is an ERROR. An attestation whose source_document_ref names a document that is not listed in source_files produces no finding on a default run; it is checked only under --verify-evidence. Choose the neutral filename deliberately: test-report filenames often name the laboratory, the product, the standard, and the test date, so a self-describing filename can disclose more than the manufacturer intends.

Expiry is record-relative. When an attestation carries a valid_until date and the record carries record_status_as_of, an attestation whose validity ended before the record’s as-of date cannot reach documented, though it still supports claimed. The comparison is always against the record’s own record_status_as_of, never the build clock, so a rebuilt record produces identical achievements; when either date is absent, no expiry check runs. To preview expiry against a wall-clock or chosen date without changing any computed state, run ulc validate --expiry (an advisory report, never a re-stamp).

The workbook’s valid_until column on attestations or shared_attestations authors this date; see templates/workbook/README.md.

Status disqualifiers. An attestation whose status is expired, withdrawn, or not_applicable contributes to no theme, as if absent. Every other status contributes normally.

Restricted-substances flag. RoHS, REACH, Prop 65, and similar restricted-substances programs populate the index.restricted_substances_declared sibling flag rather than a theme, because restricted-substances compliance is a legal floor, not a prestige achievement. The two Lumenpulse facade records and the Selux pole declare rohs there.

Quantitative sustainability payloads. An attestation can hang a sustainability_metric object off itself to carry the numbers behind a sustainability claim, so the figures inherit the attestation’s status, evidence, and expiry. Its fields: ceam_score (the TM66 CEAM design score, 0 to 4); embodied_carbon_kgco2e (a declared embodied-carbon figure in kg CO2e, recorded SI-native), which when present requires the companions embodied_carbon_scope and embodied_carbon_functional_unit so the number is unambiguous about its life-cycle boundary and unit basis; c2c_overall_level (a Cradle to Cradle overall rating); and method_variant (a regional or method sub-variant, such as a TM65 national annex). The optional issuing_authority attestation field names the specific operator when a program runs under more than one, as EPDs do. No committed example populates sustainability_metric, so the shape described here comes from the schema rather than a worked 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 (term, basis, scope, and the hashed conditions document), 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.

product_family.customization_openness

An optional array inside product_family naming the axes of the family the manufacturer states are open to requests beyond the published order-code menu. The statement is cutsheet-level ink (“other color temperatures on request”, a published modifications list), so it lives with the other family facts and is authored identically in every record of the family. It exists for the near-miss search: a specifier whose requirement falls outside the published menu can see the closest published record together with the axes its manufacturer states are open, instead of no match at all.

Author these entries on the workbook’s customization_openness sheet; see templates/workbook/README.md for the sheet rules.

Each entry carries axis (from the closed CustomizationAxis vocabulary), optionally the manufacturer’s own axis_label and on-request statement, and at least one of published_in_ref or contact_reference. Choosing between the two is mechanical: openness printed on a document cites that document (published_in_ref, a filename in the vocabulary of the record’s source_files entries and the family cutsheet, its typical carrier); openness the manufacturer states without printing it carries the routing (contact_reference); a sheet that prints both a statement and a routing carries both. axis_label is required when axis is other, and other may repeat only with distinct labels; every other axis appears at most once. On a listed axis, axis_label may carry the manufacturer’s own narrower name for the opening (a color option confined to cords, for example) without changing which token facets. An empty list is invalid: state at least one axis or omit the field. contact_reference names a role or department routing the manufacturer already makes public, never a named individual’s personal contact details. published_in_ref names a published document; a non-public document’s name can disclose more than the manufacturer intends, so the same neutral-naming caution that governs withheld evidence documents applies. The validator does not resolve published_in_ref: resolution against any one record’s file manifest is a courtesy rather than a contract, and nothing in ULC checks that the named document actually carries the statement; the citation records where the manufacturer publishes it.

The marker declares openness only. It never enumerates what the custom choices are, never implies price, lead time, minimum quantity, or that any specific request will be accepted, and never widens the record’s applicability: a configuration obtained through a request is outside the covered range and inherits none of the record’s measured values, derivations, or attestations. When the printed line itself enumerates values, cite the document and elide the list: a printed “custom lengths from two to twelve feet on request” (a constructed example) is authored as the entry’s axis plus "statement": "Custom lengths on request." with the sheet cited, or with statement omitted entirely; the enumerating clause never enters the record. ULC records openness only; a refusal has no encoding, because the printed convention is an on-request statement rather than a refusal. contact_reference carries the same meaning as the attestation verification type requires_manufacturer_confirmation: the manufacturer confirms each request per project.

How consumers should treat customization openness. An openness entry is author-asserted text: display it attributed, dated by the record’s record_status_as_of where the record carries one (a record without that date states openness with no stated date), and apply your own validation before rendering or linking a routing string. A consumer may facet on the axis tokens and return the record as a near match, visibly flagged as request-required, ranked below covered matches, and never merged into the covered set. Only listed axes may be rendered as open: an absent axis is unstated, neither open nor refused. A consumer may distinguish printed openness (published_in_ref) from stated-only openness, though the distinction is not a quality ranking. A consumer must never apply any measured value, derivation, computed grade, or attestation of the record to a custom variant; never compose or extend an order code for one; never parse a statement for values or paraphrase it as a commitment; and never infer price, lead time, minimum quantity, feasibility, or that any particular region, representative, or project size will honor a request. Tokens are deliberately coarse: an opening whose statement names cord length still facets as dimensions, and the statement carries the narrowing for human readers, so a token never licenses machine-readable openness on everything its bound surface spans. Sibling records of one family should carry identical lists; on conflicting siblings, treat the family’s statement as unresolved rather than merging.

Token bindings (authoritative). This table is the authoritative copy of the token-to-surface bindings; the taxonomy’s enum description is its summary. A token names the axis, never any value on it. The binding class states what the anchor is: a published-values surface lists or carries the published values the axis ranges over; a property block is the typed block where the axis’s data lives; a class vocabulary is a closed vocabulary typing the axis without listing a menu; a measured surface carries the tested value the axis varies; the escape hatch has no anchor, and axis_label carries the name.

TokenThe axis, in plain termsBound ULC surfacesBinding class
cctcorrelated color temperaturecolorimetry.nominal_cct_k; NominalCCTpublished-values surface
cricolor renderingconfiguration.tested_axes.cri_tier; CriTierpublished-values surface
finishany published color or finish axis, cordsets includedproduct_family.shared_mechanical.finish_color_optionspublished-values surface
opticdistribution and beamphotometry.distribution_type, photometry.beam_family; DistributionType, BeamFamilypublished-values surface
mountinghow the fixture attaches (canopy, pole, wall, recessed frame)product_family.mounting_types; MountingTypepublished-values surface
mechanicalstructure, housing, material (a custom housing, a non-standard material)product_family.shared_mechanicalproperty block
dimensionsphysical size (lengths, heights, overall dimensions)product_family.physical_dimensionsproperty block
driverthe driver itself (a non-standard driver, integral versus remote)electrical.control_gear_type; ControlGearTypeclass vocabulary (hardware, not signaling)
controlswhich control protocol the fixture speakselectrical.driver_protocol; DimmingProtocolclass vocabulary (signaling, not hardware)
light_sourcea different kind of source or tuning behavior, not a different fixed color temperature (which is cct)configuration.tested_axes.color_tunability; ColorTunabilityCapabilityclass vocabulary
outputlumen outputphotometry.total_luminous_flux_lm, photometry.led_module_luminous_flux_lmmeasured surface
emergencyfactory emergency-power optionsemergency.emergency_role and the emergency block; EmergencyRoleproperty block
otherthe manufacturer’s own labelled axisnone; axis_label carries the nameescape hatch

One trap the field names set: they cross. controls anchors to electrical.driver_protocol and driver anchors to electrical.control_gear_type; choose by what the printed line is about, never by the word inside the field name.

Authoring rules the table implies: pick the token whose bound surface carries the axis’s data; never relabel a listed axis through other, which defeats the cross-manufacturer facet the closed vocabulary exists for; file a genuinely unlisted axis as other with the manufacturer’s own label, which is also the demand signal that mints the next token. Cord colors file under finish; cord and cable lengths file under dimensions; a custom housing or a non-standard material files under mechanical. A different fixed color temperature files under cct; tunable-white, dim-to-warm, or color-mixing behavior files under light_source.

"product_family": {
  "customization_openness": [
    { "axis": "cct", "statement": "Additional color temperatures available on request.",
      "published_in_ref": "family-cutsheet.pdf" },
    { "axis": "mounting", "published_in_ref": "family-cutsheet.pdf",
      "contact_reference": "[email protected]" },
    { "axis": "other", "axis_label": "acoustic options",
      "contact_reference": "[email protected]" }
  ]
}

The boundary with the ordering menu. Openness and coverage are complementary statements. Any per-axis fact about the published menu belongs on the applicability block (covered_axes); any fact about the boundary of the menu belongs on customization_openness. The test is menu membership: if a specifier can order a value by writing a published catalog number, it is menu territory; if they cannot, it is openness territory. Some sheets publish a custom-placeholder order code (a specify-length token, a consult-factory code): a placeholder names openness, never a value and never a complete order. On an axis that does not affect photometry, the applicability block can carry the same openness as an authored covered-value placeholder, as the shipped corpus already legitimately does (the Vode pendant record covers an “any” arm-cable-length value under a photometrically-irrelevant rationale); the record then carries both statements of one openness, the covered value making it orderable and the marker making it searchable. On an axis where photometry depends on the value, no placeholder ever counts as covered, and the openness statement lives here alone. CustomizationAxis is customization-scoped: the ordering grammar’s vocabulary is separate, and neither reuses the other’s.

source_files

The top-level array naming every manufacturer-published file the record derives data from or anchors evidence to: the datasheet PDF, photometric files, installation instructions, test reports, compliance certificates. Each entry carries a file_type from the SourceFileType vocabulary and a reference with the filename and SHA-256 content hash. The hash is the record’s only assertion about the file: this document existed with these bytes when the record was published. ULC never asserts that a referenced document is current, only that it is the one the record was built from. A document can be listed here and hashed without being published; see “Withholding the evidence document” under Authoring achievement attestations above.

That makes source_files the right carrier for document-grade commercial references too. A manufacturer that publishes a lead-time or commercial-terms policy document can attach it as a commercial_terms_pdf entry, hashed like any other source file. The record then anchors the policy document without restating its contents; the live numbers (current lead times, stock, pricing) belong to transactional systems and never enter the record, because a hash-anchored copy of a number that changes weekly would be stale by design.

colorimetry.spectral_power_distribution

An optional uniformly sampled spectrum carries wavelength_start_nm, a positive wavelength_step_nm, at least two values, a unit (mW/nm, W/nm, or relative), value_type, source_kind, and optional provenance and measured_through_optics. The last wavelength follows from the start, step, and value count. Resample a curve read from a chart to the stated step and mark it digitized_chart; use laboratory_table for a tabulated test and exchange_file for a spectral exchange file. A tm27 source file may be pinned by hash. A package curve with measured_through_optics: false is not luminaire evidence. The record’s stated color values remain authoritative. From 1.15.0 a white-light record reaches Full only with this spectrum as data from a laboratory table or an exchange file, measured through the luminaire’s optics (value type measured or rated); a digitized chart or a package curve does not count.

Author spectrum samples in the workbook’s spectral_power_distribution sheet; see templates/workbook/README.md.

colorimetry.tm_30.rf_h_per_bin

The sixteen local color fidelity values of a TM-30 report, one entry per hue bin: an integer bin from 1 to 16 and rf_h, a provenanced number. A white-light record reaches Full only with this array. Author it in the workbook’s tm_30_hue_bins sheet; see templates/workbook/README.md.

attestations[].domestic_content

A domestic-content attestation may carry its US and foreign cost shares, the percentage threshold used to judge the claim, an optional effective date for that threshold, a basis (manufacturing_cost, component_cost, or other), and provenance. Author the US share, threshold, and basis together. When both shares are given, they should sum to 100 percent within source rounding. The payload is meaningful on baa, baba, and american_iron_and_steel claims; it records their cost evidence and does not decide the claim’s status.

Author the payload with the workbook’s domestic_content_* columns on attestations or shared_attestations; see templates/workbook/README.md.

lumen_maintenance_luminaire.additional_rated_claims

Keep the headline threshold in the existing manufacturer_rated_claim object. Its optional basis states tm_21_reported, tm_21_calculated, or manufacturer_stated when the cutsheet identifies the method. Put further thresholds in additional_rated_claims, each with a claim type, provenanced hours, and a required basis; a failure percentage is optional. The extra list is tracked as authored data and does not change the current grading or index projection.

Author the headline through the workbook’s lm_claim_type, lm_claimed_hours, and lm_claim_basis records columns and further claims on additional_rated_claims; see templates/workbook/README.md.

media

The optional top-level array naming the visual assets that document the product: product photographs, application photographs, and dimensional drawings. Each entry carries a role from the MediaRole vocabulary (product_photo, application_photo, dimensional_drawing, other), a media_type naming the format (image/jpeg, image/png, image/svg+xml, or image/webp), and a reference with the filename and SHA-256 content hash, exactly like a source file. Entries are pointers, never bytes: no image data enters the record. Optional descriptors cover presentation and rights: primary marks the preferred asset for its role (at most one per role; consumers seeing more than one fall back to document order), alt_text and caption describe the asset (for role other, the caption is what says what it is), language distinguishes localized variants of one drawing, width_px, height_px, and size_bytes describe the file, rights and credit carry the manufacturer’s usage-rights statement and attribution as free text, extracted_from_ref names the source_files entry an extracted asset came from, and configuration_refs scopes an asset to specific order codes when it depicts one configuration out of the record’s applicability range.

media is deliberately separate from source_files, and the boundary is what the entry is: a picture of the product belongs in media, and a document belongs in source_files. A source file is a document the record derives data from, and its file_type can serve as the provenance source for a traced value; a media asset documents appearance and geometry for presentation, carries no SourceFileType token, and can never back a measured value. The schema enforces the picture side mechanically: media admits image formats only, so a document format can never be declared there. A PDF drawing is filed in source_files like any other document, and an image file that is really a scanned document (a photographed certificate, for example) is also filed in source_files, under its document type. When authoring a dimensional drawing, prefer SVG, then PNG.

How consumers should treat media. Verification of a media asset is three-state. An asset whose bytes are present and match the declared hash is verified and renders normally. An asset that cannot be verified, because the file is absent locally or a URL copy has not been fetched and hashed, is unverified: a consumer may render an unverified product_photo or application_photo with a visible unverified marking, and must not render an unverified dimensional_drawing, because wrong bytes on a drawing are a dimensional data hazard rather than a cosmetic one. A consumer that fetches an asset from its url verifies it the same way: hash the fetched bytes and compare before trusting them. The third state is absence: the record declares no asset for a role, which is different from a declared asset whose file is missing. A default ulc validate run byte-verifies every media entry whose file sits beside the record: a matching hash is silent, a mismatch is an ERROR, an absent file is an INFO, and an unreadable file is a WARNING, the same semantics the walk applies at the record’s other default-verified reference sites: the source files, the family cutsheet, the emergency photometry reference, and the family warranty-conditions document.

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 axes
  • fixed_axes: order-code segments that must match (for example Optics=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

ordering

The optional top-level block for products sold by length or assembled from sections: tape and ribbon products cut to an ordered length, and sectional linears joined into rows. It carries the grammar of how the product is ordered, as printed on the family’s own ordering documentation: order_unit says what the product is sold as (cut_to_length_run for continuous products ordered at a specified length, fixed_length_section for discrete sections joined on site), order_increment the granularity at which lengths may be ordered, cut_increment the module at which the product may be cut, and max_continuous_run the maximum electrically continuous run. Each length uses the record’s standard dual-unit form (both mm and in, SI authoritative). A member lives in exactly one place: directly on the block when one value holds for everything the record covers, or in declared_by_configuration rows when the manufacturer publishes it per configuration, each row naming the configurations it covers by order codes in the vocabulary the record already speaks: a full catalog number, or a single option value as the applicability axes publish them, whichever of the two the manufacturer’s own documentation uses. Codes are never matched as substrings. If an option value appears in more than one of the record’s axes it is ambiguous; use full catalog numbers there instead. A row must carry at least one value beside the configurations it names. For a fixed_length_section family the enumerated set of section lengths lives where its order codes already live, on the applicability block’s length axis, and order_increment is left absent.

"ordering": {
  "order_unit": "cut_to_length_run",
  "max_continuous_run": { "mm": 12000, "in": 472.44 },
  "declared_by_configuration": [
    { "configuration_refs": ["A2"], "cut_increment": { "mm": 40, "in": 1.57 } }
  ]
}

The block carries the grammar of ordering and never the numbers of a transaction. Lead times, stock levels, prices, and minimum-order quantities change with the market and belong to the sales channel; a record that froze them under a content hash would preserve them stale. If the ordering documentation prints a constraint’s existence with no value (“cut points on request, consult factory”), the record simply omits that member: for a consumer, an absent ordering value means the manufacturer has not published it, never that the product is unconstrained. A record for a product that is neither cut to length nor joined into rows omits the block entirely.

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 file
  • validated: checked against a second source
  • optical_simulation: generated by an optical design tool calibrated against a base LM-79. Carries provenance.simulation_tool and provenance.base_attestation_ref, and pairs with value_type: rated.
  • extended_photometry: derived from a base LM-79 attestation by manufacturer-applied scaling rules. Carries provenance.base_attestation_ref and provenance.extension_method, and pairs with value_type: rated.
  • scaled: general closed-form mathematical scaling (CCT multiplier, per-foot scaling, wattage-tier scaling). Pairs with value_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": "[email protected]"
  }
}

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, the computed Product Achievements axis (achievements) and its restricted-substances flag (restricted_substances_declared), and 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 block
  • builder_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 baseline with a sibling array of declared values across the covered axis.

  • The baseline field (for example photometry.total_luminous_flux_lm) is a single ProvenancedNumber. A measured baseline holds the tested value with value_type: measured and provenance.attestation_ref pointing at the record’s LM-79 anchor (the attestation that total_luminous_flux_lm__attestation_ref names, or else the LM-79 attestation a measured total_luminous_flux_lm would auto-link to).

  • A sibling array (declared_by_cct[], or declared_by_length[] for the length axis) carries one rated entry per covered value, each with its derivation method and value_type: rated.

  • For declared_by_cct without an LM-79 anchor (no total_luminous_flux_lm__attestation_ref, and no LM-79 attestation a measured total_luminous_flux_lm would auto-link to), the baseline keeps its own provenance (for example rated from the datasheet), the declared entry at the baseline CCT repeats it, and every other declared entry carries method: scaled and extension_method: cct_multiplier with no base_attestation_ref. declared_by_length rows (generated or verbatim) still need the LM-79 anchor.

An anchored 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_ref pointing 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.
  • Supersession pointers: a superseded record names its successor in the top-level superseded_by: by successor record_id (optionally pinned to an exact revision with record_sha256), by successor catalog_number (order code) or catalog_model (family model), or several of these together. The pointer is set once and never retargeted.

Validation implications

The reference validator uses the patterns and primitives above as follows:

  • Conformance level and achievements are computed, not declared. ulc validate runs JSON Schema validation, index-builder parity, and source-file hash verification, then reports the completeness grade the builder computed (stamped as index.conformance_level) with its two roadmaps, the tier roadmap to the next grade and the non-gating enrichment roadmap, alongside the Product Achievements axis (index.achievements). A hand-edited index value fails the build-parity check like any other field. The per-grade requirement tables and the two-axis model are in the conformance rubric; a conformance level is a data-completeness grade, never a safety certification, and it is never a pass/fail gate.