@pagefront/lint-commerce 0.1.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/LICENSE +202 -0
- package/README.md +118 -0
- package/dist/catalogue.d.ts +9 -0
- package/dist/catalogue.js +43 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +144 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +18 -0
- package/dist/lint-sheet.d.ts +13 -0
- package/dist/lint-sheet.js +25 -0
- package/dist/organization-catalogue.d.ts +11 -0
- package/dist/organization-catalogue.js +21 -0
- package/dist/organization-schema.d.ts +9 -0
- package/dist/organization-schema.js +441 -0
- package/dist/product-schema.d.ts +9 -0
- package/dist/product-schema.js +1853 -0
- package/dist/rules/org-shared.d.ts +21 -0
- package/dist/rules/org-shared.js +44 -0
- package/dist/rules/org-w-001.d.ts +12 -0
- package/dist/rules/org-w-001.js +31 -0
- package/dist/rules/org-w-002.d.ts +12 -0
- package/dist/rules/org-w-002.js +32 -0
- package/dist/rules/org-w-003.d.ts +11 -0
- package/dist/rules/org-w-003.js +30 -0
- package/dist/rules/org-w-004.d.ts +12 -0
- package/dist/rules/org-w-004.js +32 -0
- package/dist/rules/org-w-005.d.ts +2 -0
- package/dist/rules/org-w-005.js +54 -0
- package/dist/rules/org-w-006.d.ts +13 -0
- package/dist/rules/org-w-006.js +32 -0
- package/dist/rules/pdp-e-001.d.ts +2 -0
- package/dist/rules/pdp-e-001.js +42 -0
- package/dist/rules/pdp-e-002.d.ts +13 -0
- package/dist/rules/pdp-e-002.js +33 -0
- package/dist/rules/pdp-i-001.d.ts +2 -0
- package/dist/rules/pdp-i-001.js +28 -0
- package/dist/rules/pdp-w-001.d.ts +12 -0
- package/dist/rules/pdp-w-001.js +22 -0
- package/dist/rules/pdp-w-002.d.ts +13 -0
- package/dist/rules/pdp-w-002.js +23 -0
- package/dist/rules/pdp-w-003.d.ts +12 -0
- package/dist/rules/pdp-w-003.js +22 -0
- package/dist/rules/pdp-w-004.d.ts +11 -0
- package/dist/rules/pdp-w-004.js +30 -0
- package/dist/rules/pdp-w-005.d.ts +18 -0
- package/dist/rules/pdp-w-005.js +44 -0
- package/dist/rules/pdp-w-006.d.ts +10 -0
- package/dist/rules/pdp-w-006.js +31 -0
- package/dist/rules/pdp-w-007.d.ts +9 -0
- package/dist/rules/pdp-w-007.js +29 -0
- package/dist/rules/pdp-w-008.d.ts +19 -0
- package/dist/rules/pdp-w-008.js +43 -0
- package/dist/rules/pdp-w-009.d.ts +9 -0
- package/dist/rules/pdp-w-009.js +29 -0
- package/dist/rules/pdp-w-010.d.ts +24 -0
- package/dist/rules/pdp-w-010.js +85 -0
- package/dist/rules/pdp-w-011.d.ts +2 -0
- package/dist/rules/pdp-w-011.js +44 -0
- package/dist/rules/pdp-w-012.d.ts +18 -0
- package/dist/rules/pdp-w-012.js +51 -0
- package/dist/rules/pdp-w-013.d.ts +13 -0
- package/dist/rules/pdp-w-013.js +25 -0
- package/dist/rules/pdp-w-014.d.ts +26 -0
- package/dist/rules/pdp-w-014.js +69 -0
- package/dist/rules/pdp-w-015.d.ts +10 -0
- package/dist/rules/pdp-w-015.js +33 -0
- package/dist/rules/pdp-w-016.d.ts +9 -0
- package/dist/rules/pdp-w-016.js +30 -0
- package/dist/rules/pdp-w-017.d.ts +2 -0
- package/dist/rules/pdp-w-017.js +55 -0
- package/dist/rules/pdp-w-018.d.ts +2 -0
- package/dist/rules/pdp-w-018.js +32 -0
- package/dist/rules/pdp-w-019.d.ts +14 -0
- package/dist/rules/pdp-w-019.js +49 -0
- package/dist/rules/pdp-w-020.d.ts +2 -0
- package/dist/rules/pdp-w-020.js +64 -0
- package/dist/rules/pdp-w-021.d.ts +2 -0
- package/dist/rules/pdp-w-021.js +53 -0
- package/dist/rules/pdp-w-022.d.ts +11 -0
- package/dist/rules/pdp-w-022.js +25 -0
- package/dist/rules/pdp-w-023.d.ts +2 -0
- package/dist/rules/pdp-w-023.js +39 -0
- package/dist/rules/pdp-w-024.d.ts +24 -0
- package/dist/rules/pdp-w-024.js +58 -0
- package/dist/rules/pdp-w-025.d.ts +2 -0
- package/dist/rules/pdp-w-025.js +35 -0
- package/dist/rules/pdp-w-026.d.ts +10 -0
- package/dist/rules/pdp-w-026.js +31 -0
- package/dist/rules/pdp-w-027.d.ts +14 -0
- package/dist/rules/pdp-w-027.js +47 -0
- package/dist/schema-properties.d.ts +10 -0
- package/dist/schema-properties.js +67 -0
- package/dist/sheet-type.d.ts +16 -0
- package/dist/sheet-type.js +20 -0
- package/dist/validate-and-lint.d.ts +47 -0
- package/dist/validate-and-lint.js +53 -0
- package/dist/validate-organization.d.ts +27 -0
- package/dist/validate-organization.js +30 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +8 -0
- package/package.json +50 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-005 — Material composition percentages don't sum to 100.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* The catalogue targets both `pagefront:materialComposition` and its
|
|
7
|
+
* textile alias `pagefront:fabricComposition`; a Rule carries one
|
|
8
|
+
* JSONPath, expressed here as an RFC 9535 multi-selector bracket. Each
|
|
9
|
+
* match is one composition array. Per the catalogue, the rule does not
|
|
10
|
+
* fire if any entry omits `percentage`; an empty array is read as having
|
|
11
|
+
* no percentage-declaring entries and does not fire (the catalogue's
|
|
12
|
+
* "all entries declare" would be vacuously true on it, but a sum of 0
|
|
13
|
+
* on zero materials is not the transcription-error signal this rule
|
|
14
|
+
* exists for). Per the catalogue, `{sum}` is rendered with at most two
|
|
15
|
+
* decimal places, so floating-point summation noise never reaches the
|
|
16
|
+
* report.
|
|
17
|
+
*/
|
|
18
|
+
export declare const pdpW005: Rule;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDP-W-005 — Material composition percentages don't sum to 100.
|
|
3
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
4
|
+
*
|
|
5
|
+
* The catalogue targets both `pagefront:materialComposition` and its
|
|
6
|
+
* textile alias `pagefront:fabricComposition`; a Rule carries one
|
|
7
|
+
* JSONPath, expressed here as an RFC 9535 multi-selector bracket. Each
|
|
8
|
+
* match is one composition array. Per the catalogue, the rule does not
|
|
9
|
+
* fire if any entry omits `percentage`; an empty array is read as having
|
|
10
|
+
* no percentage-declaring entries and does not fire (the catalogue's
|
|
11
|
+
* "all entries declare" would be vacuously true on it, but a sum of 0
|
|
12
|
+
* on zero materials is not the transcription-error signal this rule
|
|
13
|
+
* exists for). Per the catalogue, `{sum}` is rendered with at most two
|
|
14
|
+
* decimal places, so floating-point summation noise never reaches the
|
|
15
|
+
* report.
|
|
16
|
+
*/
|
|
17
|
+
export const pdpW005 = {
|
|
18
|
+
id: "PDP-W-005",
|
|
19
|
+
title: "Material composition percentages don't sum to 100",
|
|
20
|
+
severity: "warning",
|
|
21
|
+
target: "$.data['pagefront:materialComposition','pagefront:fabricComposition']",
|
|
22
|
+
check: (match) => {
|
|
23
|
+
const entries = match.value;
|
|
24
|
+
if (!Array.isArray(entries) || entries.length === 0)
|
|
25
|
+
return null;
|
|
26
|
+
const percentages = [];
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const p = typeof entry === "object" && entry !== null
|
|
29
|
+
? entry["percentage"]
|
|
30
|
+
: undefined;
|
|
31
|
+
if (typeof p !== "number")
|
|
32
|
+
return null;
|
|
33
|
+
percentages.push(p);
|
|
34
|
+
}
|
|
35
|
+
const sum = percentages.reduce((a, b) => a + b, 0);
|
|
36
|
+
return sum >= 99.5 && sum <= 100.5
|
|
37
|
+
? null
|
|
38
|
+
: { values: { sum: Math.round(sum * 100) / 100 } };
|
|
39
|
+
},
|
|
40
|
+
messageTemplate: "Material composition percentages at `{path}` sum to `{sum}`, not approximately 100.",
|
|
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
|
+
introduced: "v0.6",
|
|
43
|
+
specReference: "product.md#materials-material-and-pagefrontmaterialcomposition",
|
|
44
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-006 — Certification missing scope.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Bespoke check: fires when the certification's `additionalProperty`
|
|
7
|
+
* array has no entry with `name: "scope"`. `{name}` is the
|
|
8
|
+
* certification's `name`, which the schema requires on every entry.
|
|
9
|
+
*/
|
|
10
|
+
export declare const pdpW006: Rule;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDP-W-006 — Certification missing scope.
|
|
3
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
4
|
+
*
|
|
5
|
+
* Bespoke check: fires when the certification's `additionalProperty`
|
|
6
|
+
* array has no entry with `name: "scope"`. `{name}` is the
|
|
7
|
+
* certification's `name`, which the schema requires on every entry.
|
|
8
|
+
*/
|
|
9
|
+
export const pdpW006 = {
|
|
10
|
+
id: "PDP-W-006",
|
|
11
|
+
title: "Certification missing scope",
|
|
12
|
+
severity: "warning",
|
|
13
|
+
target: "$.data.hasCertification[*]",
|
|
14
|
+
check: (match) => {
|
|
15
|
+
const cert = match.value;
|
|
16
|
+
if (typeof cert !== "object" || cert === null)
|
|
17
|
+
return null;
|
|
18
|
+
const c = cert;
|
|
19
|
+
const props = Array.isArray(c["additionalProperty"])
|
|
20
|
+
? c["additionalProperty"]
|
|
21
|
+
: [];
|
|
22
|
+
const hasScope = props.some((p) => typeof p === "object" &&
|
|
23
|
+
p !== null &&
|
|
24
|
+
p["name"] === "scope");
|
|
25
|
+
return hasScope ? null : { values: { name: String(c["name"]) } };
|
|
26
|
+
},
|
|
27
|
+
messageTemplate: "Certification `{name}` at `{path}` has no scope declared.",
|
|
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
|
+
introduced: "v0.6",
|
|
30
|
+
specReference: "vocabulary.md#certification-scope",
|
|
31
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-007 — ProductGroup with single variant.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Bespoke cross-field check: `@type` is `ProductGroup` and `hasVariant`
|
|
7
|
+
* has exactly one entry.
|
|
8
|
+
*/
|
|
9
|
+
export declare const pdpW007: Rule;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDP-W-007 — ProductGroup with single variant.
|
|
3
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
4
|
+
*
|
|
5
|
+
* Bespoke cross-field check: `@type` is `ProductGroup` and `hasVariant`
|
|
6
|
+
* has exactly one entry.
|
|
7
|
+
*/
|
|
8
|
+
export const pdpW007 = {
|
|
9
|
+
id: "PDP-W-007",
|
|
10
|
+
title: "ProductGroup with single variant",
|
|
11
|
+
severity: "warning",
|
|
12
|
+
target: "$.data",
|
|
13
|
+
check: (match) => {
|
|
14
|
+
const data = match.value;
|
|
15
|
+
if (typeof data !== "object" || data === null)
|
|
16
|
+
return null;
|
|
17
|
+
const d = data;
|
|
18
|
+
const variants = d["hasVariant"];
|
|
19
|
+
return d["@type"] === "ProductGroup" &&
|
|
20
|
+
Array.isArray(variants) &&
|
|
21
|
+
variants.length === 1
|
|
22
|
+
? {}
|
|
23
|
+
: null;
|
|
24
|
+
},
|
|
25
|
+
messageTemplate: "ProductGroup at `{path}` has only one variant.",
|
|
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
|
+
introduced: "v0.6",
|
|
28
|
+
specReference: "product.md#configurator-vs-productgroup-guidance",
|
|
29
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-008 — Sheet expires_at is past or trivially close to generated_at.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative), with two
|
|
5
|
+
* reductions the catalogue text leaves open:
|
|
6
|
+
*
|
|
7
|
+
* - The catalogue names both `$.expires_at` and `$.generated_at` as the
|
|
8
|
+
* target; a Rule carries one JSONPath. The target here is
|
|
9
|
+
* `$.expires_at` — the rule cannot fire without it (the field is
|
|
10
|
+
* optional) — and `generated_at` is read via RuleContext.
|
|
11
|
+
* - The catalogue message's `{past | trivially close to generated_at}`
|
|
12
|
+
* prose alternation is realized as the `{reason}` placeholder carrying
|
|
13
|
+
* one of those two literal texts; when both conditions hold, `past`
|
|
14
|
+
* wins (the catalogue's order).
|
|
15
|
+
*
|
|
16
|
+
* "The time of linting" is the run's `checked_at` (ctx.checkedAt), not
|
|
17
|
+
* the wall clock, so fixture output stays deterministic.
|
|
18
|
+
*/
|
|
19
|
+
export declare const pdpW008: Rule;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDP-W-008 — Sheet expires_at is past or trivially close to generated_at.
|
|
3
|
+
* Transcribed from commerce/spec/rules.md (normative), with two
|
|
4
|
+
* reductions the catalogue text leaves open:
|
|
5
|
+
*
|
|
6
|
+
* - The catalogue names both `$.expires_at` and `$.generated_at` as the
|
|
7
|
+
* target; a Rule carries one JSONPath. The target here is
|
|
8
|
+
* `$.expires_at` — the rule cannot fire without it (the field is
|
|
9
|
+
* optional) — and `generated_at` is read via RuleContext.
|
|
10
|
+
* - The catalogue message's `{past | trivially close to generated_at}`
|
|
11
|
+
* prose alternation is realized as the `{reason}` placeholder carrying
|
|
12
|
+
* one of those two literal texts; when both conditions hold, `past`
|
|
13
|
+
* wins (the catalogue's order).
|
|
14
|
+
*
|
|
15
|
+
* "The time of linting" is the run's `checked_at` (ctx.checkedAt), not
|
|
16
|
+
* the wall clock, so fixture output stays deterministic.
|
|
17
|
+
*/
|
|
18
|
+
export const pdpW008 = {
|
|
19
|
+
id: "PDP-W-008",
|
|
20
|
+
title: "Sheet expires_at is past or trivially close to generated_at",
|
|
21
|
+
severity: "warning",
|
|
22
|
+
target: "$.expires_at",
|
|
23
|
+
check: (match, ctx) => {
|
|
24
|
+
const expiresAt = Date.parse(String(match.value));
|
|
25
|
+
if (Number.isNaN(expiresAt))
|
|
26
|
+
return null;
|
|
27
|
+
const generatedAt = Date.parse(String(ctx.sheet.generated_at));
|
|
28
|
+
const past = expiresAt < Date.parse(ctx.checkedAt);
|
|
29
|
+
const close = !Number.isNaN(generatedAt) && expiresAt - generatedAt < 60_000;
|
|
30
|
+
if (!past && !close)
|
|
31
|
+
return null;
|
|
32
|
+
return {
|
|
33
|
+
values: {
|
|
34
|
+
expires_at: String(match.value),
|
|
35
|
+
reason: past ? "past" : "trivially close to generated_at",
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
messageTemplate: "Sheet `expires_at` is `{expires_at}`, which is {reason}.",
|
|
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
|
+
introduced: "v0.6",
|
|
42
|
+
specReference: "product.md#the-envelope",
|
|
43
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-009 — Empty hasVariant.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Bespoke cross-field check: `@type` is `ProductGroup` and `hasVariant`
|
|
7
|
+
* is absent or an empty array.
|
|
8
|
+
*/
|
|
9
|
+
export declare const pdpW009: Rule;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDP-W-009 — Empty hasVariant.
|
|
3
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
4
|
+
*
|
|
5
|
+
* Bespoke cross-field check: `@type` is `ProductGroup` and `hasVariant`
|
|
6
|
+
* is absent or an empty array.
|
|
7
|
+
*/
|
|
8
|
+
export const pdpW009 = {
|
|
9
|
+
id: "PDP-W-009",
|
|
10
|
+
title: "Empty hasVariant",
|
|
11
|
+
severity: "warning",
|
|
12
|
+
target: "$.data",
|
|
13
|
+
check: (match) => {
|
|
14
|
+
const data = match.value;
|
|
15
|
+
if (typeof data !== "object" || data === null)
|
|
16
|
+
return null;
|
|
17
|
+
const d = data;
|
|
18
|
+
const variants = d["hasVariant"];
|
|
19
|
+
return d["@type"] === "ProductGroup" &&
|
|
20
|
+
(variants === undefined ||
|
|
21
|
+
(Array.isArray(variants) && variants.length === 0))
|
|
22
|
+
? {}
|
|
23
|
+
: null;
|
|
24
|
+
},
|
|
25
|
+
messageTemplate: "ProductGroup at `{path}` has no variants.",
|
|
26
|
+
remediation: "Add variant entries to `hasVariant`. If the product genuinely has no variants, change `@type` to `Product`.",
|
|
27
|
+
introduced: "v0.6",
|
|
28
|
+
specReference: "product.md#configurator-vs-productgroup-guidance",
|
|
29
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-010 — Configurator and isVariantOf both populated for same dimension.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative), with two
|
|
5
|
+
* reductions:
|
|
6
|
+
*
|
|
7
|
+
* - The catalogue's target is `$.data`, but its message is per colliding
|
|
8
|
+
* dimension (`{dimension}`, `{configurator_path}`, `{variant_path}`),
|
|
9
|
+
* and a check yields at most one finding per match. The target here is
|
|
10
|
+
* each configurator dimension, so every colliding dimension produces
|
|
11
|
+
* its own finding — the per-dimension reading the message implies.
|
|
12
|
+
* - "A property declared on a variant" is a direct property of the
|
|
13
|
+
* variant entry, or an `additionalProperty` entry whose `propertyID`
|
|
14
|
+
* matches the dimension — or, when the entry carries no `propertyID`,
|
|
15
|
+
* whose `name` matches (lenient fallback for content predating the
|
|
16
|
+
* convention; `propertyID` is the language-neutral coordination key,
|
|
17
|
+
* `name` the possibly-localized display label). An entry whose
|
|
18
|
+
* `propertyID` names a *different* dimension never falls back to
|
|
19
|
+
* `name`.
|
|
20
|
+
*
|
|
21
|
+
* `{variant_path}` is the first matching variant entry's normalized
|
|
22
|
+
* path, built in the engine's path style.
|
|
23
|
+
*/
|
|
24
|
+
export declare const pdpW010: Rule;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDP-W-010 — Configurator and isVariantOf both populated for same dimension.
|
|
3
|
+
* Transcribed from commerce/spec/rules.md (normative), with two
|
|
4
|
+
* reductions:
|
|
5
|
+
*
|
|
6
|
+
* - The catalogue's target is `$.data`, but its message is per colliding
|
|
7
|
+
* dimension (`{dimension}`, `{configurator_path}`, `{variant_path}`),
|
|
8
|
+
* and a check yields at most one finding per match. The target here is
|
|
9
|
+
* each configurator dimension, so every colliding dimension produces
|
|
10
|
+
* its own finding — the per-dimension reading the message implies.
|
|
11
|
+
* - "A property declared on a variant" is a direct property of the
|
|
12
|
+
* variant entry, or an `additionalProperty` entry whose `propertyID`
|
|
13
|
+
* matches the dimension — or, when the entry carries no `propertyID`,
|
|
14
|
+
* whose `name` matches (lenient fallback for content predating the
|
|
15
|
+
* convention; `propertyID` is the language-neutral coordination key,
|
|
16
|
+
* `name` the possibly-localized display label). An entry whose
|
|
17
|
+
* `propertyID` names a *different* dimension never falls back to
|
|
18
|
+
* `name`.
|
|
19
|
+
*
|
|
20
|
+
* `{variant_path}` is the first matching variant entry's normalized
|
|
21
|
+
* path, built in the engine's path style.
|
|
22
|
+
*/
|
|
23
|
+
export const pdpW010 = {
|
|
24
|
+
id: "PDP-W-010",
|
|
25
|
+
title: "Configurator and isVariantOf both populated for same dimension",
|
|
26
|
+
severity: "warning",
|
|
27
|
+
target: "$.data['pagefront:configurator'].dimensions[*]",
|
|
28
|
+
check: (match, ctx) => {
|
|
29
|
+
const dim = match.value;
|
|
30
|
+
if (typeof dim !== "object" || dim === null)
|
|
31
|
+
return null;
|
|
32
|
+
const name = dim["name"];
|
|
33
|
+
if (typeof name !== "string")
|
|
34
|
+
return null;
|
|
35
|
+
const data = typeof ctx.sheet.data === "object" && ctx.sheet.data !== null
|
|
36
|
+
? ctx.sheet.data
|
|
37
|
+
: {};
|
|
38
|
+
let variants;
|
|
39
|
+
let variantBase;
|
|
40
|
+
if (data["@type"] === "ProductGroup") {
|
|
41
|
+
variants = data["hasVariant"];
|
|
42
|
+
variantBase = "$['data']['hasVariant']";
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const parent = data["isVariantOf"];
|
|
46
|
+
variants =
|
|
47
|
+
typeof parent === "object" && parent !== null
|
|
48
|
+
? parent["hasVariant"]
|
|
49
|
+
: undefined;
|
|
50
|
+
variantBase = "$['data']['isVariantOf']['hasVariant']";
|
|
51
|
+
}
|
|
52
|
+
if (!Array.isArray(variants) || variants.length === 0)
|
|
53
|
+
return null;
|
|
54
|
+
for (let i = 0; i < variants.length; i++) {
|
|
55
|
+
const variant = variants[i];
|
|
56
|
+
if (typeof variant !== "object" || variant === null)
|
|
57
|
+
continue;
|
|
58
|
+
const v = variant;
|
|
59
|
+
const direct = v[name] !== undefined;
|
|
60
|
+
const viaProps = Array.isArray(v["additionalProperty"]) &&
|
|
61
|
+
v["additionalProperty"].some((p) => {
|
|
62
|
+
if (typeof p !== "object" || p === null)
|
|
63
|
+
return false;
|
|
64
|
+
const prop = p;
|
|
65
|
+
return prop["propertyID"] !== undefined
|
|
66
|
+
? prop["propertyID"] === name
|
|
67
|
+
: prop["name"] === name;
|
|
68
|
+
});
|
|
69
|
+
if (direct || viaProps) {
|
|
70
|
+
return {
|
|
71
|
+
values: {
|
|
72
|
+
dimension: name,
|
|
73
|
+
configurator_path: match.path,
|
|
74
|
+
variant_path: `${variantBase}[${i}]`,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
},
|
|
81
|
+
messageTemplate: "Dimension `{dimension}` is declared in both `pagefront:configurator` at `{configurator_path}` and as a variant property at `{variant_path}`.",
|
|
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
|
+
introduced: "v0.6",
|
|
84
|
+
specReference: "product.md#configurator-vs-productgroup-guidance",
|
|
85
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDP-W-011 — Multi-language reference without availableLanguages.
|
|
3
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
4
|
+
*
|
|
5
|
+
* The catalogue's "recognizable language segment (`/fr/`, `/en/`,
|
|
6
|
+
* `.fr.`, `.en.`, etc.)" is realized as: a path segment that is a
|
|
7
|
+
* two-letter lowercase code optionally followed by a region subtag
|
|
8
|
+
* (`/fr/`, `/en-GB/`), or a two-letter lowercase dotted label
|
|
9
|
+
* (`.fr.`). Both the canonical URL and the Sheet's `sheet_url` are
|
|
10
|
+
* inspected — the dotted-label form covers language-suffixed Sheet
|
|
11
|
+
* names such as `….en.json` (pass #6, from the case-study run). The
|
|
12
|
+
* catalogue declares the check heuristic and false positives
|
|
13
|
+
* acceptable. "Does not declare" covers both an absent
|
|
14
|
+
* `pagefront:availableLanguages` and an empty array (an empty
|
|
15
|
+
* declaration declares nothing).
|
|
16
|
+
*/
|
|
17
|
+
const LANGUAGE_SEGMENT = /\/[a-z]{2}(?:-[A-Za-z]{2,4})?\/|\.[a-z]{2}\./;
|
|
18
|
+
export const pdpW011 = {
|
|
19
|
+
id: "PDP-W-011",
|
|
20
|
+
title: "Multi-language reference without availableLanguages",
|
|
21
|
+
severity: "warning",
|
|
22
|
+
target: "$.data",
|
|
23
|
+
check: (match, ctx) => {
|
|
24
|
+
const canonicalUrl = ctx.sheet.canonical_url;
|
|
25
|
+
if (typeof canonicalUrl !== "string")
|
|
26
|
+
return null;
|
|
27
|
+
const sheetUrl = ctx.sheet.sheet_url;
|
|
28
|
+
const signal = LANGUAGE_SEGMENT.test(canonicalUrl) ||
|
|
29
|
+
(typeof sheetUrl === "string" && LANGUAGE_SEGMENT.test(sheetUrl));
|
|
30
|
+
if (!signal)
|
|
31
|
+
return null;
|
|
32
|
+
const data = match.value;
|
|
33
|
+
const declared = typeof data === "object" && data !== null
|
|
34
|
+
? data["pagefront:availableLanguages"]
|
|
35
|
+
: undefined;
|
|
36
|
+
if (Array.isArray(declared) && declared.length > 0)
|
|
37
|
+
return null;
|
|
38
|
+
return { values: { canonical_url: canonicalUrl } };
|
|
39
|
+
},
|
|
40
|
+
messageTemplate: "Sheet at `{canonical_url}` appears to be part of a multi-language site but does not declare `pagefront:availableLanguages`.",
|
|
41
|
+
remediation: "Add a `pagefront:availableLanguages` array listing the URLs of sibling sheets in other languages, with their BCP 47 language tags.",
|
|
42
|
+
introduced: "v0.6",
|
|
43
|
+
specReference: "product.md#internationalization",
|
|
44
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-012 — Video missing accessibility metadata.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Severity is context-dependent: warning for videos with role
|
|
7
|
+
* `overview` or `tutorial`, info for other roles — realized as the
|
|
8
|
+
* CheckHit severity override, with the rule's declared severity staying
|
|
9
|
+
* the catalogue's stronger default. Two readings:
|
|
10
|
+
*
|
|
11
|
+
* - The check is the literal "has neither `caption` nor `transcript`"
|
|
12
|
+
* (key presence); a present-but-empty caption does not fire.
|
|
13
|
+
* - The video's role lives in `additionalProperty` (`name: "role"`),
|
|
14
|
+
* mirroring the image-role pattern. A video with no declared role is
|
|
15
|
+
* not overview/tutorial, so it downgrades to info, and `{role}`
|
|
16
|
+
* renders as `none`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const pdpW012: Rule;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDP-W-012 — Video missing accessibility metadata.
|
|
3
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
4
|
+
*
|
|
5
|
+
* Severity is context-dependent: warning for videos with role
|
|
6
|
+
* `overview` or `tutorial`, info for other roles — realized as the
|
|
7
|
+
* CheckHit severity override, with the rule's declared severity staying
|
|
8
|
+
* the catalogue's stronger default. Two readings:
|
|
9
|
+
*
|
|
10
|
+
* - The check is the literal "has neither `caption` nor `transcript`"
|
|
11
|
+
* (key presence); a present-but-empty caption does not fire.
|
|
12
|
+
* - The video's role lives in `additionalProperty` (`name: "role"`),
|
|
13
|
+
* mirroring the image-role pattern. A video with no declared role is
|
|
14
|
+
* not overview/tutorial, so it downgrades to info, and `{role}`
|
|
15
|
+
* renders as `none`.
|
|
16
|
+
*/
|
|
17
|
+
export const pdpW012 = {
|
|
18
|
+
id: "PDP-W-012",
|
|
19
|
+
title: "Video missing accessibility metadata",
|
|
20
|
+
severity: "warning",
|
|
21
|
+
target: "$.data.video[*]",
|
|
22
|
+
check: (match) => {
|
|
23
|
+
const video = match.value;
|
|
24
|
+
if (typeof video !== "object" || video === null)
|
|
25
|
+
return null;
|
|
26
|
+
const v = video;
|
|
27
|
+
if (v["caption"] !== undefined || v["transcript"] !== undefined)
|
|
28
|
+
return null;
|
|
29
|
+
let role = "none";
|
|
30
|
+
if (Array.isArray(v["additionalProperty"])) {
|
|
31
|
+
for (const p of v["additionalProperty"]) {
|
|
32
|
+
if (typeof p === "object" &&
|
|
33
|
+
p !== null &&
|
|
34
|
+
p["name"] === "role" &&
|
|
35
|
+
typeof p["value"] === "string") {
|
|
36
|
+
role = p["value"];
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const isPrimaryRole = role === "overview" || role === "tutorial";
|
|
42
|
+
return {
|
|
43
|
+
values: { role },
|
|
44
|
+
...(isPrimaryRole ? {} : { severity: "info" }),
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
messageTemplate: "Video at `{path}` with role `{role}` has no caption or transcript.",
|
|
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
|
+
introduced: "v0.6",
|
|
50
|
+
specReference: "product.md#accessibility",
|
|
51
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-013 — Legacy HTTP Schema.org URI.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* The catalogue targets "`$..availability`, `$..itemCondition`, and any
|
|
7
|
+
* other Schema.org URI fields" — an open-ended set. Reduced here to
|
|
8
|
+
* `$..*` with a string check: any string value anywhere in the sheet
|
|
9
|
+
* matching `http://schema.org/...` fires, which covers every
|
|
10
|
+
* "Schema.org URI field" by definition of carrying such a value, and
|
|
11
|
+
* cannot fire on anything else.
|
|
12
|
+
*/
|
|
13
|
+
export declare const pdpW013: Rule;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDP-W-013 — Legacy HTTP Schema.org URI.
|
|
3
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
4
|
+
*
|
|
5
|
+
* The catalogue targets "`$..availability`, `$..itemCondition`, and any
|
|
6
|
+
* other Schema.org URI fields" — an open-ended set. Reduced here to
|
|
7
|
+
* `$..*` with a string check: any string value anywhere in the sheet
|
|
8
|
+
* matching `http://schema.org/...` fires, which covers every
|
|
9
|
+
* "Schema.org URI field" by definition of carrying such a value, and
|
|
10
|
+
* cannot fire on anything else.
|
|
11
|
+
*/
|
|
12
|
+
export const pdpW013 = {
|
|
13
|
+
id: "PDP-W-013",
|
|
14
|
+
title: "Legacy HTTP Schema.org URI",
|
|
15
|
+
severity: "warning",
|
|
16
|
+
target: "$..*",
|
|
17
|
+
check: (match) => typeof match.value === "string" &&
|
|
18
|
+
match.value.startsWith("http://schema.org/")
|
|
19
|
+
? { values: { value: match.value } }
|
|
20
|
+
: null,
|
|
21
|
+
messageTemplate: "Legacy HTTP Schema.org URI `{value}` at `{path}`.",
|
|
22
|
+
remediation: "Change `http://schema.org/...` to `https://schema.org/...`. The value is otherwise unchanged.",
|
|
23
|
+
introduced: "v0.6",
|
|
24
|
+
specReference: "product.md#offers",
|
|
25
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-014 — ProductGroup variant URL absent on distinctly-addressable
|
|
4
|
+
* variants. Transcribed from commerce/spec/rules.md (normative), with
|
|
5
|
+
* two reductions:
|
|
6
|
+
*
|
|
7
|
+
* - The catalogue's target is `$.data.hasVariant[*]`, but the check and
|
|
8
|
+
* message are group-level ("ProductGroup at `{path}` has `{count}`
|
|
9
|
+
* variants…", firing when *none* of the entries declare a `url`). The
|
|
10
|
+
* target here is `$.data`, producing the one group-level finding the
|
|
11
|
+
* message describes.
|
|
12
|
+
* - A variant identifier is contained in the group's `url` when a
|
|
13
|
+
* variant's `sku` or bare `@id` token (`#` stripped) appears in it as
|
|
14
|
+
* a whole token — delimited on both sides by one of `/ - _ . ? = & #`
|
|
15
|
+
* or the string's start/end. An identifier appearing only as part of
|
|
16
|
+
* a longer alphanumeric run (sku `12` inside `product-12345`) does
|
|
17
|
+
* not count, so coincidental substrings no longer suppress the
|
|
18
|
+
* warning.
|
|
19
|
+
*
|
|
20
|
+
* Per the catalogue: no fire without a top-level `url` (page-less
|
|
21
|
+
* parent), and the partial-population case belongs to PDP-W-018. Per the
|
|
22
|
+
* pass-#6 editor decision, same-document fragment `@id`s on variants
|
|
23
|
+
* identify but do not address — they neither suppress the rule nor
|
|
24
|
+
* change when it fires; only the message wording acknowledges them.
|
|
25
|
+
*/
|
|
26
|
+
export declare const pdpW014: Rule;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDP-W-014 — ProductGroup variant URL absent on distinctly-addressable
|
|
3
|
+
* variants. Transcribed from commerce/spec/rules.md (normative), with
|
|
4
|
+
* two reductions:
|
|
5
|
+
*
|
|
6
|
+
* - The catalogue's target is `$.data.hasVariant[*]`, but the check and
|
|
7
|
+
* message are group-level ("ProductGroup at `{path}` has `{count}`
|
|
8
|
+
* variants…", firing when *none* of the entries declare a `url`). The
|
|
9
|
+
* target here is `$.data`, producing the one group-level finding the
|
|
10
|
+
* message describes.
|
|
11
|
+
* - A variant identifier is contained in the group's `url` when a
|
|
12
|
+
* variant's `sku` or bare `@id` token (`#` stripped) appears in it as
|
|
13
|
+
* a whole token — delimited on both sides by one of `/ - _ . ? = & #`
|
|
14
|
+
* or the string's start/end. An identifier appearing only as part of
|
|
15
|
+
* a longer alphanumeric run (sku `12` inside `product-12345`) does
|
|
16
|
+
* not count, so coincidental substrings no longer suppress the
|
|
17
|
+
* warning.
|
|
18
|
+
*
|
|
19
|
+
* Per the catalogue: no fire without a top-level `url` (page-less
|
|
20
|
+
* parent), and the partial-population case belongs to PDP-W-018. Per the
|
|
21
|
+
* pass-#6 editor decision, same-document fragment `@id`s on variants
|
|
22
|
+
* identify but do not address — they neither suppress the rule nor
|
|
23
|
+
* change when it fires; only the message wording acknowledges them.
|
|
24
|
+
*/
|
|
25
|
+
export const pdpW014 = {
|
|
26
|
+
id: "PDP-W-014",
|
|
27
|
+
title: "ProductGroup variant URL absent on distinctly-addressable variants",
|
|
28
|
+
severity: "warning",
|
|
29
|
+
target: "$.data",
|
|
30
|
+
check: (match) => {
|
|
31
|
+
const data = match.value;
|
|
32
|
+
if (typeof data !== "object" || data === null)
|
|
33
|
+
return null;
|
|
34
|
+
const d = data;
|
|
35
|
+
if (d["@type"] !== "ProductGroup")
|
|
36
|
+
return null;
|
|
37
|
+
const variants = d["hasVariant"];
|
|
38
|
+
if (!Array.isArray(variants) || variants.length < 2)
|
|
39
|
+
return null;
|
|
40
|
+
const groupUrl = d["url"];
|
|
41
|
+
if (typeof groupUrl !== "string" || groupUrl.trim() === "")
|
|
42
|
+
return null;
|
|
43
|
+
const entries = variants.filter((v) => typeof v === "object" && v !== null);
|
|
44
|
+
const anyUrl = entries.some((v) => typeof v["url"] === "string" && v["url"].trim() !== "");
|
|
45
|
+
if (anyUrl)
|
|
46
|
+
return null;
|
|
47
|
+
const DELIMITER = "[/\\-_.?=&#]";
|
|
48
|
+
const containsAsToken = (url, id) => {
|
|
49
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
50
|
+
return new RegExp(`(?:^|${DELIMITER})${escaped}(?:${DELIMITER}|$)`).test(url);
|
|
51
|
+
};
|
|
52
|
+
const urlContainsVariantId = entries.some((v) => {
|
|
53
|
+
const ids = [
|
|
54
|
+
typeof v["sku"] === "string" ? v["sku"] : undefined,
|
|
55
|
+
typeof v["@id"] === "string"
|
|
56
|
+
? v["@id"].replace(/^#/, "")
|
|
57
|
+
: undefined,
|
|
58
|
+
];
|
|
59
|
+
return ids.some((id) => id !== undefined && id !== "" && containsAsToken(groupUrl, id));
|
|
60
|
+
});
|
|
61
|
+
if (urlContainsVariantId)
|
|
62
|
+
return null;
|
|
63
|
+
return { values: { count: variants.length } };
|
|
64
|
+
},
|
|
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
|
+
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
|
+
introduced: "v0.6",
|
|
68
|
+
specReference: "product.md#per-variant-urls",
|
|
69
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-015 — Price-on-request without contact channel.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Bespoke check: `method` is anything other than `unspecified` and none
|
|
7
|
+
* of `contactUrl`, `contactEmail`, `contactPhone` is declared (key
|
|
8
|
+
* presence — the catalogue's "declares none of").
|
|
9
|
+
*/
|
|
10
|
+
export declare const pdpW015: Rule;
|