@pagefront/lint-commerce 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +49 -16
  2. package/dist/catalogue.js +4 -2
  3. package/dist/generated/release-manifest.d.ts +30 -0
  4. package/dist/generated/release-manifest.js +36 -0
  5. package/dist/generated/schema-ids.d.ts +1 -0
  6. package/dist/generated/schema-ids.js +9 -0
  7. package/dist/generated/schema-properties.d.ts +1 -0
  8. package/dist/{schema-properties.js → generated/schema-properties.js} +2 -10
  9. package/dist/generated/schemas.d.ts +1 -0
  10. package/dist/generated/schemas.js +7326 -0
  11. package/dist/index.d.ts +8 -5
  12. package/dist/index.js +8 -5
  13. package/dist/organization-catalogue.js +3 -2
  14. package/dist/releases.d.ts +12 -26
  15. package/dist/releases.js +16 -30
  16. package/dist/rules/org-e-001.js +2 -0
  17. package/dist/rules/org-e-002.js +2 -0
  18. package/dist/rules/org-e-003.js +2 -0
  19. package/dist/rules/org-e-004.d.ts +11 -0
  20. package/dist/rules/org-e-004.js +18 -0
  21. package/dist/rules/org-w-001.js +2 -0
  22. package/dist/rules/org-w-002.js +2 -0
  23. package/dist/rules/org-w-003.js +2 -0
  24. package/dist/rules/org-w-004.js +2 -0
  25. package/dist/rules/org-w-005.js +2 -0
  26. package/dist/rules/org-w-006.js +2 -0
  27. package/dist/rules/pdp-e-001.js +2 -0
  28. package/dist/rules/pdp-e-002.js +2 -0
  29. package/dist/rules/pdp-e-003.js +2 -0
  30. package/dist/rules/pdp-e-004.js +2 -0
  31. package/dist/rules/pdp-e-005.js +2 -0
  32. package/dist/rules/pdp-e-006.js +2 -0
  33. package/dist/rules/pdp-e-007.js +2 -0
  34. package/dist/rules/pdp-e-008.js +2 -0
  35. package/dist/rules/pdp-e-009.js +2 -0
  36. package/dist/rules/pdp-e-010.js +2 -0
  37. package/dist/rules/pdp-e-011.js +2 -0
  38. package/dist/rules/pdp-e-012.js +2 -0
  39. package/dist/rules/pdp-e-013.js +2 -0
  40. package/dist/rules/pdp-e-014.js +2 -0
  41. package/dist/rules/pdp-e-015.js +2 -0
  42. package/dist/rules/pdp-e-016.js +2 -0
  43. package/dist/rules/pdp-e-017.d.ts +2 -0
  44. package/dist/rules/pdp-e-017.js +80 -0
  45. package/dist/rules/pdp-e-018.d.ts +11 -0
  46. package/dist/rules/pdp-e-018.js +18 -0
  47. package/dist/rules/pdp-i-001.js +5 -2
  48. package/dist/rules/pdp-w-001.js +2 -0
  49. package/dist/rules/pdp-w-002.js +2 -0
  50. package/dist/rules/pdp-w-003.js +2 -0
  51. package/dist/rules/pdp-w-004.js +2 -0
  52. package/dist/rules/pdp-w-005.js +2 -0
  53. package/dist/rules/pdp-w-006.js +2 -0
  54. package/dist/rules/pdp-w-007.js +2 -0
  55. package/dist/rules/pdp-w-008.js +2 -0
  56. package/dist/rules/pdp-w-009.js +2 -0
  57. package/dist/rules/pdp-w-010.js +2 -0
  58. package/dist/rules/pdp-w-011.js +2 -0
  59. package/dist/rules/pdp-w-012.js +2 -0
  60. package/dist/rules/pdp-w-013.js +2 -0
  61. package/dist/rules/pdp-w-014.js +2 -0
  62. package/dist/rules/pdp-w-015.js +2 -0
  63. package/dist/rules/pdp-w-016.js +2 -0
  64. package/dist/rules/pdp-w-017.js +2 -0
  65. package/dist/rules/pdp-w-018.js +2 -0
  66. package/dist/rules/pdp-w-019.js +2 -0
  67. package/dist/rules/pdp-w-020.js +2 -0
  68. package/dist/rules/pdp-w-021.js +2 -0
  69. package/dist/rules/pdp-w-022.js +2 -0
  70. package/dist/rules/pdp-w-023.js +2 -0
  71. package/dist/rules/pdp-w-024.js +2 -0
  72. package/dist/rules/pdp-w-025.js +2 -0
  73. package/dist/rules/pdp-w-026.js +2 -0
  74. package/dist/rules/pdp-w-027.js +2 -0
  75. package/dist/rules/pdp-w-028.js +2 -0
  76. package/dist/rules/pdp-w-029.js +2 -0
  77. package/dist/rules/pdp-w-030.js +2 -0
  78. package/dist/rules/pdp-w-031.js +2 -0
  79. package/dist/rules/pdp-w-032.js +2 -0
  80. package/dist/rules/pdp-w-033.js +2 -0
  81. package/dist/rules/pdp-w-034.js +2 -0
  82. package/dist/rules/pdp-w-035.js +2 -0
  83. package/dist/rules/pdp-w-036.js +2 -0
  84. package/dist/rules/pdp-w-037.js +2 -0
  85. package/dist/rules/pdp-w-038.js +2 -0
  86. package/dist/rules/pdp-w-039.js +2 -0
  87. package/dist/rules/release-schema.d.ts +18 -7
  88. package/dist/rules/release-schema.js +55 -15
  89. package/dist/schema-registry.d.ts +42 -0
  90. package/dist/schema-registry.js +52 -0
  91. package/dist/schema-validators.d.ts +7 -8
  92. package/dist/schema-validators.js +18 -4
  93. package/dist/validate-and-lint.d.ts +28 -21
  94. package/dist/validate-and-lint.js +60 -36
  95. package/dist/validate-organization.d.ts +5 -5
  96. package/dist/validate-organization.js +8 -18
  97. package/dist/version.d.ts +1 -1
  98. package/dist/version.js +1 -1
  99. package/package.json +3 -3
  100. package/dist/legacy-schemas.d.ts +0 -9
  101. package/dist/legacy-schemas.js +0 -2446
  102. package/dist/organization-schema.d.ts +0 -10
  103. package/dist/organization-schema.js +0 -443
  104. package/dist/product-schema.d.ts +0 -9
  105. package/dist/product-schema.js +0 -2015
  106. package/dist/schema-properties.d.ts +0 -10
@@ -27,5 +27,7 @@ export const pdpW006 = {
27
27
  messageTemplate: "Certification `{name}` at `{path}` has no scope declared.",
28
28
  remediation: "Add a `PropertyValue` to `additionalProperty` with name `scope` and a value drawn from the conventional vocabulary (`yarn`, `fabric`, `finished_product`, `diamond`, `gemstone`, etc.).",
29
29
  introduced: "v0.6",
30
+ appliesFrom: "0.3.0",
31
+ appliesToLegacy: true,
30
32
  specReference: "vocabulary.md#certification-scope",
31
33
  };
@@ -25,5 +25,7 @@ export const pdpW007 = {
25
25
  messageTemplate: "ProductGroup at `{path}` has only one variant.",
26
26
  remediation: "Either add the missing variants to `hasVariant`, or change `@type` to `Product` and model the single variant directly. The `productGroupID` field should be replaced with `productID` or `sku` if switching to Product.",
27
27
  introduced: "v0.6",
28
+ appliesFrom: "0.3.0",
29
+ appliesToLegacy: true,
28
30
  specReference: "product.md#configurator-vs-productgroup-guidance",
29
31
  };
@@ -39,5 +39,7 @@ export const pdpW008 = {
39
39
  messageTemplate: "Sheet `expires_at` is `{expires_at}`, which is {reason}.",
40
40
  remediation: "Set `expires_at` to a sensible window after `generated_at`. For static product catalogs, hours to days; for sheets with live pricing or inventory signals, minutes.",
41
41
  introduced: "v0.6",
42
+ appliesFrom: "0.3.0",
43
+ appliesToLegacy: true,
42
44
  specReference: "product.md#the-envelope",
43
45
  };
@@ -25,5 +25,7 @@ export const pdpW009 = {
25
25
  messageTemplate: "ProductGroup at `{path}` has no variants.",
26
26
  remediation: "Add variant entries to `hasVariant`. If the product genuinely has no variants, change `@type` to `Product`.",
27
27
  introduced: "v0.6",
28
+ appliesFrom: "0.3.0",
29
+ appliesToLegacy: true,
28
30
  specReference: "product.md#configurator-vs-productgroup-guidance",
29
31
  };
@@ -81,5 +81,7 @@ export const pdpW010 = {
81
81
  messageTemplate: "Dimension `{dimension}` is declared in both `pagefront:configurator` at `{configurator_path}` and as a variant property at `{variant_path}`.",
82
82
  remediation: "Choose one representation. Use the configurator for combinatorial spaces, made-to-order products, or continuous dimensions. Use `ProductGroup` + `hasVariant` for discrete SKU families that ship from inventory.",
83
83
  introduced: "v0.6",
84
+ appliesFrom: "0.3.0",
85
+ appliesToLegacy: true,
84
86
  specReference: "product.md#configurator-vs-productgroup-guidance",
85
87
  };
@@ -40,5 +40,7 @@ export const pdpW011 = {
40
40
  messageTemplate: "Sheet at `{canonical_url}` appears to be part of a multi-language site but does not declare `pagefront:availableLanguages`.",
41
41
  remediation: "Add a `pagefront:availableLanguages` array listing the URLs of sibling sheets in other languages, with their BCP 47 language tags.",
42
42
  introduced: "v0.6",
43
+ appliesFrom: "0.3.0",
44
+ appliesToLegacy: true,
43
45
  specReference: "product.md#internationalization",
44
46
  };
@@ -47,5 +47,7 @@ export const pdpW012 = {
47
47
  messageTemplate: "Video at `{path}` with role `{role}` has no caption or transcript.",
48
48
  remediation: "Add a `caption` field with a 1–3 sentence summary of the video's content, or a `transcript` field with the full spoken content. For overview and tutorial videos, at least one is strongly recommended.",
49
49
  introduced: "v0.6",
50
+ appliesFrom: "0.3.0",
51
+ appliesToLegacy: true,
50
52
  specReference: "product.md#accessibility",
51
53
  };
@@ -21,5 +21,7 @@ export const pdpW013 = {
21
21
  messageTemplate: "Legacy HTTP Schema.org URI `{value}` at `{path}`.",
22
22
  remediation: "Change `http://schema.org/...` to `https://schema.org/...`. The value is otherwise unchanged.",
23
23
  introduced: "v0.6",
24
+ appliesFrom: "0.3.0",
25
+ appliesToLegacy: true,
24
26
  specReference: "product.md#offers",
25
27
  };
@@ -65,5 +65,7 @@ export const pdpW014 = {
65
65
  messageTemplate: "ProductGroup at `{path}` has `{count}` variants with no declared per-variant `url`s. If each variant is genuinely reachable at its own address — its own page or a variant-selecting parameter — declare it in the variant's `url` field; a fragment `@id` identifies the variant but does not promise it is addressable.",
66
66
  remediation: "For each variant entry in `hasVariant`, add a `url` field with the variant's addressable URL — its own page (case 3) or the shared page with its variant-selecting query parameter (case 2). If all variants share the exact same URL with no parameter-based differentiation (case 1), the per-variant `url` field can be omitted.",
67
67
  introduced: "v0.6",
68
+ appliesFrom: "0.3.0",
69
+ appliesToLegacy: true,
68
70
  specReference: "product.md#per-variant-urls",
69
71
  };
@@ -29,5 +29,7 @@ export const pdpW015 = {
29
29
  messageTemplate: "Price-on-request at `{path}` declares method `{method}` but no contact channel (`contactUrl`, `contactEmail`, or `contactPhone`).",
30
30
  remediation: "Add the contact channel matching the declared method (e.g., `contactUrl` for `authorized_retailer` and `dealer_network`, `contactEmail` for `email`, `contactPhone` for `phone`, `contactUrl` for `quote_form`). If no such channel exists, change `method` to `unspecified`.",
31
31
  introduced: "v0.6",
32
+ appliesFrom: "0.3.0",
33
+ appliesToLegacy: true,
32
34
  specReference: "product.md#price-on-request-offers",
33
35
  };
@@ -26,5 +26,7 @@ export const pdpW016 = {
26
26
  messageTemplate: "Offer at `{path}` has commercial structure (availability, priceCurrency) but no price declaration (no `price`, no `priceSpecification`, no `pagefront:priceOnRequest`).",
27
27
  remediation: "Add `price` (and confirm `priceCurrency`) if the price was missed during extraction. If the merchant intentionally publishes no price, declare `pagefront:priceOnRequest` with an appropriate `method` and contact channel.",
28
28
  introduced: "v0.6",
29
+ appliesFrom: "0.3.0",
30
+ appliesToLegacy: true,
29
31
  specReference: "product.md#price-on-request-offers",
30
32
  };
@@ -51,5 +51,7 @@ export const pdpW017 = {
51
51
  messageTemplate: 'Language tag at `{path}` uses non-canonical case: `"{value}"`. Canonical form is `"{canonical}"`.',
52
52
  remediation: "Normalize the tag to canonical BCP 47 case. The semantic meaning is unchanged; only the case differs.",
53
53
  introduced: "v0.6",
54
+ appliesFrom: "0.3.0",
55
+ appliesToLegacy: true,
54
56
  specReference: "product.md#upstream-equivalent-forms",
55
57
  };
@@ -28,5 +28,7 @@ export const pdpW018 = {
28
28
  messageTemplate: "Variant at `{path}` has no `url`, but other variants in the same ProductGroup declare one.",
29
29
  remediation: "Add a `url` to this variant matching its distinct addressable page state, consistent with its siblings. If this variant genuinely shares the exact same URL as the others with no parameter-based differentiation (case 1 in the spec), the per-variant `url` should instead be omitted from *all* entries so presence is uniform.",
30
30
  introduced: "v0.7",
31
+ appliesFrom: "0.3.0",
32
+ appliesToLegacy: true,
31
33
  specReference: "product.md#per-variant-urls",
32
34
  };
@@ -45,5 +45,7 @@ export const pdpW019 = {
45
45
  messageTemplate: "`pagefront:relatedProduct` entry `{name}` at `{path}` carries an identifier field that is present but empty or non-dereferenceable, so it has no usable structured identifier.",
46
46
  remediation: "Supply a real, dereferenceable identifier — a URL-shaped `@id`, or a non-empty `sku`, `gtin`, `gtin13`, or `mpn`. If no structured identifier is available for this reference, it does not meet `pagefront:relatedProduct`'s requirement: move it to Schema.org's native `isRelatedTo` (which carries no identifier requirement) and drop the `relationship` typing.",
47
47
  introduced: "v0.7",
48
+ appliesFrom: "0.3.0",
49
+ appliesToLegacy: true,
48
50
  specReference: "product.md#the-structured-identifier-requirement",
49
51
  };
@@ -60,5 +60,7 @@ export const pdpW020 = {
60
60
  messageTemplate: "Relationship reference `{name}` at `{path}` in `{field}` {problem}.",
61
61
  remediation: "Add at least one identifying field — `@id`, `url`, `name`, `sku`, `gtin`, `gtin13`, or `mpn` — so the reference is dereferenceable, displayable, or matchable. If the `@id` was meant to name an entity in this Sheet, correct it to match a declared `@id` or add that entity; if it was meant to point at another product, use the target's full URL.",
62
62
  introduced: "v0.7",
63
+ appliesFrom: "0.3.0",
64
+ appliesToLegacy: true,
63
65
  specReference: "product.md#five-patterns-for-product-to-product-relationships",
64
66
  };
@@ -49,5 +49,7 @@ export const pdpW021 = {
49
49
  messageTemplate: "Variant link `{name}` at `{path}` appears to reference a separate Sheet by `@id` but omits the recommended `sheet_url`; consumers must reach the target Sheet via `url` + `describedby`.",
50
50
  remediation: "Add `sheet_url` with the target's Sheet URL — the variant's Sheet URL on a parent `hasVariant` entry, or the parent's Sheet URL on a variant's `isVariantOf`. If you do not emit a separate Sheet for the target (the family is modeled inline in one ProductGroup Sheet), no cross-Sheet link exists and `sheet_url` does not apply.",
51
51
  introduced: "v0.7",
52
+ appliesFrom: "0.3.0",
53
+ appliesToLegacy: true,
52
54
  specReference: "product.md#multi-sheet-variant-families",
53
55
  };
@@ -21,5 +21,7 @@ export const pdpW022 = {
21
21
  messageTemplate: "`pagefront:productHighlights[{index}]` is empty or whitespace-only; highlights must carry the publisher's selling-point text.",
22
22
  remediation: "Populate the entry with the highlight text from the merchant's page, or remove the entry. If the pipeline cannot recover the text, drop the entry rather than emitting a placeholder — a shorter honest list beats a ranked list with blanks.",
23
23
  introduced: "v0.8",
24
+ appliesFrom: "0.3.0",
25
+ appliesToLegacy: true,
24
26
  specReference: "product.md#product-highlights",
25
27
  };
@@ -35,5 +35,7 @@ export const pdpW023 = {
35
35
  messageTemplate: "Recall safety notice at `{path}` coexists with offer at `{offerPath}` declaring `InStock`; the recall overrides commercial signals and the offer data likely needs updating.",
36
36
  remediation: "Update the offer's `availability` to reflect the corrective measure (typically `Discontinued` or `OutOfStock`). If the recall has been resolved and the product legitimately relisted, remove the stale `pagefront:safetyNotice` entry instead.",
37
37
  introduced: "v0.8",
38
+ appliesFrom: "0.3.0",
39
+ appliesToLegacy: true,
38
40
  specReference: "product.md#safety-notices",
39
41
  };
@@ -54,5 +54,7 @@ export const pdpW024 = {
54
54
  messageTemplate: "`pagefront:legalEntity` entry with role `{role}` at `{path}` declares an organization without an `address`.",
55
55
  remediation: "Add the entity's postal address to the `organization`. If no address is available, the entry still identifies the organization but should not be relied on as a regulatory responsible-person declaration.",
56
56
  introduced: "v0.8",
57
+ appliesFrom: "0.3.0",
58
+ appliesToLegacy: true,
57
59
  specReference: "product.md#legal-entities",
58
60
  };
@@ -31,5 +31,7 @@ export const pdpW025 = {
31
31
  messageTemplate: "Feature `{name}` at `{path}` carries value `{value}`, which parses as a number plus a unit.",
32
32
  remediation: "Move the numeric part into `value`, express the unit as a `unitCode`, and if the merchant's display formatting matters, carry the original string in `pagefront:presentationValue`.",
33
33
  introduced: "v0.8",
34
+ appliesFrom: "0.3.0",
35
+ appliesToLegacy: true,
34
36
  specReference: "product.md#feature-groups",
35
37
  };
@@ -27,5 +27,7 @@ export const pdpW026 = {
27
27
  messageTemplate: "Safety data sheet at `{path}` declares no `inLanguage`.",
28
28
  remediation: "Add the document's BCP 47 language tag as `inLanguage`. If the document is multilingual, declare the primary language.",
29
29
  introduced: "v0.8",
30
+ appliesFrom: "0.3.0",
31
+ appliesToLegacy: true,
30
32
  specReference: "product.md#documents",
31
33
  };
@@ -43,5 +43,7 @@ export const pdpW027 = {
43
43
  messageTemplate: "Question `{name}` at `{path}` has an `acceptedAnswer` with no text.",
44
44
  remediation: "Populate `acceptedAnswer.text` with the answer prose, or remove the `acceptedAnswer` (and consider whether an unanswered question belongs in the sheet at all).",
45
45
  introduced: "v0.8",
46
+ appliesFrom: "0.3.0",
47
+ appliesToLegacy: true,
46
48
  specReference: "product.md#product-qa",
47
49
  };
@@ -25,5 +25,7 @@ export const pdpW028 = {
25
25
  messageTemplate: "Offer at `{path}` states a price but not whether value-added tax is included.",
26
26
  remediation: "Add a `UnitPriceSpecification` entry mirroring `price` with `valueAddedTaxIncluded` set to `true` or `false`; on an Offer that already uses `priceSpecification`, set the flag on each entry.",
27
27
  introduced: "v0.9",
28
+ appliesFrom: "0.3.0",
29
+ appliesToLegacy: true,
28
30
  specReference: "product.md#tax-inclusion",
29
31
  };
@@ -24,5 +24,7 @@ export const pdpW029 = {
24
24
  messageTemplate: "Offer at `{path}` states a price but no region it applies to.",
25
25
  remediation: "Declare `areaServed` (or `eligibleRegion`) for the market whose displayed price this is. Where the market cannot be determined from the storefront, stating the tax basis via `valueAddedTaxIncluded` is the higher priority; see \"Tax inclusion.\"",
26
26
  introduced: "v0.9",
27
+ appliesFrom: "0.3.0",
28
+ appliesToLegacy: true,
27
29
  specReference: "product.md#tax-inclusion",
28
30
  };
@@ -48,5 +48,7 @@ export const pdpW030 = {
48
48
  messageTemplate: "Warranty at `{path}` states neither a duration nor a description.",
49
49
  remediation: "State the warranty as written in `description`, and add `durationOfWarranty` where a finite length is stated. A lifetime warranty carries `description` (and optionally `name`) with no `durationOfWarranty`.",
50
50
  introduced: "v0.9",
51
+ appliesFrom: "0.3.0",
52
+ appliesToLegacy: true,
51
53
  specReference: "product.md#warranty",
52
54
  };
@@ -31,5 +31,7 @@ export const pdpW031 = {
31
31
  messageTemplate: "Seller at `{path}` has an `@id` but no `pagefront:sheetUrl`, and the Offer states no return or shipping terms of its own.",
32
32
  remediation: "Add `pagefront:sheetUrl` to the seller reference, or state the terms on the Offer.",
33
33
  introduced: "v0.9",
34
+ appliesFrom: "0.3.0",
35
+ appliesToLegacy: true,
34
36
  specReference: "organization.md#linking-contract-identity-only",
35
37
  };
@@ -46,5 +46,7 @@ export const pdpW032 = {
46
46
  messageTemplate: "Pricing block at `{path}` {problem}, and `pricing` is not `partial`.",
47
47
  remediation: "State the modifier, zero included; for a text dimension, state it under the key `\"*\"`. If the dimension never changes the price, declare `affectsPrice: false` on it. If the price is not known, declare `pricing: \"partial\"`.",
48
48
  introduced: "v0.9 (2026-10-01 revision)",
49
+ appliesFrom: "0.3.0",
50
+ appliesToLegacy: true,
49
51
  specReference: "product.md#configurator-pricing-and-price-bands",
50
52
  };
@@ -26,5 +26,7 @@ export const pdpW033 = {
26
26
  messageTemplate: "Configurator at `{path}` has priced dimensions but no offer prices it.",
27
27
  remediation: "Add an offer carrying `pagefront:configuratorPricing` for this configurator. If no option changes the price, declare `affectsPrice: false` on each dimension.",
28
28
  introduced: "v0.9 (2026-10-01 revision)",
29
+ appliesFrom: "0.3.0",
30
+ appliesToLegacy: true,
29
31
  specReference: "product.md#configurator-pricing-and-price-bands",
30
32
  };
@@ -24,5 +24,7 @@ export const pdpW034 = {
24
24
  messageTemplate: "Offer price is not linked to the configurator; state which configuration it prices.",
25
25
  remediation: "Add `pagefront:configuratorPricing` to the offer: give the configurator an `@id`, reference it, and name the configuration the price is the total of. When only that one total is known, declare `pricing: \"partial\"`.",
26
26
  introduced: "v0.9 (2026-10-01 revision)",
27
+ appliesFrom: "0.3.0",
28
+ appliesToLegacy: true,
27
29
  specReference: "product.md#configurator-pricing-and-price-bands",
28
30
  };
@@ -37,5 +37,7 @@ export const pdpW035 = {
37
37
  messageTemplate: "basePrice may be a component price, not a configured total.",
38
38
  remediation: "Confirm that `basePrice` is what a buyer pays for the whole of `baseConfiguration`. If it is the price of one part, replace it with the configured total and recompute the modifiers as differences from that total.",
39
39
  introduced: "v0.9 (2026-10-01 revision)",
40
+ appliesFrom: "0.3.0",
41
+ appliesToLegacy: true,
40
42
  specReference: "product.md#configurator-pricing-and-price-bands",
41
43
  };
@@ -27,5 +27,7 @@ export const pdpW036 = {
27
27
  messageTemplate: "Option value should be a stable identifier; put the label in description.",
28
28
  remediation: "Replace the value with a lowercase identifier without spaces (`yellow_gold`, `0.5`) and move the label to `description`. Put a unit in the dimension's `note`. Update the pricing block's keys, `baseConfiguration`, `when` and `exclusions` to the new value.",
29
29
  introduced: "v0.9 (2026-10-01 revision)",
30
+ appliesFrom: "0.3.0",
31
+ appliesToLegacy: true,
30
32
  specReference: "product.md#configurator-pricing-and-price-bands",
31
33
  };
@@ -33,5 +33,7 @@ export const pdpW037 = {
33
33
  messageTemplate: "Option does not look like a size in the declared system.",
34
34
  remediation: "Remove the entry if it is a service, and describe the service in the dimension's `note` or in `pagefront:madeToOrderTerms.aftermarketServices`. If it is a size, write it as the system writes it, without the system's name.",
35
35
  introduced: "v0.9 (2026-10-01 revision)",
36
+ appliesFrom: "0.3.0",
37
+ appliesToLegacy: true,
36
38
  specReference: "product.md#configurator-pricing-and-price-bands",
37
39
  };
@@ -26,5 +26,7 @@ export const pdpW038 = {
26
26
  messageTemplate: "Note reads as page copy; state the fact.",
27
27
  remediation: "Restate the note as a fact in the third person: what the merchant offers or recommends, with no \"you\" and no instruction.",
28
28
  introduced: "v0.9 (2026-10-01 revision)",
29
+ appliesFrom: "0.3.0",
30
+ appliesToLegacy: true,
29
31
  specReference: "product.md#configurator-pricing-and-price-bands",
30
32
  };
@@ -25,5 +25,7 @@ export const pdpW039 = {
25
25
  messageTemplate: "Size advice at `{path}` could not be stated in the Sheet's sizing system (`{system}`); it is carried as prose only.",
26
26
  remediation: "If the merchant states the advice in the sizing dimension's system, add a `sizeAdjustment` in that system. If it does not, no action: do not convert between systems.",
27
27
  introduced: "v0.9 (2026-10-01 revision)",
28
+ appliesFrom: "0.3.0",
29
+ appliesToLegacy: true,
28
30
  specReference: "product.md#size-guidance-pagefrontsizechart-and-pagefrontsizerecommendation",
29
31
  };
@@ -1,16 +1,27 @@
1
1
  import type { CheckFn, Rule } from "@pagefront/lint-core";
2
2
  import type { SheetType } from "../sheet-type.js";
3
3
  /**
4
- * Release / schema mismatch. On the release line the `$schema` version
5
- * segment must be the sheet-spec version the release manifest maps the
6
- * declared `release` and the sheet type to; a release the manifest does
7
- * not list has no mapping and the rule is silent. On the legacy line
8
- * the segment must equal `format_version`, as before. Quiet when
9
- * `$schema` is absent or names no version, and when the sheet carries
10
- * both fields (the both-fields rule reports that).
4
+ * Release / schema mismatch. The rule reads `$schema` and is quiet
5
+ * whenever it is absent, on both lines: whether `$schema` is required is
6
+ * the schema's own statement (Product Sheet spec 0.10.0 and Organization
7
+ * Sheet spec 0.4.0 onward), not this rule's. On the release line a
8
+ * present `$schema` must be the URL of the sheet-spec schema the release
9
+ * manifest maps the declared `release` and the sheet type to; a release
10
+ * the manifest does not list has no mapping (the unknown-release rule
11
+ * reports it). On the legacy line the `$schema` version segment must
12
+ * equal `format_version`, as before. Quiet when the sheet carries both
13
+ * fields (the both-fields rule reports that).
11
14
  */
12
15
  export declare function releaseSchemaMismatch(sheetType: SheetType): CheckFn;
13
16
  export declare const RELEASE_SCHEMA_MISMATCH: Pick<Rule, "title" | "severity" | "target" | "messageTemplate" | "remediation">;
14
17
  /** A sheet carrying both `format_version` and `release`. */
15
18
  export declare const bothVersionFields: CheckFn;
16
19
  export declare const BOTH_VERSION_FIELDS: Pick<Rule, "title" | "severity" | "target" | "messageTemplate" | "remediation">;
20
+ /**
21
+ * Unknown release: the sheet declares a `release` the release manifest
22
+ * does not list, so no sheet-spec version and no schema can be resolved
23
+ * for it. Only a string `release` is judged; another JSON type is
24
+ * Layer 1's concern.
25
+ */
26
+ export declare const unknownRelease: CheckFn;
27
+ export declare const UNKNOWN_RELEASE: Pick<Rule, "title" | "severity" | "target" | "messageTemplate" | "remediation">;
@@ -1,4 +1,4 @@
1
- import { sheetSpecVersionFor } from "../releases.js";
1
+ import { KNOWN_RELEASES, schemaUrlFor, sheetSpecVersionFor } from "../releases.js";
2
2
  /**
3
3
  * Shared checks for the envelope's version fields, used by the product
4
4
  * and organization catalogues (PDP-E-002 / ORG-E-003, PDP-E-016 /
@@ -11,39 +11,59 @@ const SHEET_LABEL = {
11
11
  organization: "Organization Sheet",
12
12
  };
13
13
  /**
14
- * Release / schema mismatch. On the release line the `$schema` version
15
- * segment must be the sheet-spec version the release manifest maps the
16
- * declared `release` and the sheet type to; a release the manifest does
17
- * not list has no mapping and the rule is silent. On the legacy line
18
- * the segment must equal `format_version`, as before. Quiet when
19
- * `$schema` is absent or names no version, and when the sheet carries
20
- * both fields (the both-fields rule reports that).
14
+ * Release / schema mismatch. The rule reads `$schema` and is quiet
15
+ * whenever it is absent, on both lines: whether `$schema` is required is
16
+ * the schema's own statement (Product Sheet spec 0.10.0 and Organization
17
+ * Sheet spec 0.4.0 onward), not this rule's. On the release line a
18
+ * present `$schema` must be the URL of the sheet-spec schema the release
19
+ * manifest maps the declared `release` and the sheet type to; a release
20
+ * the manifest does not list has no mapping (the unknown-release rule
21
+ * reports it). On the legacy line the `$schema` version segment must
22
+ * equal `format_version`, as before. Quiet when the sheet carries both
23
+ * fields (the both-fields rule reports that).
21
24
  */
22
25
  export function releaseSchemaMismatch(sheetType) {
23
26
  return (_match, ctx) => {
24
27
  const { release, format_version: formatVersion, $schema: schema } = ctx.sheet;
25
28
  if (typeof schema !== "string")
26
29
  return null;
27
- const schemaVersion = SCHEMA_VERSION.exec(schema)?.[1];
28
- if (schemaVersion === undefined)
29
- return null;
30
30
  if (release !== undefined && formatVersion !== undefined)
31
31
  return null;
32
+ const schemaVersion = SCHEMA_VERSION.exec(schema)?.[1];
32
33
  if (typeof release === "string") {
33
34
  const expected = sheetSpecVersionFor(release, sheetType);
34
- if (expected === undefined || expected === schemaVersion)
35
+ if (expected === undefined)
36
+ return null;
37
+ const expectedUrl = schemaUrlFor(sheetType, expected);
38
+ if (schema === expectedUrl)
35
39
  return null;
40
+ const declared = `\`release\` \`${release}\` bundles ${SHEET_LABEL[sheetType]} spec \`${expected}\``;
41
+ if (schemaVersion !== undefined && schemaVersion !== expected) {
42
+ return {
43
+ path: "$['release']",
44
+ values: {
45
+ declared,
46
+ found: `\`$schema\` URL declares version \`${schemaVersion}\``,
47
+ release,
48
+ expected,
49
+ schema_version: schemaVersion,
50
+ },
51
+ };
52
+ }
36
53
  return {
37
54
  path: "$['release']",
38
55
  values: {
39
- declared: `\`release\` \`${release}\` bundles ${SHEET_LABEL[sheetType]} spec \`${expected}\``,
56
+ declared,
57
+ found: `\`$schema\` is not that schema's URL, \`${expectedUrl}\``,
40
58
  release,
41
59
  expected,
42
- schema_version: schemaVersion,
60
+ expected_url: expectedUrl,
43
61
  },
44
62
  };
45
63
  }
46
64
  if (formatVersion !== undefined) {
65
+ if (schemaVersion === undefined)
66
+ return null;
47
67
  const declared = String(formatVersion);
48
68
  if (declared === schemaVersion)
49
69
  return null;
@@ -51,6 +71,7 @@ export function releaseSchemaMismatch(sheetType) {
51
71
  path: "$['format_version']",
52
72
  values: {
53
73
  declared: `\`format_version\` is \`${declared}\``,
74
+ found: `\`$schema\` URL declares version \`${schemaVersion}\``,
54
75
  format_version: declared,
55
76
  schema_version: schemaVersion,
56
77
  },
@@ -63,7 +84,7 @@ export const RELEASE_SCHEMA_MISMATCH = {
63
84
  title: "Release / schema mismatch",
64
85
  severity: "error",
65
86
  target: "$",
66
- messageTemplate: "{declared} but `$schema` URL declares version `{schema_version}`.",
87
+ messageTemplate: "{declared} but {found}.",
67
88
  remediation: "Point `$schema` at the sheet-spec version the declared release bundles (see releases.md), or declare the release that bundles the schema the sheet was written against. On a legacy sheet, align `format_version` and the `$schema` version.",
68
89
  };
69
90
  /** A sheet carrying both `format_version` and `release`. */
@@ -75,3 +96,22 @@ export const BOTH_VERSION_FIELDS = {
75
96
  messageTemplate: "Sheet carries both `format_version` and `release`; a sheet declares one.",
76
97
  remediation: "Keep `release` and remove `format_version`. A sheet on the legacy draft line keeps `format_version` alone and its legacy `$schema`.",
77
98
  };
99
+ /**
100
+ * Unknown release: the sheet declares a `release` the release manifest
101
+ * does not list, so no sheet-spec version and no schema can be resolved
102
+ * for it. Only a string `release` is judged; another JSON type is
103
+ * Layer 1's concern.
104
+ */
105
+ export const unknownRelease = (_match, ctx) => {
106
+ const { release } = ctx.sheet;
107
+ if (typeof release !== "string" || KNOWN_RELEASES.includes(release))
108
+ return null;
109
+ return { path: "$['release']", values: { release, known: KNOWN_RELEASES.join(", ") } };
110
+ };
111
+ export const UNKNOWN_RELEASE = {
112
+ title: "Unknown release",
113
+ severity: "error",
114
+ target: "$",
115
+ messageTemplate: "`release` at `{path}` is `{release}`, which is not a release this catalogue knows ({known}).",
116
+ remediation: "Declare a published commerce release (see releases.md). If the release is newer than this linter, update the linter.",
117
+ };
@@ -0,0 +1,42 @@
1
+ import type { Sheet } from "@pagefront/lint-core";
2
+ import type { SheetType } from "./sheet-type.js";
3
+ /**
4
+ * Which frozen schema a sheet is validated against. The package bundles
5
+ * every schema ever published (commerce/schemas/), keyed by `$id`. A
6
+ * sheet is judged by the release it declares: on the release line the
7
+ * schema is the one the release manifest gives for the declared
8
+ * `release` and the sheet's type, whatever `$schema` says (a `$schema`
9
+ * that disagrees is PDP-E-002's finding, at Layer 2). Legacy sheets,
10
+ * which declare no release, keep resolving through their `$schema`.
11
+ */
12
+ export type SchemaResolution = {
13
+ kind: "schema";
14
+ id: string;
15
+ }
16
+ /** `release` names no release in the manifest. */
17
+ | {
18
+ kind: "unknown-release";
19
+ declared: string;
20
+ }
21
+ /** A legacy sheet's `$schema` names no schema this package bundles. */
22
+ | {
23
+ kind: "unknown-schema";
24
+ declared: string;
25
+ };
26
+ /**
27
+ * Resolve the schema for a document.
28
+ *
29
+ * - Release line (`release` present): the sheet-spec version the
30
+ * manifest bundles in that release for the sheet's type. The type
31
+ * comes from the usual dispatch (`$schema` path, else data-block
32
+ * shape), so it does not depend on `$schema` being present.
33
+ * - Legacy line (`format_version`, no `release`): the schema `$schema`
34
+ * names when it is bundled, the legacy schema for the pre-2026-09-08
35
+ * identifier or when `$schema` is absent.
36
+ * - Neither field: the schema `$schema` names when it is bundled, else
37
+ * the current release's, whose `required` then reports the missing
38
+ * `release`.
39
+ *
40
+ * `sheetType` forces the type for callers that validate one type only.
41
+ */
42
+ export declare function resolveSchema(sheet: Sheet, sheetType?: SheetType): SchemaResolution;
@@ -0,0 +1,52 @@
1
+ import { SCHEMA_IDS } from "./generated/schema-ids.js";
2
+ import { CURRENT_RELEASE, legacySchemaUrl, schemaUrlFor, sheetSpecVersionFor, } from "./releases.js";
3
+ import { detectSheetLine, detectSheetType } from "./sheet-type.js";
4
+ /**
5
+ * The identifiers the legacy schemas carried before 2026-09-08
6
+ * (`…/pagefront/commerce/schemas/<type>/v0.9.json`); sheets declaring
7
+ * them validate against the legacy schema they became.
8
+ */
9
+ const FORMER_LEGACY_ID = /^https:\/\/themachineweb\.org\/pagefront\/commerce\/schemas\/(product|organization)\/v0\.9\.json$/;
10
+ /**
11
+ * Resolve the schema for a document.
12
+ *
13
+ * - Release line (`release` present): the sheet-spec version the
14
+ * manifest bundles in that release for the sheet's type. The type
15
+ * comes from the usual dispatch (`$schema` path, else data-block
16
+ * shape), so it does not depend on `$schema` being present.
17
+ * - Legacy line (`format_version`, no `release`): the schema `$schema`
18
+ * names when it is bundled, the legacy schema for the pre-2026-09-08
19
+ * identifier or when `$schema` is absent.
20
+ * - Neither field: the schema `$schema` names when it is bundled, else
21
+ * the current release's, whose `required` then reports the missing
22
+ * `release`.
23
+ *
24
+ * `sheetType` forces the type for callers that validate one type only.
25
+ */
26
+ export function resolveSchema(sheet, sheetType) {
27
+ const type = sheetType ?? detectSheetType(sheet);
28
+ const declared = sheet.$schema;
29
+ if (sheet.release !== undefined) {
30
+ // A non-string release is validated against the current schema,
31
+ // whose `release` pattern rejects it.
32
+ if (typeof sheet.release !== "string") {
33
+ return { kind: "schema", id: schemaUrlFor(type, sheetSpecVersionFor(CURRENT_RELEASE, type)) };
34
+ }
35
+ const version = sheetSpecVersionFor(sheet.release, type);
36
+ if (version === undefined)
37
+ return { kind: "unknown-release", declared: sheet.release };
38
+ return { kind: "schema", id: schemaUrlFor(type, version) };
39
+ }
40
+ if (typeof declared === "string") {
41
+ if (SCHEMA_IDS.includes(declared))
42
+ return { kind: "schema", id: declared };
43
+ const former = FORMER_LEGACY_ID.exec(declared);
44
+ if (former !== null)
45
+ return { kind: "schema", id: legacySchemaUrl(former[1]) };
46
+ if (detectSheetLine(sheet) === "legacy")
47
+ return { kind: "unknown-schema", declared };
48
+ }
49
+ if (detectSheetLine(sheet) === "legacy")
50
+ return { kind: "schema", id: legacySchemaUrl(type) };
51
+ return { kind: "schema", id: schemaUrlFor(type, sheetSpecVersionFor(CURRENT_RELEASE, type)) };
52
+ }
@@ -1,12 +1,11 @@
1
1
  /**
2
- * The Layer 1 validators, compiled ahead of time from the bundled
3
- * schemas by scripts/compile-validators.mjs into dist/schema-validators.js
4
- * (the second half of `npm run build`). This declaration is the module's
5
- * type; the script copies it beside the generated file.
2
+ * The Layer 1 validators, compiled ahead of time from every bundled
3
+ * schema (commerce/schemas/) by scripts/compile-validators.mjs into
4
+ * dist/schema-validators.js (the last step of `npm run build`). This
5
+ * declaration is the module's type; the script copies it beside the
6
+ * generated file.
6
7
  */
7
8
  import type { ValidateFunction } from "ajv";
8
9
 
9
- export const validateProduct: ValidateFunction;
10
- export const validateOrganization: ValidateFunction;
11
- export const validateLegacyProduct: ValidateFunction;
12
- export const validateLegacyOrganization: ValidateFunction;
10
+ /** One validator per bundled schema, keyed by the schema's `$id`. */
11
+ export const VALIDATORS: Readonly<Record<string, ValidateFunction>>;