@pagefront/lint-commerce
v0.12.1
Published
Pagefront commerce linter: the commerce rule catalogue and CLI, on the @pagefront/lint-core engine.
Maintainers
Readme
@pagefront/lint-commerce
The Pagefront commerce linter: the commerce rule catalogue
(commerce/spec/rules.md, normative) and the
pagefront-lint-commerce CLI, on the
@pagefront/lint-core engine. This is the
reference implementation of Layer 2 of the validation stack defined in
core/spec/linter.md.
CLI
pagefront-lint-commerce <sheet-url-or-path> [options]
--format <json|text|html|github> output format (default: json)
--fail-on <error|warning> lowest severity that fails the exit code
--checked-at <iso8601> inject the report timestamp (reproducible output)The CLI validates Layer 1 (JSON Schema) first; a failing Sheet gets its
schema errors on stderr and no report. Exit codes: 0 nothing
blocking, 1 Layer 1 failure or blocking findings, 2 operational
failure. Both commerce page types are handled: product sheets and
Commerce Organization Sheets each validate against their own schema and
lint with their own catalogue, dispatched by sheet type.
Catalogue
Catalogue 0.16.1, supporting commerce releases 0.4.0 (Product Sheet spec
0.10.0, Organization Sheet spec 0.4.0) and 0.3.0, and the legacy draft line
(format 0.9): 58 product rules (PDP-E-001 to PDP-E-018, PDP-W-001 to
PDP-W-039, PDP-I-001) and 10 organization rules (ORG-E-001 to
ORG-E-004, ORG-W-001 to ORG-W-006), each defined in
rules.md
with its target, check, message and remediation. Every report carries the
catalogue_version it was produced with.
Added in 0.5.0 (catalogue 0.8.0), from the product spec's "Tax inclusion" section:
PDP-W-028— an Offer states a price but nopriceSpecificationentry carries a booleanvalueAddedTaxIncludedPDP-W-029— an Offer states a price but declares neitherareaServednoreligibleRegionPDP-E-004— apriceSpecificationentry mirroring the Offer's price states a different amount in the same currency
The two warnings exempt offers carrying pagefront:priceOnRequest.
Added in 0.6.0 (catalogue 0.9.0), from the product spec's "Warranty" section:
PDP-W-030— awarrantyon an Offer, or apagefront:manufacturerWarrantyon the Product, carries neitherdurationOfWarrantynordescription
Added in 0.7.0 (catalogue 0.10.0), from the organization spec's "Linking contract" section:
PDP-E-005—pagefront:sheetUrlon an Offer'sselleris on a host other than the product sheet'sPDP-E-006—pagefront:sheetUrlonbrandis on a host other than that of the brand's@idPDP-W-031— an Offer'ssellerhas an@idbut nopagefront:sheetUrl, and the Offer states no return or shipping terms of its own
Added in 0.8.0 (catalogue 0.11.0), from the product spec's
"Configurator pricing and price bands"
section. Configurator pricing moved from pagefront:configurator into the
offer's pagefront:configuratorPricing; a sheet carrying prices inside the
configurator now fails Layer 1.
PDP-E-001now also covers the pricing block'sconfiguratorreference, which must equal the@idof the Sheet'spagefront:configuratorPDP-E-007—baseConfigurationomits a priced dimension, or names a dimension or option the configurator does not declarePDP-E-008— the modifier of a base option is not zeroPDP-E-009— a modifier names an unknown dimension or option, or a dimension declaredaffectsPrice: falsePDP-E-010— an Offer'spricediffers from its pricing block'sbasePricePDP-E-011— an AggregateOffer'slowPriceorhighPricediffers from the band computed from a complete pricing blockPDP-E-012— more than one offer prices the same configuratorPDP-W-032— a priced option has no modifier andpricingis notpartialPDP-W-033— a configurator has priced dimensions and no offer prices itPDP-W-034— an offer states a price beside a configurator and no offer carries a pricing blockPDP-W-035—basePricelooks like a component price (heuristic)
Amounts are summed and compared as exact decimals.
Added in 0.9.0 (catalogue 0.14.0), from the same section and from
"Size guidance":
option values are identifiers, free-text dimensions, order-time services
kept out of options, notes as facts, and size advice in the sizing
dimension's system. From 0.9.0 every amount is a JSON number or a plain
decimal string, one carrier per Sheet, and amounts are compared numerically
whatever the carrier; 0.8.0 validated pricing-block amounts as decimal
strings only. An Offer's price, a variant's price and a promotion's
discountAmount no longer accept a string that is not a plain decimal, and
prices, bounds and base prices no longer accept a negative amount.
PDP-E-013— a dimension declaresinputType: "text"and enumerates optionsPDP-W-036— an option value looks like a display label (whitespace, an uppercase letter, or a unit suffix); sizing dimensions are exemptPDP-W-037— an option in a sizing dimension (systemofUK,EU,USorJP) is not written as a size in that systemPDP-W-032,PDP-E-009and the computed band ofPDP-E-011now cover the"*"modifier of a priced text dimensionPDP-E-014— a size adjustment'ssizeSystemis another system than the configurator's sizing dimensionPDP-E-015— the Sheet's amounts mix JSON numbers and stringsPDP-W-038— a configurator dimension'snotereads as page copy (English Sheets:you,your,select,order,contact)PDP-W-039— size advice carries adescriptionand nosizeAdjustmentbeside a sizing dimension
Added in 0.10.0 (catalogue 0.15.0), with commerce release 0.3.0 and the versioning scheme that replaces format versions:
- Sheets carry
releasein place offormat_version, and$schemanames the sheet-spec version (product/v0.9.0.json,organization/v0.3.0.json). Sheets carryingformat_versionare the legacy draft line and keep validating against the bundled legacy schemas. PDP-E-002becomes "Release / schema mismatch":$schemamust name the sheet-spec version the release manifest maps the declaredreleaseto. On legacy sheets it comparesformat_versionwith$schema, as before.ORG-E-003is the same check for Organization Sheets.PDP-E-016andORG-E-002— a sheet carries bothformat_versionandrelease- Breaking: the report carries
release(the value the sheet declares; a legacy sheet'sformat_versionvalue) andsheet_spec_version(parsed from$schema) in place ofspec_version. Requires@pagefront/lint-core0.2.0. - New exports:
detectSheetLine,RELEASE_MANIFEST,KNOWN_RELEASES,sheetSpecVersionFor.
Changed in 0.11.0 (catalogue 0.15.0, unchanged):
- Layer 1 validates a sheet against the published schema its
$schemanames. The package bundles every file in the repository's frozen schema store (commerce/schemas/) at build time, with the release manifest; nothing is embedded by hand. - A
$schemanaming no bundled schema fails Layer 1 with one error. In 0.10.0 the schema was chosen from the envelope field, so a release sheet pointing at another version passed Layer 1 and firedPDP-E-002; it now fails Layer 1 against the schema it names. - New exports:
CURRENT_RELEASE,schemaUrlFor,BUNDLED_SCHEMA_IDS.
Added in 0.12.0 (catalogue 0.16.0), with commerce release 0.4.0. Requires
@pagefront/lint-core 0.3.0.
- A sheet is judged by the release it declares. Layer 1 validates it
against the schema its
releasebundles for its type (release 0.4.0:product/v0.10.0.json,organization/v0.4.0.json, which require$schema; release 0.3.0: the 0.3.0 schemas). Legacy sheets keep resolving through$schema. - Rules are gated by release. Every rule has an
appliesFromrelease and the engine skips it on a sheet declaring a lower one. All earlier rules apply from 0.3.0 and to legacy sheets. PDP-E-002andORG-E-003compare a present$schemawith the URL of the schema the release bundles, and are quiet when$schemais absent.PDP-E-017(applies from release 0.4.0) — an exclusion, or an adjustment'swhen, names a dimension the configurator does not declare or an option the dimension does not list. Values are compared strictly:1is not"1".PDP-E-018andORG-E-004— the sheet declares areleasethe release manifest does not list. At Layer 1 this is a single error at/release.
Changed in 0.12.1 (catalogue 0.16.1):
PDP-E-017adds a type hint. When a reference matches no option but a declared option has the same text under another JSON type, the message names both types, and the finding'svaluescarryreference_type,declared_optionanddeclared_type. Comparison is unchanged and the finding stays an error.
API
The public surface — everything importable from the package root, and nothing else (the bundled schemas and the individual rule modules are internal):
validateAndLint(input: unknown, options?: LintOptions)— the CLI's own pipeline as a function: Layer 1 against the published schema the document's declared release bundles for its page type, then Layer 2 against that page type's catalogue (seedetectSheetType), with the rules that apply from that release. Returns{ layer: 1, schemaErrors: SchemaError[] }or{ layer: 2, report: Report }. The package bundles every published schema, on both lines. Areleasethe manifest does not list fails Layer 1 with a single error at/release(keywordrelease). A$schemathat disagrees with the release is a Layer 2 finding. Legacy sheets (format_version) are validated against the schema their$schemanames, or the legacy schema when it is absent. Layer 1 runs validators compiled ahead of time at package build, so the pipeline needs no runtime code generation and runs under Cloudflare Workers and strict Content Security Policies.lintSheet(sheet: Sheet, options?: LintOptions): Report— Layer 2 only, for input you have already schema-validated. Dispatches to the page type's catalogue the same way.validateOrganizationSheet(input: unknown)— Layer 1 only, against the Commerce Organization Sheet schema the document names. Returns{ valid: true }or{ valid: false, schemaErrors: SchemaError[] }.detectSheetType(sheet: Sheet): SheetType— the page-type dispatch:"organization"when the envelope$schemapath contains/spec/commerce/organization/or/schemas/organization/(a/spec/commerce/product/or/schemas/product/path likewise wins for"product"), falling back to a data block whose@typeisOrganization/OnlineStore/OnlineBusinesswithpagefront:commerceRolepresent;"product"otherwise.commerceCatalogue,organizationCatalogue— the per-page-type catalogue objects (read-only), for direct engine use or introspection. Product rules never run on organization sheets and vice versa.- Types:
SchemaError,ValidateAndLintResult,ValidateOrganizationResult,SheetType, and the core types the signatures mention —Report,Finding,Summary,Severity,LintOptions,Sheet.
CI script
import { readFile } from "node:fs/promises";
import { validateAndLint } from "@pagefront/lint-commerce";
const document = JSON.parse(await readFile(process.argv[2], "utf8"));
const result = validateAndLint(document);
if (result.layer === 1) {
for (const e of result.schemaErrors) {
console.error(`${e.instancePath === "" ? "(root)" : e.instancePath}: ${e.message}`);
}
process.exit(1);
}
process.exit(result.report.summary.errors > 0 ? 1 : 0);Pipeline embed
A production pipeline with its own validation layer calls the same function and folds the union into its own result shape — the schema compiles once at module load, so per-document calls are cheap:
import { validateAndLint } from "@pagefront/lint-commerce";
export function validateSheet(document) {
const result = validateAndLint(document);
return result.layer === 1
? { ok: false, schemaErrors: result.schemaErrors }
: { ok: result.report.summary.errors === 0, report: result.report };
}Rendering stored reports
The renderers are pure functions over the canonical Report JSON — they
live in @pagefront/lint-core and work on any report, including one
you stored earlier:
import { readFile } from "node:fs/promises";
import { renderHtml } from "@pagefront/lint-core";
const report = JSON.parse(await readFile("report.json", "utf8"));
await writeFile("report.html", renderHtml(report));Versioning
This package has its own semver, separate from the rule catalogue's
(catalogue_version in every report) and from the commerce release and
sheet-spec versions a sheet declares (release and sheet_spec_version
in every report). The scheme is in
releases.md.
Pre-1.0: minor versions may break the API. The surface listed above is
what versioning tracks — imports that reach past it (dist/ paths,
rule modules) are internal and may change without notice in any
release.
