@pagefront/lint-commerce 0.8.0 → 0.10.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.
- package/README.md +64 -6
- package/dist/catalogue.js +12 -3
- package/dist/index.d.ts +7 -4
- package/dist/index.js +6 -3
- package/dist/legacy-schemas.d.ts +9 -0
- package/dist/legacy-schemas.js +2446 -0
- package/dist/organization-catalogue.js +6 -3
- package/dist/organization-schema.d.ts +2 -1
- package/dist/organization-schema.js +9 -7
- package/dist/product-schema.d.ts +2 -2
- package/dist/product-schema.js +54 -22
- package/dist/releases.d.ts +36 -0
- package/dist/releases.js +41 -0
- package/dist/rules/configurator-pricing.d.ts +21 -2
- package/dist/rules/configurator-pricing.js +41 -5
- package/dist/rules/org-e-002.d.ts +7 -0
- package/dist/rules/org-e-002.js +13 -0
- package/dist/rules/org-e-003.d.ts +7 -0
- package/dist/rules/org-e-003.js +13 -0
- package/dist/rules/pdp-e-002.d.ts +3 -9
- package/dist/rules/pdp-e-002.js +6 -26
- package/dist/rules/pdp-e-008.js +1 -1
- package/dist/rules/pdp-e-009.d.ts +1 -1
- package/dist/rules/pdp-e-009.js +3 -3
- package/dist/rules/pdp-e-013.d.ts +9 -0
- package/dist/rules/pdp-e-013.js +26 -0
- package/dist/rules/pdp-e-014.d.ts +11 -0
- package/dist/rules/pdp-e-014.js +34 -0
- package/dist/rules/pdp-e-015.d.ts +2 -0
- package/dist/rules/pdp-e-015.js +91 -0
- package/dist/rules/pdp-e-016.d.ts +7 -0
- package/dist/rules/pdp-e-016.js +13 -0
- package/dist/rules/pdp-w-032.d.ts +3 -2
- package/dist/rules/pdp-w-032.js +20 -12
- package/dist/rules/pdp-w-036.d.ts +2 -0
- package/dist/rules/pdp-w-036.js +31 -0
- package/dist/rules/pdp-w-037.d.ts +2 -0
- package/dist/rules/pdp-w-037.js +37 -0
- package/dist/rules/pdp-w-038.d.ts +2 -0
- package/dist/rules/pdp-w-038.js +30 -0
- package/dist/rules/pdp-w-039.d.ts +10 -0
- package/dist/rules/pdp-w-039.js +29 -0
- package/dist/rules/release-schema.d.ts +16 -0
- package/dist/rules/release-schema.js +77 -0
- package/dist/schema-validators.d.ts +2 -0
- package/dist/schema-validators.js +10 -4
- package/dist/sheet-type.d.ts +17 -0
- package/dist/sheet-type.js +25 -0
- package/dist/validate-and-lint.d.ts +9 -5
- package/dist/validate-and-lint.js +17 -9
- package/dist/validate-organization.d.ts +4 -4
- package/dist/validate-organization.js +8 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -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,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,7 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-E-016 — Sheet carries both format_version and release.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative); the check is
|
|
5
|
+
* shared with ORG-E-002 (release-schema.ts).
|
|
6
|
+
*/
|
|
7
|
+
export declare const pdpE016: Rule;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BOTH_VERSION_FIELDS, bothVersionFields } from "./release-schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-E-016 — Sheet carries both format_version and release.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative); the check is
|
|
5
|
+
* shared with ORG-E-002 (release-schema.ts).
|
|
6
|
+
*/
|
|
7
|
+
export const pdpE016 = {
|
|
8
|
+
id: "PDP-E-016",
|
|
9
|
+
...BOTH_VERSION_FIELDS,
|
|
10
|
+
check: bothVersionFields,
|
|
11
|
+
introduced: "release 0.3.0",
|
|
12
|
+
specReference: "product.md#the-envelope",
|
|
13
|
+
};
|
|
@@ -4,7 +4,8 @@ import type { Rule } from "@pagefront/lint-core";
|
|
|
4
4
|
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
5
|
*
|
|
6
6
|
* Applies to a resolving pricing block whose `pricing` is absent or
|
|
7
|
-
* `complete`. One finding per priced option with no modifier,
|
|
8
|
-
*
|
|
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.
|
|
9
10
|
*/
|
|
10
11
|
export declare const pdpW032: Rule;
|
package/dist/rules/pdp-w-032.js
CHANGED
|
@@ -4,8 +4,9 @@ import { asObject, isComplete, missingModifierProblems, resolveConfigurator, } f
|
|
|
4
4
|
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
5
|
*
|
|
6
6
|
* Applies to a resolving pricing block whose `pricing` is absent or
|
|
7
|
-
* `complete`. One finding per priced option with no modifier,
|
|
8
|
-
*
|
|
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.
|
|
9
10
|
*/
|
|
10
11
|
export const pdpW032 = {
|
|
11
12
|
id: "PDP-W-032",
|
|
@@ -19,24 +20,31 @@ export const pdpW032 = {
|
|
|
19
20
|
const configurator = resolveConfigurator(block, ctx.sheet);
|
|
20
21
|
if (configurator === null)
|
|
21
22
|
return null;
|
|
22
|
-
const hits = missingModifierProblems(configurator, block).map((p) => p.kind === "no-
|
|
23
|
+
const hits = missingModifierProblems(configurator, block).map((p) => p.kind === "no-text-modifier"
|
|
23
24
|
? {
|
|
24
25
|
values: {
|
|
25
26
|
dimension: p.dimension,
|
|
26
|
-
problem: `
|
|
27
|
+
problem: `states no \`*\` modifier for text dimension \`${p.dimension}\``,
|
|
27
28
|
},
|
|
28
29
|
}
|
|
29
|
-
:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
});
|
|
36
44
|
return hits.length > 0 ? hits : null;
|
|
37
45
|
},
|
|
38
46
|
messageTemplate: "Pricing block at `{path}` {problem}, and `pricing` is not `partial`.",
|
|
39
|
-
remediation: "State the modifier, `\"
|
|
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\"`.",
|
|
40
48
|
introduced: "v0.9 (2026-10-01 revision)",
|
|
41
49
|
specReference: "product.md#configurator-pricing-and-price-bands",
|
|
42
50
|
};
|
|
@@ -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,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,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
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CheckFn, Rule } from "@pagefront/lint-core";
|
|
2
|
+
import type { SheetType } from "../sheet-type.js";
|
|
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).
|
|
11
|
+
*/
|
|
12
|
+
export declare function releaseSchemaMismatch(sheetType: SheetType): CheckFn;
|
|
13
|
+
export declare const RELEASE_SCHEMA_MISMATCH: Pick<Rule, "title" | "severity" | "target" | "messageTemplate" | "remediation">;
|
|
14
|
+
/** A sheet carrying both `format_version` and `release`. */
|
|
15
|
+
export declare const bothVersionFields: CheckFn;
|
|
16
|
+
export declare const BOTH_VERSION_FIELDS: Pick<Rule, "title" | "severity" | "target" | "messageTemplate" | "remediation">;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { sheetSpecVersionFor } from "../releases.js";
|
|
2
|
+
/**
|
|
3
|
+
* Shared checks for the envelope's version fields, used by the product
|
|
4
|
+
* and organization catalogues (PDP-E-002 / ORG-E-003, PDP-E-016 /
|
|
5
|
+
* ORG-E-002). Transcribed from commerce/spec/rules.md (normative).
|
|
6
|
+
*/
|
|
7
|
+
/** The trailing `v<version>.json` of a `$schema` URL. */
|
|
8
|
+
const SCHEMA_VERSION = /v(\d+(?:\.\d+)*)\.json$/;
|
|
9
|
+
const SHEET_LABEL = {
|
|
10
|
+
product: "Product Sheet",
|
|
11
|
+
organization: "Organization Sheet",
|
|
12
|
+
};
|
|
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).
|
|
21
|
+
*/
|
|
22
|
+
export function releaseSchemaMismatch(sheetType) {
|
|
23
|
+
return (_match, ctx) => {
|
|
24
|
+
const { release, format_version: formatVersion, $schema: schema } = ctx.sheet;
|
|
25
|
+
if (typeof schema !== "string")
|
|
26
|
+
return null;
|
|
27
|
+
const schemaVersion = SCHEMA_VERSION.exec(schema)?.[1];
|
|
28
|
+
if (schemaVersion === undefined)
|
|
29
|
+
return null;
|
|
30
|
+
if (release !== undefined && formatVersion !== undefined)
|
|
31
|
+
return null;
|
|
32
|
+
if (typeof release === "string") {
|
|
33
|
+
const expected = sheetSpecVersionFor(release, sheetType);
|
|
34
|
+
if (expected === undefined || expected === schemaVersion)
|
|
35
|
+
return null;
|
|
36
|
+
return {
|
|
37
|
+
path: "$['release']",
|
|
38
|
+
values: {
|
|
39
|
+
declared: `\`release\` \`${release}\` bundles ${SHEET_LABEL[sheetType]} spec \`${expected}\``,
|
|
40
|
+
release,
|
|
41
|
+
expected,
|
|
42
|
+
schema_version: schemaVersion,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (formatVersion !== undefined) {
|
|
47
|
+
const declared = String(formatVersion);
|
|
48
|
+
if (declared === schemaVersion)
|
|
49
|
+
return null;
|
|
50
|
+
return {
|
|
51
|
+
path: "$['format_version']",
|
|
52
|
+
values: {
|
|
53
|
+
declared: `\`format_version\` is \`${declared}\``,
|
|
54
|
+
format_version: declared,
|
|
55
|
+
schema_version: schemaVersion,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export const RELEASE_SCHEMA_MISMATCH = {
|
|
63
|
+
title: "Release / schema mismatch",
|
|
64
|
+
severity: "error",
|
|
65
|
+
target: "$",
|
|
66
|
+
messageTemplate: "{declared} but `$schema` URL declares version `{schema_version}`.",
|
|
67
|
+
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
|
+
};
|
|
69
|
+
/** A sheet carrying both `format_version` and `release`. */
|
|
70
|
+
export const bothVersionFields = (_match, ctx) => ctx.sheet.release !== undefined && ctx.sheet.format_version !== undefined ? {} : null;
|
|
71
|
+
export const BOTH_VERSION_FIELDS = {
|
|
72
|
+
title: "Sheet carries both format_version and release",
|
|
73
|
+
severity: "error",
|
|
74
|
+
target: "$",
|
|
75
|
+
messageTemplate: "Sheet carries both `format_version` and `release`; a sheet declares one.",
|
|
76
|
+
remediation: "Keep `release` and remove `format_version`. A sheet on the legacy draft line keeps `format_version` alone and its legacy `$schema`.",
|
|
77
|
+
};
|