@pagefront/lint-commerce 0.7.0 → 0.9.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 (45) hide show
  1. package/README.md +57 -2
  2. package/dist/catalogue.js +19 -2
  3. package/dist/organization-catalogue.js +1 -1
  4. package/dist/product-schema.js +164 -25
  5. package/dist/rules/configurator-pricing.d.ts +133 -0
  6. package/dist/rules/configurator-pricing.js +385 -0
  7. package/dist/rules/pdp-e-001.js +12 -3
  8. package/dist/rules/pdp-e-007.d.ts +12 -0
  9. package/dist/rules/pdp-e-007.js +53 -0
  10. package/dist/rules/pdp-e-008.d.ts +10 -0
  11. package/dist/rules/pdp-e-008.js +41 -0
  12. package/dist/rules/pdp-e-009.d.ts +12 -0
  13. package/dist/rules/pdp-e-009.js +62 -0
  14. package/dist/rules/pdp-e-010.d.ts +10 -0
  15. package/dist/rules/pdp-e-010.js +37 -0
  16. package/dist/rules/pdp-e-011.d.ts +11 -0
  17. package/dist/rules/pdp-e-011.js +53 -0
  18. package/dist/rules/pdp-e-012.d.ts +2 -0
  19. package/dist/rules/pdp-e-012.js +32 -0
  20. package/dist/rules/pdp-e-013.d.ts +9 -0
  21. package/dist/rules/pdp-e-013.js +26 -0
  22. package/dist/rules/pdp-e-014.d.ts +11 -0
  23. package/dist/rules/pdp-e-014.js +34 -0
  24. package/dist/rules/pdp-e-015.d.ts +2 -0
  25. package/dist/rules/pdp-e-015.js +91 -0
  26. package/dist/rules/pdp-w-032.d.ts +11 -0
  27. package/dist/rules/pdp-w-032.js +50 -0
  28. package/dist/rules/pdp-w-033.d.ts +10 -0
  29. package/dist/rules/pdp-w-033.js +30 -0
  30. package/dist/rules/pdp-w-034.d.ts +10 -0
  31. package/dist/rules/pdp-w-034.js +28 -0
  32. package/dist/rules/pdp-w-035.d.ts +11 -0
  33. package/dist/rules/pdp-w-035.js +41 -0
  34. package/dist/rules/pdp-w-036.d.ts +2 -0
  35. package/dist/rules/pdp-w-036.js +31 -0
  36. package/dist/rules/pdp-w-037.d.ts +2 -0
  37. package/dist/rules/pdp-w-037.js +37 -0
  38. package/dist/rules/pdp-w-038.d.ts +2 -0
  39. package/dist/rules/pdp-w-038.js +30 -0
  40. package/dist/rules/pdp-w-039.d.ts +10 -0
  41. package/dist/rules/pdp-w-039.js +29 -0
  42. package/dist/schema-validators.js +8 -2
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.js +1 -1
  45. package/package.json +1 -1
@@ -0,0 +1,11 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ /**
3
+ * PDP-E-011 — AggregateOffer band disagrees with computed band.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Applies to an `AggregateOffer` whose pricing block is complete. Silent
7
+ * when the band cannot be computed: the reference does not resolve, the
8
+ * block fires PDP-E-007 or PDP-W-032, or every configuration is
9
+ * excluded. One finding per disagreeing bound, at the bound.
10
+ */
11
+ export declare const pdpE011: Rule;
@@ -0,0 +1,53 @@
1
+ import { asObject, baseConfigurationProblems, compareDecimal, computeBand, formatDecimal, isComplete, missingModifierProblems, parseDecimal, pricingBlock, resolveConfigurator, } from "./configurator-pricing.js";
2
+ /**
3
+ * PDP-E-011 — AggregateOffer band disagrees with computed band.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Applies to an `AggregateOffer` whose pricing block is complete. Silent
7
+ * when the band cannot be computed: the reference does not resolve, the
8
+ * block fires PDP-E-007 or PDP-W-032, or every configuration is
9
+ * excluded. One finding per disagreeing bound, at the bound.
10
+ */
11
+ export const pdpE011 = {
12
+ id: "PDP-E-011",
13
+ title: "AggregateOffer band disagrees with computed band",
14
+ severity: "error",
15
+ target: "$.data.offers[*]",
16
+ check: (match, ctx) => {
17
+ const offer = asObject(match.value);
18
+ if (offer === null || offer["@type"] !== "AggregateOffer")
19
+ return null;
20
+ const block = pricingBlock(offer);
21
+ if (block === null || !isComplete(block))
22
+ return null;
23
+ const configurator = resolveConfigurator(block, ctx.sheet);
24
+ if (configurator === null)
25
+ return null;
26
+ if (baseConfigurationProblems(configurator, block).length > 0)
27
+ return null;
28
+ if (missingModifierProblems(configurator, block).length > 0)
29
+ return null;
30
+ const band = computeBand(configurator, block);
31
+ if (band === null)
32
+ return null;
33
+ const hits = [];
34
+ const bounds = [
35
+ { bound: "lowPrice", extreme: "cheapest", computed: band.min },
36
+ { bound: "highPrice", extreme: "dearest", computed: band.max },
37
+ ];
38
+ for (const { bound, extreme, computed } of bounds) {
39
+ const stated = parseDecimal(offer[bound]);
40
+ if (stated === undefined || compareDecimal(stated, computed) === 0)
41
+ continue;
42
+ hits.push({
43
+ path: `${match.path}['${bound}']`,
44
+ values: { bound, extreme, stated: String(offer[bound]), computed: formatDecimal(computed) },
45
+ });
46
+ }
47
+ return hits.length > 0 ? hits : null;
48
+ },
49
+ messageTemplate: "`{bound}` at `{path}` is `{stated}` but the {extreme} non-excluded configuration computed from the pricing block totals `{computed}`.",
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
+ introduced: "v0.9 (2026-10-01 revision)",
52
+ specReference: "product.md#configurator-pricing-and-price-bands",
53
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ export declare const pdpE012: Rule;
@@ -0,0 +1,32 @@
1
+ import { pricedConfiguratorId, pricingBlock } from "./configurator-pricing.js";
2
+ /**
3
+ * PDP-E-012 — Configurator priced by more than one offer.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Fires on each `offers` entry whose pricing block names a configurator
7
+ * that an earlier entry already prices (exact string match on
8
+ * `configurator.@id`); `{firstPath}` is the first such entry.
9
+ */
10
+ function pricedId(offer) {
11
+ const block = pricingBlock(offer);
12
+ return block === null ? undefined : pricedConfiguratorId(block);
13
+ }
14
+ export const pdpE012 = {
15
+ id: "PDP-E-012",
16
+ title: "Configurator priced by more than one offer",
17
+ severity: "error",
18
+ target: "$.data.offers[*]",
19
+ check: (match, ctx) => {
20
+ const value = pricedId(match.value);
21
+ if (value === undefined)
22
+ return null;
23
+ const first = ctx.allMatches.find((m) => pricedId(m.value) === value);
24
+ if (first === undefined || first.path === match.path)
25
+ return null;
26
+ return { values: { value, firstPath: first.path } };
27
+ },
28
+ messageTemplate: "Offer at `{path}` prices configurator `{value}`, which the offer at `{firstPath}` already prices.",
29
+ remediation: "Keep one pricing block per configurator. Publish each further market's price list in that market's own Sheet.",
30
+ introduced: "v0.9 (2026-10-01 revision)",
31
+ specReference: "product.md#configurator-pricing-and-price-bands",
32
+ };
@@ -0,0 +1,9 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ /**
3
+ * PDP-E-013 — Text dimension with options.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Fires on a dimension whose `inputType` is `text` and whose `options`
7
+ * is an array with at least one entry.
8
+ */
9
+ export declare const pdpE013: Rule;
@@ -0,0 +1,26 @@
1
+ import { asObject } from "./configurator-pricing.js";
2
+ /**
3
+ * PDP-E-013 — Text dimension with options.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Fires on a dimension whose `inputType` is `text` and whose `options`
7
+ * is an array with at least one entry.
8
+ */
9
+ export const pdpE013 = {
10
+ id: "PDP-E-013",
11
+ title: "Text dimension with options",
12
+ severity: "error",
13
+ target: "$.data['pagefront:configurator'].dimensions[*]",
14
+ check: (match) => {
15
+ const d = asObject(match.value);
16
+ if (d === null || d["inputType"] !== "text")
17
+ return null;
18
+ if (!Array.isArray(d["options"]) || d["options"].length === 0)
19
+ return null;
20
+ return { values: { dimension: typeof d["name"] === "string" ? d["name"] : "(unnamed)" } };
21
+ },
22
+ messageTemplate: "Dimension `{dimension}` at `{path}` declares `inputType: \"text\"` and enumerates options.",
23
+ remediation: "Remove `options` from a free-text dimension, or remove `inputType: \"text\"` from a dimension the buyer picks a value in.",
24
+ introduced: "v0.9 (2026-10-01 revision)",
25
+ specReference: "product.md#configurator-pricing-and-price-bands",
26
+ };
@@ -0,0 +1,11 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ /**
3
+ * PDP-E-014 — Size advice in another system than the sizing dimension.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Fires when the adjustment carries a `sizeSystem`, the configurator has
7
+ * a sizing dimension in `UK`, `EU`, `US` or `JP`, and the adjustment's
8
+ * system corresponds to none of them. The `https://schema.org/` prefix
9
+ * on `sizeSystem` is accepted.
10
+ */
11
+ export declare const pdpE014: Rule;
@@ -0,0 +1,34 @@
1
+ import { asObject, CORRESPONDING_SIZE_SYSTEM, readConfigurator } from "./configurator-pricing.js";
2
+ /**
3
+ * PDP-E-014 — Size advice in another system than the sizing dimension.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Fires when the adjustment carries a `sizeSystem`, the configurator has
7
+ * a sizing dimension in `UK`, `EU`, `US` or `JP`, and the adjustment's
8
+ * system corresponds to none of them. The `https://schema.org/` prefix
9
+ * on `sizeSystem` is accepted.
10
+ */
11
+ export const pdpE014 = {
12
+ id: "PDP-E-014",
13
+ title: "Size advice in another system than the sizing dimension",
14
+ severity: "error",
15
+ target: "$.data['pagefront:sizeRecommendation'].sizeAdjustment",
16
+ check: (match, ctx) => {
17
+ const sizeSystem = asObject(match.value)?.["sizeSystem"];
18
+ if (typeof sizeSystem !== "string")
19
+ return null;
20
+ const systems = (readConfigurator(ctx.sheet)?.dimensions ?? [])
21
+ .map((d) => d.system)
22
+ .filter((s) => s !== undefined && Object.hasOwn(CORRESPONDING_SIZE_SYSTEM, s));
23
+ if (systems.length === 0)
24
+ return null;
25
+ const bare = sizeSystem.replace(/^https?:\/\/schema\.org\//, "");
26
+ if (systems.some((s) => CORRESPONDING_SIZE_SYSTEM[s] === bare))
27
+ return null;
28
+ return { values: { sizeSystem, dimensionSystem: systems[0] } };
29
+ },
30
+ messageTemplate: "Size adjustment at `{path}` is in `{sizeSystem}` but the Sheet's sizing dimension is in `{dimensionSystem}`.",
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
+ introduced: "v0.9 (2026-10-01 revision)",
33
+ specReference: "product.md#size-guidance-pagefrontsizechart-and-pagefrontsizerecommendation",
34
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ export declare const pdpE015: Rule;
@@ -0,0 +1,91 @@
1
+ import { asObject, seg } from "./configurator-pricing.js";
2
+ /**
3
+ * PDP-E-015 — Amounts mix numbers and strings.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Walks `data` in document order over the catalogue's closed list of
7
+ * amount fields and fires once per Sheet when at least one amount is a
8
+ * JSON number and at least one is a string. `numberPath` and
9
+ * `stringPath` are the first amount of each carrier.
10
+ */
11
+ const OFFER_FIELDS = ["price", "lowPrice", "highPrice"];
12
+ const SPECIFICATION_FIELDS = ["price", "minPrice", "maxPrice"];
13
+ const MONETARY_FIELDS = ["value", "minValue", "maxValue"];
14
+ const ROLE_OF_KEY = {
15
+ offers: "offer",
16
+ priceSpecification: "specification",
17
+ hasVariant: "variant",
18
+ "pagefront:promotions": "promotion",
19
+ "pagefront:configuratorPricing": "pricing",
20
+ };
21
+ function record(found, value, path) {
22
+ if (typeof value === "number")
23
+ found.numberPath ??= path;
24
+ else if (typeof value === "string")
25
+ found.stringPath ??= path;
26
+ }
27
+ function amountFields(node, role) {
28
+ const fields = [];
29
+ if (role === "offer")
30
+ fields.push(...OFFER_FIELDS);
31
+ if (role === "specification")
32
+ fields.push(...SPECIFICATION_FIELDS);
33
+ if (role === "variant")
34
+ fields.push("price");
35
+ if (role === "promotion")
36
+ fields.push("discountAmount");
37
+ if (role === "pricing")
38
+ fields.push("basePrice");
39
+ if (role === "adjustment")
40
+ fields.push("priceModifier");
41
+ if (node["@type"] === "MonetaryAmount")
42
+ fields.push(...MONETARY_FIELDS);
43
+ return fields;
44
+ }
45
+ function walk(node, path, role, found) {
46
+ if (Array.isArray(node)) {
47
+ node.forEach((item, index) => walk(item, `${path}[${index}]`, role, found));
48
+ return;
49
+ }
50
+ const obj = asObject(node);
51
+ if (obj === null)
52
+ return;
53
+ const fields = amountFields(obj, role);
54
+ for (const [key, value] of Object.entries(obj)) {
55
+ const childPath = `${path}${seg(key)}`;
56
+ if (fields.includes(key)) {
57
+ record(found, value, childPath);
58
+ continue;
59
+ }
60
+ if (role === "modifiers") {
61
+ // one entry per dimension; every value under it is an amount
62
+ for (const [option, amount] of Object.entries(asObject(value) ?? {})) {
63
+ record(found, amount, `${childPath}${seg(option)}`);
64
+ }
65
+ continue;
66
+ }
67
+ let childRole = ROLE_OF_KEY[key];
68
+ if (role === "pricing" && key === "modifiers")
69
+ childRole = "modifiers";
70
+ if (role === "pricing" && key === "adjustments")
71
+ childRole = "adjustment";
72
+ walk(value, childPath, childRole, found);
73
+ }
74
+ }
75
+ export const pdpE015 = {
76
+ id: "PDP-E-015",
77
+ title: "Amounts mix numbers and strings",
78
+ severity: "error",
79
+ target: "$.data",
80
+ check: (match) => {
81
+ const found = {};
82
+ walk(match.value, match.path, undefined, found);
83
+ if (found.numberPath === undefined || found.stringPath === undefined)
84
+ return null;
85
+ return { values: { numberPath: found.numberPath, stringPath: found.stringPath } };
86
+ },
87
+ messageTemplate: "Amounts mix numbers and strings; use one carrier throughout the Sheet.",
88
+ remediation: "Carry every amount the same way. Decimal strings are recommended when amounts have fractional parts.",
89
+ introduced: "v0.9 (2026-10-01 revision)",
90
+ specReference: "product.md#amounts",
91
+ };
@@ -0,0 +1,11 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ /**
3
+ * PDP-W-032 — Priced option without a modifier.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Applies to a resolving pricing block whose `pricing` is absent or
7
+ * `complete`. One finding per priced option with no modifier, one per
8
+ * priced text dimension with no `*` modifier, and one per priced select
9
+ * dimension that enumerates no options, all at the block.
10
+ */
11
+ export declare const pdpW032: Rule;
@@ -0,0 +1,50 @@
1
+ import { asObject, isComplete, missingModifierProblems, resolveConfigurator, } from "./configurator-pricing.js";
2
+ /**
3
+ * PDP-W-032 — Priced option without a modifier.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Applies to a resolving pricing block whose `pricing` is absent or
7
+ * `complete`. One finding per priced option with no modifier, one per
8
+ * priced text dimension with no `*` modifier, and one per priced select
9
+ * dimension that enumerates no options, all at the block.
10
+ */
11
+ export const pdpW032 = {
12
+ id: "PDP-W-032",
13
+ title: "Priced option without a modifier",
14
+ severity: "warning",
15
+ target: "$.data.offers[*]['pagefront:configuratorPricing']",
16
+ check: (match, ctx) => {
17
+ const block = asObject(match.value);
18
+ if (block === null || !isComplete(block))
19
+ return null;
20
+ const configurator = resolveConfigurator(block, ctx.sheet);
21
+ if (configurator === null)
22
+ return null;
23
+ const hits = missingModifierProblems(configurator, block).map((p) => p.kind === "no-text-modifier"
24
+ ? {
25
+ values: {
26
+ dimension: p.dimension,
27
+ problem: `states no \`*\` modifier for text dimension \`${p.dimension}\``,
28
+ },
29
+ }
30
+ : p.kind === "no-options"
31
+ ? {
32
+ values: {
33
+ dimension: p.dimension,
34
+ problem: `cannot state modifiers for \`${p.dimension}\`, a priced dimension that enumerates no options`,
35
+ },
36
+ }
37
+ : {
38
+ values: {
39
+ dimension: p.dimension,
40
+ option: p.option,
41
+ problem: `states no modifier for option \`${p.option}\` of \`${p.dimension}\``,
42
+ },
43
+ });
44
+ return hits.length > 0 ? hits : null;
45
+ },
46
+ messageTemplate: "Pricing block at `{path}` {problem}, and `pricing` is not `partial`.",
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
+ introduced: "v0.9 (2026-10-01 revision)",
49
+ specReference: "product.md#configurator-pricing-and-price-bands",
50
+ };
@@ -0,0 +1,10 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ /**
3
+ * PDP-W-033 — Priced configurator with no offer pricing it.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Fires when the configurator has a priced dimension, no `offers` entry
7
+ * carries a pricing block, and no `offers` entry states a price. The
8
+ * last case is PDP-W-034's; the two never fire on the same Sheet.
9
+ */
10
+ export declare const pdpW033: Rule;
@@ -0,0 +1,30 @@
1
+ import { entryStatesPrice, hasPricedDimension, offerEntries, pricingBlock, readConfigurator, } from "./configurator-pricing.js";
2
+ /**
3
+ * PDP-W-033 — Priced configurator with no offer pricing it.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Fires when the configurator has a priced dimension, no `offers` entry
7
+ * carries a pricing block, and no `offers` entry states a price. The
8
+ * last case is PDP-W-034's; the two never fire on the same Sheet.
9
+ */
10
+ export const pdpW033 = {
11
+ id: "PDP-W-033",
12
+ title: "Priced configurator with no offer pricing it",
13
+ severity: "warning",
14
+ target: "$.data['pagefront:configurator']",
15
+ check: (_match, ctx) => {
16
+ const configurator = readConfigurator(ctx.sheet);
17
+ if (configurator === null || !hasPricedDimension(configurator))
18
+ return null;
19
+ const offers = offerEntries(ctx.sheet);
20
+ if (offers.some((o) => pricingBlock(o) !== null))
21
+ return null;
22
+ if (offers.some(entryStatesPrice))
23
+ return null;
24
+ return {};
25
+ },
26
+ messageTemplate: "Configurator at `{path}` has priced dimensions but no offer prices it.",
27
+ remediation: "Add an offer carrying `pagefront:configuratorPricing` for this configurator. If no option changes the price, declare `affectsPrice: false` on each dimension.",
28
+ introduced: "v0.9 (2026-10-01 revision)",
29
+ specReference: "product.md#configurator-pricing-and-price-bands",
30
+ };
@@ -0,0 +1,10 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ /**
3
+ * PDP-W-034 — Offer beside a configurator with no pricing block.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Fires once per Sheet, at the first `offers` entry that states a
7
+ * price, when the Sheet carries a configurator with a priced dimension
8
+ * and no `offers` entry carries a pricing block.
9
+ */
10
+ export declare const pdpW034: Rule;
@@ -0,0 +1,28 @@
1
+ import { entryStatesPrice, hasPricedDimension, pricingBlock, readConfigurator, } from "./configurator-pricing.js";
2
+ /**
3
+ * PDP-W-034 — Offer beside a configurator with no pricing block.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Fires once per Sheet, at the first `offers` entry that states a
7
+ * price, when the Sheet carries a configurator with a priced dimension
8
+ * and no `offers` entry carries a pricing block.
9
+ */
10
+ export const pdpW034 = {
11
+ id: "PDP-W-034",
12
+ title: "Offer beside a configurator with no pricing block",
13
+ severity: "warning",
14
+ target: "$.data.offers[*]",
15
+ check: (match, ctx) => {
16
+ const configurator = readConfigurator(ctx.sheet);
17
+ if (configurator === null || !hasPricedDimension(configurator))
18
+ return null;
19
+ if (ctx.allMatches.some((m) => pricingBlock(m.value) !== null))
20
+ return null;
21
+ const first = ctx.allMatches.find((m) => entryStatesPrice(m.value));
22
+ return first !== undefined && first.path === match.path ? {} : null;
23
+ },
24
+ messageTemplate: "Offer price is not linked to the configurator; state which configuration it prices.",
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
+ introduced: "v0.9 (2026-10-01 revision)",
27
+ specReference: "product.md#configurator-pricing-and-price-bands",
28
+ };
@@ -0,0 +1,11 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ /**
3
+ * PDP-W-035 — `basePrice` may be a component price.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Heuristic, one finding per pricing block: (a) `basePrice` equals the
7
+ * largest modifier of one dimension's `modifiers` entry, or (b)
8
+ * `basePrice` is below every non-zero modifier of one entry that has at
9
+ * least one.
10
+ */
11
+ export declare const pdpW035: Rule;
@@ -0,0 +1,41 @@
1
+ import { asObject, compareDecimal, isZero, parseDecimal, } from "./configurator-pricing.js";
2
+ /**
3
+ * PDP-W-035 — `basePrice` may be a component price.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Heuristic, one finding per pricing block: (a) `basePrice` equals the
7
+ * largest modifier of one dimension's `modifiers` entry, or (b)
8
+ * `basePrice` is below every non-zero modifier of one entry that has at
9
+ * least one.
10
+ */
11
+ export const pdpW035 = {
12
+ id: "PDP-W-035",
13
+ title: "`basePrice` may be a component price",
14
+ severity: "warning",
15
+ target: "$.data.offers[*]['pagefront:configuratorPricing']",
16
+ check: (match) => {
17
+ const block = asObject(match.value);
18
+ const modifiers = asObject(block?.["modifiers"]);
19
+ const basePrice = parseDecimal(block?.["basePrice"]);
20
+ if (modifiers === null || basePrice === undefined)
21
+ return null;
22
+ for (const entry of Object.values(modifiers)) {
23
+ const amounts = Object.values(asObject(entry) ?? {})
24
+ .map(parseDecimal)
25
+ .filter((d) => d !== undefined);
26
+ if (amounts.length === 0)
27
+ continue;
28
+ const largest = amounts.reduce((a, b) => (compareDecimal(b, a) > 0 ? b : a));
29
+ if (compareDecimal(basePrice, largest) === 0)
30
+ return {};
31
+ const nonZero = amounts.filter((d) => !isZero(d));
32
+ if (nonZero.length > 0 && nonZero.every((d) => compareDecimal(basePrice, d) < 0))
33
+ return {};
34
+ }
35
+ return null;
36
+ },
37
+ messageTemplate: "basePrice may be a component price, not a configured total.",
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
+ introduced: "v0.9 (2026-10-01 revision)",
40
+ specReference: "product.md#configurator-pricing-and-price-bands",
41
+ };
@@ -0,0 +1,2 @@
1
+ import { type Rule } from "@pagefront/lint-core";
2
+ export declare const pdpW036: Rule;
@@ -0,0 +1,31 @@
1
+ import { valueAtPath } from "@pagefront/lint-core";
2
+ import { asObject } from "./configurator-pricing.js";
3
+ /**
4
+ * PDP-W-036 — Option value looks like a display label.
5
+ * Transcribed from commerce/spec/rules.md (normative).
6
+ *
7
+ * Heuristic on string option values: whitespace, an uppercase letter,
8
+ * or a decimal number followed by letters to the end (a unit suffix).
9
+ * Options of a sizing dimension (one declaring `system`) are exempt;
10
+ * PDP-W-037 checks those.
11
+ */
12
+ const UNIT_SUFFIX = /^-?\d+(\.\d+)?\p{L}+$/u;
13
+ export const pdpW036 = {
14
+ id: "PDP-W-036",
15
+ title: "Option value looks like a display label",
16
+ severity: "warning",
17
+ target: "$.data['pagefront:configurator'].dimensions[*].options[*]",
18
+ check: (match, ctx) => {
19
+ const value = asObject(match.value)?.["value"];
20
+ if (typeof value !== "string")
21
+ return null;
22
+ const dimension = asObject(valueAtPath(ctx.sheet, match.path.replace(/\['options'\]\[\d+\]$/, "")));
23
+ if (typeof dimension?.["system"] === "string")
24
+ return null;
25
+ return /\s/.test(value) || /\p{Lu}/u.test(value) || UNIT_SUFFIX.test(value) ? {} : null;
26
+ },
27
+ messageTemplate: "Option value should be a stable identifier; put the label in description.",
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
+ introduced: "v0.9 (2026-10-01 revision)",
30
+ specReference: "product.md#configurator-pricing-and-price-bands",
31
+ };
@@ -0,0 +1,2 @@
1
+ import { type Rule } from "@pagefront/lint-core";
2
+ export declare const pdpW037: Rule;
@@ -0,0 +1,37 @@
1
+ import { valueAtPath } from "@pagefront/lint-core";
2
+ import { asObject } from "./configurator-pricing.js";
3
+ /**
4
+ * PDP-W-037 — Option in a sizing dimension is not a size.
5
+ * Transcribed from commerce/spec/rules.md (normative).
6
+ *
7
+ * Heuristic on string option values of a dimension whose `system` is
8
+ * `UK`, `EU`, `US` or `JP`: the value is written as that system writes
9
+ * a size. Other systems are out of scope.
10
+ */
11
+ const NUMERIC_SIZE = /^\d+(\.\d+|½)?$/;
12
+ const SIZE_IN_SYSTEM = {
13
+ UK: /^[A-Z]\s*½?$/,
14
+ EU: NUMERIC_SIZE,
15
+ US: NUMERIC_SIZE,
16
+ JP: NUMERIC_SIZE,
17
+ };
18
+ export const pdpW037 = {
19
+ id: "PDP-W-037",
20
+ title: "Option in a sizing dimension is not a size",
21
+ severity: "warning",
22
+ target: "$.data['pagefront:configurator'].dimensions[*].options[*]",
23
+ check: (match, ctx) => {
24
+ const value = asObject(match.value)?.["value"];
25
+ if (typeof value !== "string")
26
+ return null;
27
+ const dimension = asObject(valueAtPath(ctx.sheet, match.path.replace(/\['options'\]\[\d+\]$/, "")));
28
+ const system = dimension?.["system"];
29
+ if (typeof system !== "string" || !Object.hasOwn(SIZE_IN_SYSTEM, system))
30
+ return null;
31
+ return SIZE_IN_SYSTEM[system].test(value) ? null : {};
32
+ },
33
+ messageTemplate: "Option does not look like a size in the declared system.",
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
+ introduced: "v0.9 (2026-10-01 revision)",
36
+ specReference: "product.md#configurator-pricing-and-price-bands",
37
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ export declare const pdpW038: Rule;
@@ -0,0 +1,30 @@
1
+ import { asObject } from "./configurator-pricing.js";
2
+ /**
3
+ * PDP-W-038 — Configurator note reads as page copy.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Heuristic on English Sheets (`inLanguage` `en` or `en-…`): the
7
+ * dimension's `note` contains `you`, `your`, `select`, `order` or
8
+ * `contact` as a whole word, delimited by non-letters.
9
+ */
10
+ const PAGE_COPY_WORD = /(?<!\p{L})(you|your|select|order|contact)(?!\p{L})/iu;
11
+ export const pdpW038 = {
12
+ id: "PDP-W-038",
13
+ title: "Configurator note reads as page copy",
14
+ severity: "warning",
15
+ target: "$.data['pagefront:configurator'].dimensions[*]",
16
+ check: (match, ctx) => {
17
+ const inLanguage = asObject(ctx.sheet.data)?.["inLanguage"];
18
+ if (typeof inLanguage !== "string" || !/^en(-|$)/i.test(inLanguage))
19
+ return null;
20
+ const note = asObject(match.value)?.["note"];
21
+ if (typeof note !== "string")
22
+ return null;
23
+ const found = PAGE_COPY_WORD.exec(note);
24
+ return found === null ? null : { values: { word: found[1].toLowerCase() } };
25
+ },
26
+ messageTemplate: "Note reads as page copy; state the fact.",
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
+ introduced: "v0.9 (2026-10-01 revision)",
29
+ specReference: "product.md#configurator-pricing-and-price-bands",
30
+ };
@@ -0,0 +1,10 @@
1
+ import type { Rule } from "@pagefront/lint-core";
2
+ /**
3
+ * PDP-W-039 — Size advice carried as prose only.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Fires when `pagefront:sizeRecommendation` carries a non-empty
7
+ * `description` and no `sizeAdjustment`, and the configurator has a
8
+ * sizing dimension. `{system}` is the first sizing dimension's system.
9
+ */
10
+ export declare const pdpW039: Rule;
@@ -0,0 +1,29 @@
1
+ import { asObject, readConfigurator } from "./configurator-pricing.js";
2
+ /**
3
+ * PDP-W-039 — Size advice carried as prose only.
4
+ * Transcribed from commerce/spec/rules.md (normative).
5
+ *
6
+ * Fires when `pagefront:sizeRecommendation` carries a non-empty
7
+ * `description` and no `sizeAdjustment`, and the configurator has a
8
+ * sizing dimension. `{system}` is the first sizing dimension's system.
9
+ */
10
+ export const pdpW039 = {
11
+ id: "PDP-W-039",
12
+ title: "Size advice carried as prose only",
13
+ severity: "warning",
14
+ target: "$.data['pagefront:sizeRecommendation']",
15
+ check: (match, ctx) => {
16
+ const recommendation = asObject(match.value);
17
+ if (recommendation === null || recommendation["sizeAdjustment"] !== undefined)
18
+ return null;
19
+ const description = recommendation["description"];
20
+ if (typeof description !== "string" || description.trim() === "")
21
+ return null;
22
+ const sizing = (readConfigurator(ctx.sheet)?.dimensions ?? []).find((d) => d.system !== undefined);
23
+ return sizing?.system === undefined ? null : { values: { system: sizing.system } };
24
+ },
25
+ messageTemplate: "Size advice at `{path}` could not be stated in the Sheet's sizing system (`{system}`); it is carried as prose only.",
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
+ introduced: "v0.9 (2026-10-01 revision)",
28
+ specReference: "product.md#size-guidance-pagefrontsizechart-and-pagefrontsizerecommendation",
29
+ };