@pagefront/lint-commerce 0.11.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 (88) hide show
  1. package/README.md +33 -13
  2. package/dist/catalogue.js +4 -2
  3. package/dist/generated/release-manifest.d.ts +8 -0
  4. package/dist/generated/release-manifest.js +9 -0
  5. package/dist/generated/schema-ids.js +2 -0
  6. package/dist/generated/schemas.js +2450 -7
  7. package/dist/organization-catalogue.js +3 -2
  8. package/dist/rules/org-e-001.js +2 -0
  9. package/dist/rules/org-e-002.js +2 -0
  10. package/dist/rules/org-e-003.js +2 -0
  11. package/dist/rules/org-e-004.d.ts +11 -0
  12. package/dist/rules/org-e-004.js +18 -0
  13. package/dist/rules/org-w-001.js +2 -0
  14. package/dist/rules/org-w-002.js +2 -0
  15. package/dist/rules/org-w-003.js +2 -0
  16. package/dist/rules/org-w-004.js +2 -0
  17. package/dist/rules/org-w-005.js +2 -0
  18. package/dist/rules/org-w-006.js +2 -0
  19. package/dist/rules/pdp-e-001.js +2 -0
  20. package/dist/rules/pdp-e-002.js +2 -0
  21. package/dist/rules/pdp-e-003.js +2 -0
  22. package/dist/rules/pdp-e-004.js +2 -0
  23. package/dist/rules/pdp-e-005.js +2 -0
  24. package/dist/rules/pdp-e-006.js +2 -0
  25. package/dist/rules/pdp-e-007.js +2 -0
  26. package/dist/rules/pdp-e-008.js +2 -0
  27. package/dist/rules/pdp-e-009.js +2 -0
  28. package/dist/rules/pdp-e-010.js +2 -0
  29. package/dist/rules/pdp-e-011.js +2 -0
  30. package/dist/rules/pdp-e-012.js +2 -0
  31. package/dist/rules/pdp-e-013.js +2 -0
  32. package/dist/rules/pdp-e-014.js +2 -0
  33. package/dist/rules/pdp-e-015.js +2 -0
  34. package/dist/rules/pdp-e-016.js +2 -0
  35. package/dist/rules/pdp-e-017.d.ts +2 -0
  36. package/dist/rules/pdp-e-017.js +80 -0
  37. package/dist/rules/pdp-e-018.d.ts +11 -0
  38. package/dist/rules/pdp-e-018.js +18 -0
  39. package/dist/rules/pdp-i-001.js +2 -0
  40. package/dist/rules/pdp-w-001.js +2 -0
  41. package/dist/rules/pdp-w-002.js +2 -0
  42. package/dist/rules/pdp-w-003.js +2 -0
  43. package/dist/rules/pdp-w-004.js +2 -0
  44. package/dist/rules/pdp-w-005.js +2 -0
  45. package/dist/rules/pdp-w-006.js +2 -0
  46. package/dist/rules/pdp-w-007.js +2 -0
  47. package/dist/rules/pdp-w-008.js +2 -0
  48. package/dist/rules/pdp-w-009.js +2 -0
  49. package/dist/rules/pdp-w-010.js +2 -0
  50. package/dist/rules/pdp-w-011.js +2 -0
  51. package/dist/rules/pdp-w-012.js +2 -0
  52. package/dist/rules/pdp-w-013.js +2 -0
  53. package/dist/rules/pdp-w-014.js +2 -0
  54. package/dist/rules/pdp-w-015.js +2 -0
  55. package/dist/rules/pdp-w-016.js +2 -0
  56. package/dist/rules/pdp-w-017.js +2 -0
  57. package/dist/rules/pdp-w-018.js +2 -0
  58. package/dist/rules/pdp-w-019.js +2 -0
  59. package/dist/rules/pdp-w-020.js +2 -0
  60. package/dist/rules/pdp-w-021.js +2 -0
  61. package/dist/rules/pdp-w-022.js +2 -0
  62. package/dist/rules/pdp-w-023.js +2 -0
  63. package/dist/rules/pdp-w-024.js +2 -0
  64. package/dist/rules/pdp-w-025.js +2 -0
  65. package/dist/rules/pdp-w-026.js +2 -0
  66. package/dist/rules/pdp-w-027.js +2 -0
  67. package/dist/rules/pdp-w-028.js +2 -0
  68. package/dist/rules/pdp-w-029.js +2 -0
  69. package/dist/rules/pdp-w-030.js +2 -0
  70. package/dist/rules/pdp-w-031.js +2 -0
  71. package/dist/rules/pdp-w-032.js +2 -0
  72. package/dist/rules/pdp-w-033.js +2 -0
  73. package/dist/rules/pdp-w-034.js +2 -0
  74. package/dist/rules/pdp-w-035.js +2 -0
  75. package/dist/rules/pdp-w-036.js +2 -0
  76. package/dist/rules/pdp-w-037.js +2 -0
  77. package/dist/rules/pdp-w-038.js +2 -0
  78. package/dist/rules/pdp-w-039.js +2 -0
  79. package/dist/rules/release-schema.d.ts +18 -7
  80. package/dist/rules/release-schema.js +55 -15
  81. package/dist/schema-registry.d.ts +27 -14
  82. package/dist/schema-registry.js +29 -12
  83. package/dist/schema-validators.js +12 -4
  84. package/dist/validate-and-lint.d.ts +13 -9
  85. package/dist/validate-and-lint.js +23 -10
  86. package/dist/version.d.ts +1 -1
  87. package/dist/version.js +1 -1
  88. package/package.json +2 -2
@@ -2,6 +2,7 @@ import { KNOWN_RELEASES } from "./releases.js";
2
2
  import { orgE001 } from "./rules/org-e-001.js";
3
3
  import { orgE002 } from "./rules/org-e-002.js";
4
4
  import { orgE003 } from "./rules/org-e-003.js";
5
+ import { orgE004 } from "./rules/org-e-004.js";
5
6
  import { orgW001 } from "./rules/org-w-001.js";
6
7
  import { orgW002 } from "./rules/org-w-002.js";
7
8
  import { orgW003 } from "./rules/org-w-003.js";
@@ -20,6 +21,6 @@ import { orgW006 } from "./rules/org-w-006.js";
20
21
  export const organizationCatalogue = {
21
22
  vertical: "commerce",
22
23
  releases: KNOWN_RELEASES,
23
- catalogueVersion: "0.15.0",
24
- rules: [orgE001, orgE002, orgE003, orgW001, orgW002, orgW003, orgW004, orgW005, orgW006],
24
+ catalogueVersion: "0.16.0",
25
+ rules: [orgE001, orgE002, orgE003, orgE004, orgW001, orgW002, orgW003, orgW004, orgW005, orgW006],
25
26
  };
@@ -13,5 +13,7 @@ export const orgE001 = {
13
13
  messageTemplate: "The Sheet uses `{term}` but `@context` at `{path}` does not declare the `pagefront` prefix, so the term does not resolve to the Pagefront vocabulary.",
14
14
  remediation: 'Declare the prefix: `"@context": ["https://schema.org", {"pagefront": "https://themachineweb.org/pagefront/vocabulary/"}]`.',
15
15
  introduced: "v0.9",
16
+ appliesFrom: "0.3.0",
17
+ appliesToLegacy: true,
16
18
  specReference: "vocabulary.md#how-to-use-this-vocabulary",
17
19
  };
@@ -9,5 +9,7 @@ export const orgE002 = {
9
9
  ...BOTH_VERSION_FIELDS,
10
10
  check: bothVersionFields,
11
11
  introduced: "release 0.3.0",
12
+ appliesFrom: "0.3.0",
13
+ appliesToLegacy: true,
12
14
  specReference: "organization.md#the-envelope",
13
15
  };
@@ -9,5 +9,7 @@ export const orgE003 = {
9
9
  ...RELEASE_SCHEMA_MISMATCH,
10
10
  check: releaseSchemaMismatch("organization"),
11
11
  introduced: "release 0.3.0",
12
+ appliesFrom: "0.3.0",
13
+ appliesToLegacy: true,
12
14
  specReference: "organization.md#the-envelope",
13
15
  };
@@ -0,0 +1,11 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ /**
3
+ * ORG-E-004 — Unknown release.
4
+ * Transcribed from commerce/spec/rules.md (normative); the check is
5
+ * shared between the two catalogues (release-schema.ts).
6
+ *
7
+ * `appliesFrom` is "0.0.0": the rule judges the declared release
8
+ * itself, so it cannot be gated by it. It never fires on a sheet of a
9
+ * published release.
10
+ */
11
+ export declare const orgE004: Rule;
@@ -0,0 +1,18 @@
1
+ import { UNKNOWN_RELEASE, unknownRelease } from "./release-schema.js";
2
+ /**
3
+ * ORG-E-004 — Unknown release.
4
+ * Transcribed from commerce/spec/rules.md (normative); the check is
5
+ * shared between the two catalogues (release-schema.ts).
6
+ *
7
+ * `appliesFrom` is "0.0.0": the rule judges the declared release
8
+ * itself, so it cannot be gated by it. It never fires on a sheet of a
9
+ * published release.
10
+ */
11
+ export const orgE004 = {
12
+ id: "ORG-E-004",
13
+ ...UNKNOWN_RELEASE,
14
+ check: unknownRelease,
15
+ introduced: "release 0.4.0",
16
+ appliesFrom: "0.0.0",
17
+ specReference: "organization.md#the-envelope",
18
+ };
@@ -27,5 +27,7 @@ export const orgW001 = {
27
27
  messageTemplate: "`pagefront:commerceRole` at `{path}` declares `brand_owner`, but the sheet carries no `brand` node.",
28
28
  remediation: "Add the `Brand` node with `@id` and `name` — the `Brand.@id` is the identity Product Sheets resolve via `Product.brand`.",
29
29
  introduced: "v0.9",
30
+ appliesFrom: "0.3.0",
31
+ appliesToLegacy: true,
30
32
  specReference: "organization.md#role-discriminator--pagefrontcommercerole",
31
33
  };
@@ -28,5 +28,7 @@ export const orgW002 = {
28
28
  messageTemplate: "`pagefront:commerceRole` at `{path}` declares `seller`, but none of `hasMerchantReturnPolicy`, `pagefront:fulfillmentTerms`, `paymentAccepted`, or `location` is present.",
29
29
  remediation: "Add the store-wide terms the seller role promises: the return policy, fulfillment terms, payment methods, or physical locations.",
30
30
  introduced: "v0.9",
31
+ appliesFrom: "0.3.0",
32
+ appliesToLegacy: true,
31
33
  specReference: "organization.md#seller-block-seller",
32
34
  };
@@ -26,5 +26,7 @@ export const orgW003 = {
26
26
  messageTemplate: "A `brand` node is present at `{path}`, but `pagefront:commerceRole` does not declare `brand_owner`.",
27
27
  remediation: "Declare `brand_owner` in `pagefront:commerceRole`, or remove the `brand` node if this sheet is not the brand's sheet.",
28
28
  introduced: "v0.9",
29
+ appliesFrom: "0.3.0",
30
+ appliesToLegacy: true,
29
31
  specReference: "organization.md#role-discriminator--pagefrontcommercerole",
30
32
  };
@@ -28,5 +28,7 @@ export const orgW004 = {
28
28
  messageTemplate: "Seller content (`{fields}`) is present at `{path}`, but `pagefront:commerceRole` does not declare `seller`.",
29
29
  remediation: "Declare `seller` in `pagefront:commerceRole`, or move the store-wide terms to the seller's own sheet.",
30
30
  introduced: "v0.9",
31
+ appliesFrom: "0.3.0",
32
+ appliesToLegacy: true,
31
33
  specReference: "organization.md#seller-block-seller",
32
34
  };
@@ -50,5 +50,7 @@ export const orgW005 = {
50
50
  messageTemplate: "`{field}` at `{path}` carries `{value}`, which does not match the {form}.",
51
51
  remediation: "Correct the machine key: `iso6523Code` is `NNNN:register-number` (e.g. `0002:808641435` for a SIREN), `leiCode` is a 20-character ISO 17442 LEI. If no well-formed machine key exists for this register, drop the field and keep the display-form `identifier` — absence is conformant.",
52
52
  introduced: "v0.9",
53
+ appliesFrom: "0.3.0",
54
+ appliesToLegacy: true,
53
55
  specReference: "organization.md#company-registration-identity",
54
56
  };
@@ -28,5 +28,7 @@ export const orgW006 = {
28
28
  messageTemplate: "`pagefront:authorizedResellers` is present at `{path}`, but `pagefront:commerceRole` does not declare `brand_owner`.",
29
29
  remediation: "Declare `brand_owner` in `pagefront:commerceRole` if this is the brand's own sheet; otherwise remove the field — only the brand's sheet can state whom the brand authorizes.",
30
30
  introduced: "v0.9",
31
+ appliesFrom: "0.3.0",
32
+ appliesToLegacy: true,
31
33
  specReference: "organization.md#authorized-resellers",
32
34
  };
@@ -47,5 +47,7 @@ export const pdpE001 = {
47
47
  messageTemplate: "Reference `{value}` at `{path}` does not resolve to any entity in this Sheet.",
48
48
  remediation: "Either correct the reference to match an existing `@id`, or add the referenced entity to the Sheet. Same-document references (fragments, bare tokens, or full URLs on this Sheet's own base) must resolve within the Sheet, by exact string match. A pricing block's `configurator.@id` must equal the `@id` declared on the Sheet's `pagefront:configurator`. A full-URL `@id` on another document's base in `isVariantOf` or a `hasVariant` entry is a legitimate cross-Sheet reference (multi-Sheet variant family) and is validated for URL shape only — see PDP-W-021 for the traversal-hint recommendation that accompanies it.",
49
49
  introduced: "v0.6",
50
+ appliesFrom: "0.3.0",
51
+ appliesToLegacy: true,
50
52
  specReference: "product.md#itemoffered-references",
51
53
  };
@@ -9,5 +9,7 @@ export const pdpE002 = {
9
9
  ...RELEASE_SCHEMA_MISMATCH,
10
10
  check: releaseSchemaMismatch("product"),
11
11
  introduced: "v0.6",
12
+ appliesFrom: "0.3.0",
13
+ appliesToLegacy: true,
12
14
  specReference: "product.md#the-envelope",
13
15
  };
@@ -13,5 +13,7 @@ export const pdpE003 = {
13
13
  messageTemplate: "The Sheet uses `{term}` but `@context` at `{path}` does not declare the `pagefront` prefix, so the term does not resolve to the Pagefront vocabulary.",
14
14
  remediation: 'Declare the prefix: `"@context": ["https://schema.org", {"pagefront": "https://themachineweb.org/pagefront/vocabulary/"}]`.',
15
15
  introduced: "v0.9",
16
+ appliesFrom: "0.3.0",
17
+ appliesToLegacy: true,
16
18
  specReference: "vocabulary.md#how-to-use-this-vocabulary",
17
19
  };
@@ -43,5 +43,7 @@ export const pdpE004 = {
43
43
  messageTemplate: "Price specification at `{path}` states `{specPrice}` but its Offer's `price` is `{price}`.",
44
44
  remediation: "Make the two amounts equal, or remove the duplicated amount from the specification entry and keep `valueAddedTaxIncluded` (and `priceCurrency`) on it.",
45
45
  introduced: "v0.9",
46
+ appliesFrom: "0.3.0",
47
+ appliesToLegacy: true,
46
48
  specReference: "product.md#tax-inclusion",
47
49
  };
@@ -36,5 +36,7 @@ export const pdpE005 = {
36
36
  messageTemplate: "Seller sheet URL at `{path}` is on host `{sheetUrlHost}`; the product sheet is on `{sheetHost}`.",
37
37
  remediation: "Publish the seller's organization sheet on the storefront host.",
38
38
  introduced: "v0.9",
39
+ appliesFrom: "0.3.0",
40
+ appliesToLegacy: true,
39
41
  specReference: "organization.md#linking-contract-identity-only",
40
42
  };
@@ -32,5 +32,7 @@ export const pdpE006 = {
32
32
  messageTemplate: "Brand sheet URL at `{path}` is on host `{sheetUrlHost}`; the brand's `@id` is on `{idHost}`.",
33
33
  remediation: "Point `pagefront:sheetUrl` at the sheet the brand publishes on its own host, or remove it.",
34
34
  introduced: "v0.9",
35
+ appliesFrom: "0.3.0",
36
+ appliesToLegacy: true,
35
37
  specReference: "organization.md#linking-contract-identity-only",
36
38
  };
@@ -49,5 +49,7 @@ export const pdpE007 = {
49
49
  messageTemplate: "Base configuration at `{path}` {problem}.",
50
50
  remediation: "Name exactly one option for every priced dimension, spelled as the configurator's `options` spell it. If a dimension never changes the price, declare `affectsPrice: false` on it and leave it out.",
51
51
  introduced: "v0.9 (2026-10-01 revision)",
52
+ appliesFrom: "0.3.0",
53
+ appliesToLegacy: true,
52
54
  specReference: "product.md#configurator-pricing-and-price-bands",
53
55
  };
@@ -37,5 +37,7 @@ export const pdpE008 = {
37
37
  messageTemplate: "Modifier for base option `{option}` of `{dimension}` at `{path}` is `{modifier}`; the base option's modifier is `0`.",
38
38
  remediation: "Set the base option's modifier to zero and express the other options of the dimension as differences from it. If the modifier is right, `basePrice` is the total of another configuration: correct `baseConfiguration` or `basePrice`.",
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
  };
@@ -58,5 +58,7 @@ export const pdpE009 = {
58
58
  messageTemplate: "Modifier at `{path}` {problem}.",
59
59
  remediation: "Spell dimension names and option values as the configurator spells them. Remove modifiers for dimensions that do not change the price, or remove `affectsPrice: false` from a dimension that does.",
60
60
  introduced: "v0.9 (2026-10-01 revision)",
61
+ appliesFrom: "0.3.0",
62
+ appliesToLegacy: true,
61
63
  specReference: "product.md#configurator-pricing-and-price-bands",
62
64
  };
@@ -33,5 +33,7 @@ export const pdpE010 = {
33
33
  messageTemplate: "Offer `price` at `{path}` is `{price}` but its pricing block's `basePrice` is `{basePrice}`.",
34
34
  remediation: "Make the two amounts equal. If `price` is the amount the page shows on load, set `baseConfiguration` to the configuration the page preselects and `basePrice` to its total.",
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
  };
@@ -49,5 +49,7 @@ export const pdpE011 = {
49
49
  messageTemplate: "`{bound}` at `{path}` is `{stated}` but the {extreme} non-excluded configuration computed from the pricing block totals `{computed}`.",
50
50
  remediation: "Correct the bound, or correct the price list: a missing adjustment, a missing exclusion and a wrong modifier each move the computed band. If the price list is known to have gaps, declare `pricing: \"partial\"`.",
51
51
  introduced: "v0.9 (2026-10-01 revision)",
52
+ appliesFrom: "0.3.0",
53
+ appliesToLegacy: true,
52
54
  specReference: "product.md#configurator-pricing-and-price-bands",
53
55
  };
@@ -28,5 +28,7 @@ export const pdpE012 = {
28
28
  messageTemplate: "Offer at `{path}` prices configurator `{value}`, which the offer at `{firstPath}` already prices.",
29
29
  remediation: "Keep one pricing block per configurator. Publish each further market's price list in that market's own Sheet.",
30
30
  introduced: "v0.9 (2026-10-01 revision)",
31
+ appliesFrom: "0.3.0",
32
+ appliesToLegacy: true,
31
33
  specReference: "product.md#configurator-pricing-and-price-bands",
32
34
  };
@@ -22,5 +22,7 @@ export const pdpE013 = {
22
22
  messageTemplate: "Dimension `{dimension}` at `{path}` declares `inputType: \"text\"` and enumerates options.",
23
23
  remediation: "Remove `options` from a free-text dimension, or remove `inputType: \"text\"` from a dimension the buyer picks a value in.",
24
24
  introduced: "v0.9 (2026-10-01 revision)",
25
+ appliesFrom: "0.3.0",
26
+ appliesToLegacy: true,
25
27
  specReference: "product.md#configurator-pricing-and-price-bands",
26
28
  };
@@ -30,5 +30,7 @@ export const pdpE014 = {
30
30
  messageTemplate: "Size adjustment at `{path}` is in `{sizeSystem}` but the Sheet's sizing dimension is in `{dimensionSystem}`.",
31
31
  remediation: "State the adjustment in the sizing dimension's system if the merchant states it there. If the merchant's advice is in another system, remove `sizeAdjustment` and keep the merchant's wording in `description`.",
32
32
  introduced: "v0.9 (2026-10-01 revision)",
33
+ appliesFrom: "0.3.0",
34
+ appliesToLegacy: true,
33
35
  specReference: "product.md#size-guidance-pagefrontsizechart-and-pagefrontsizerecommendation",
34
36
  };
@@ -87,5 +87,7 @@ export const pdpE015 = {
87
87
  messageTemplate: "Amounts mix numbers and strings; use one carrier throughout the Sheet.",
88
88
  remediation: "Carry every amount the same way. Decimal strings are recommended when amounts have fractional parts.",
89
89
  introduced: "v0.9 (2026-10-01 revision)",
90
+ appliesFrom: "0.3.0",
91
+ appliesToLegacy: true,
90
92
  specReference: "product.md#amounts",
91
93
  };
@@ -9,5 +9,7 @@ export const pdpE016 = {
9
9
  ...BOTH_VERSION_FIELDS,
10
10
  check: bothVersionFields,
11
11
  introduced: "release 0.3.0",
12
+ appliesFrom: "0.3.0",
13
+ appliesToLegacy: true,
12
14
  specReference: "product.md#the-envelope",
13
15
  };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ export declare const pdpE017: Rule;
@@ -0,0 +1,80 @@
1
+ import { asObject, own, pricedConfiguratorId, pricingBlock, seg } from "./configurator-pricing.js";
2
+ function declaredDimensions(configurator) {
3
+ const out = new Map();
4
+ const dimensions = configurator["dimensions"];
5
+ if (!Array.isArray(dimensions))
6
+ return out;
7
+ for (const entry of dimensions) {
8
+ const d = asObject(entry);
9
+ if (d === null || typeof d["name"] !== "string" || out.has(d["name"]))
10
+ continue;
11
+ const options = d["options"];
12
+ out.set(d["name"], {
13
+ values: Array.isArray(options) && options.length > 0
14
+ ? options.map((option) => own(asObject(option), "value"))
15
+ : null,
16
+ });
17
+ }
18
+ return out;
19
+ }
20
+ function unresolved(pairs, path, dimensions) {
21
+ const hits = [];
22
+ for (const [dimension, value] of Object.entries(asObject(pairs) ?? {})) {
23
+ const option = JSON.stringify(value);
24
+ const declared = dimensions.get(dimension);
25
+ let problem = null;
26
+ if (declared === undefined) {
27
+ problem = `the configurator declares no dimension \`${dimension}\``;
28
+ }
29
+ else if (declared.values !== null && !declared.values.some((v) => v === value)) {
30
+ problem = `\`${dimension}\` lists no option \`${option}\``;
31
+ }
32
+ if (problem !== null) {
33
+ hits.push({ path: `${path}${seg(dimension)}`, values: { dimension, option, problem } });
34
+ }
35
+ }
36
+ return hits;
37
+ }
38
+ export const pdpE017 = {
39
+ id: "PDP-E-017",
40
+ title: "Configurator reference does not resolve",
41
+ severity: "error",
42
+ target: "$.data",
43
+ check: (match) => {
44
+ const data = asObject(match.value);
45
+ const configurator = asObject(own(data, "pagefront:configurator"));
46
+ if (data === null || configurator === null)
47
+ return null;
48
+ const dimensions = declaredDimensions(configurator);
49
+ const hits = [];
50
+ const exclusions = configurator["exclusions"];
51
+ if (Array.isArray(exclusions)) {
52
+ exclusions.forEach((entry, index) => {
53
+ hits.push(...unresolved(entry, `${match.path}['pagefront:configurator']['exclusions'][${index}]`, dimensions));
54
+ });
55
+ }
56
+ // An adjustment's `when` refers to the configurator its pricing block
57
+ // names. A block naming another `@id` is PDP-E-001's finding, and its
58
+ // references are not resolved against this configurator.
59
+ const offers = own(data, "offers");
60
+ if (Array.isArray(offers)) {
61
+ offers.forEach((offer, offerIndex) => {
62
+ const block = pricingBlock(offer);
63
+ if (block === null || !Array.isArray(block["adjustments"]))
64
+ return;
65
+ if (pricedConfiguratorId(block) !== configurator["@id"])
66
+ return;
67
+ block["adjustments"].forEach((adjustment, index) => {
68
+ hits.push(...unresolved(asObject(adjustment)?.["when"], `${match.path}['offers'][${offerIndex}]['pagefront:configuratorPricing']['adjustments'][${index}]['when']`, dimensions));
69
+ });
70
+ });
71
+ }
72
+ return hits.length > 0 ? hits : null;
73
+ },
74
+ messageTemplate: "Configuration reference at `{path}` (dimension `{dimension}`, option `{option}`) does not resolve: {problem}.",
75
+ remediation: "Name a dimension the configurator declares, and write the option exactly as its `value` is written in that dimension's `options`, including its JSON type. Remove an exclusion or adjustment that refers to an option the product no longer offers.",
76
+ introduced: "release 0.4.0",
77
+ // The requirement is introduced by release 0.4.0 (Product Sheet spec 0.10.0).
78
+ appliesFrom: "0.4.0",
79
+ specReference: "product.md#configurator-pricing-and-price-bands",
80
+ };
@@ -0,0 +1,11 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ /**
3
+ * PDP-E-018 — Unknown release.
4
+ * Transcribed from commerce/spec/rules.md (normative); the check is
5
+ * shared between the two catalogues (release-schema.ts).
6
+ *
7
+ * `appliesFrom` is "0.0.0": the rule judges the declared release
8
+ * itself, so it cannot be gated by it. It never fires on a sheet of a
9
+ * published release.
10
+ */
11
+ export declare const pdpE018: Rule;
@@ -0,0 +1,18 @@
1
+ import { UNKNOWN_RELEASE, unknownRelease } from "./release-schema.js";
2
+ /**
3
+ * PDP-E-018 — Unknown release.
4
+ * Transcribed from commerce/spec/rules.md (normative); the check is
5
+ * shared between the two catalogues (release-schema.ts).
6
+ *
7
+ * `appliesFrom` is "0.0.0": the rule judges the declared release
8
+ * itself, so it cannot be gated by it. It never fires on a sheet of a
9
+ * published release.
10
+ */
11
+ export const pdpE018 = {
12
+ id: "PDP-E-018",
13
+ ...UNKNOWN_RELEASE,
14
+ check: unknownRelease,
15
+ introduced: "release 0.4.0",
16
+ appliesFrom: "0.0.0",
17
+ specReference: "product.md#the-envelope",
18
+ };
@@ -25,5 +25,7 @@ export const pdpI001 = {
25
25
  messageTemplate: "Property `{property}` at `{path}` is valid Schema.org but is not explicitly modeled by Pagefront.",
26
26
  remediation: "No action required. This finding is informational. If you find yourself using a Schema.org property frequently and would value formal Pagefront modeling for it, consider opening a spec proposal.",
27
27
  introduced: "v0.6",
28
+ appliesFrom: "0.3.0",
29
+ appliesToLegacy: true,
28
30
  specReference: "product.md#data--schemaorg-json-ld",
29
31
  };
@@ -18,5 +18,7 @@ export const pdpW001 = {
18
18
  messageTemplate: "No images declared at `{path}`.",
19
19
  remediation: "Add at least one `ImageObject` to the `data.image` array. The primary image should typically declare role `hero` or `packshot`.",
20
20
  introduced: "v0.6",
21
+ appliesFrom: "0.3.0",
22
+ appliesToLegacy: true,
21
23
  specReference: "product.md#image-roles",
22
24
  };
@@ -19,5 +19,7 @@ export const pdpW002 = {
19
19
  messageTemplate: "No description at `{path}`.",
20
20
  remediation: "Add a `description` field with a 1–3 paragraph prose description of the product. The description should match the Sheet's `inLanguage`.",
21
21
  introduced: "v0.6",
22
+ appliesFrom: "0.3.0",
23
+ appliesToLegacy: true,
22
24
  specReference: "product.md#required-and-recommended-core-fields",
23
25
  };
@@ -18,5 +18,7 @@ export const pdpW003 = {
18
18
  messageTemplate: "No brand declared at `{path}`.",
19
19
  remediation: "Add a `brand` field with a typed `Brand` or `Organization` value, including at minimum `name` and optionally `url`.",
20
20
  introduced: "v0.6",
21
+ appliesFrom: "0.3.0",
22
+ appliesToLegacy: true,
21
23
  specReference: "product.md#required-and-recommended-core-fields",
22
24
  };
@@ -26,5 +26,7 @@ export const pdpW004 = {
26
26
  messageTemplate: "Offer at `{path}` has `price` but no `priceCurrency`.",
27
27
  remediation: "Add a `priceCurrency` field with the ISO 4217 currency code matching the offer's currency.",
28
28
  introduced: "v0.6",
29
+ appliesFrom: "0.3.0",
30
+ appliesToLegacy: true,
29
31
  specReference: "product.md#offers",
30
32
  };
@@ -40,5 +40,7 @@ export const pdpW005 = {
40
40
  messageTemplate: "Material composition percentages at `{path}` sum to `{sum}`, not approximately 100.",
41
41
  remediation: "Verify the composition data. If a material is missing, add it. If the values are correct but the publisher's source data uses percentages for a different denominator, omit `percentage` rather than use a value that doesn't represent a fraction of total composition.",
42
42
  introduced: "v0.6",
43
+ appliesFrom: "0.3.0",
44
+ appliesToLegacy: true,
43
45
  specReference: "product.md#materials-material-and-pagefrontmaterialcomposition",
44
46
  };
@@ -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
  };