@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,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the Commerce Organization Sheet rules
|
|
3
|
+
* (ORG-W-001 … ORG-W-006). Kept local to the org rules: the notions
|
|
4
|
+
* here — the role array, the seller-block field set, the catalogue's
|
|
5
|
+
* "empty or whitespace-only" emptiness — are organization-catalogue
|
|
6
|
+
* vocabulary, not engine machinery.
|
|
7
|
+
*/
|
|
8
|
+
/** The Definitions section's "empty or whitespace-only". */
|
|
9
|
+
export declare function isEmptyOrWhitespace(value: unknown): boolean;
|
|
10
|
+
/** A field is "present" when it exists and is not empty per Definitions. */
|
|
11
|
+
export declare function isPresent(data: Record<string, unknown>, field: string): boolean;
|
|
12
|
+
/** The declared commerce roles, as strings; [] when absent or malformed. */
|
|
13
|
+
export declare function commerceRoles(data: Record<string, unknown>): string[];
|
|
14
|
+
export declare function asRecord(value: unknown): Record<string, unknown> | null;
|
|
15
|
+
/**
|
|
16
|
+
* The seller-block fields whose presence implies the `seller` role
|
|
17
|
+
* (ORG-W-004) — deliberately excluding `location`, which a
|
|
18
|
+
* manufacturer legitimately carries. ORG-W-002's role-without-content
|
|
19
|
+
* check reads these plus `location`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SELLER_CONTENT_FIELDS: readonly ["hasMerchantReturnPolicy", "pagefront:fulfillmentTerms", "paymentAccepted"];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the Commerce Organization Sheet rules
|
|
3
|
+
* (ORG-W-001 … ORG-W-006). Kept local to the org rules: the notions
|
|
4
|
+
* here — the role array, the seller-block field set, the catalogue's
|
|
5
|
+
* "empty or whitespace-only" emptiness — are organization-catalogue
|
|
6
|
+
* vocabulary, not engine machinery.
|
|
7
|
+
*/
|
|
8
|
+
/** The Definitions section's "empty or whitespace-only". */
|
|
9
|
+
export function isEmptyOrWhitespace(value) {
|
|
10
|
+
return (value === undefined ||
|
|
11
|
+
value === null ||
|
|
12
|
+
(typeof value === "string" && value.trim() === "") ||
|
|
13
|
+
(Array.isArray(value) && value.length === 0) ||
|
|
14
|
+
(typeof value === "object" &&
|
|
15
|
+
!Array.isArray(value) &&
|
|
16
|
+
Object.keys(value).length === 0));
|
|
17
|
+
}
|
|
18
|
+
/** A field is "present" when it exists and is not empty per Definitions. */
|
|
19
|
+
export function isPresent(data, field) {
|
|
20
|
+
return field in data && !isEmptyOrWhitespace(data[field]);
|
|
21
|
+
}
|
|
22
|
+
/** The declared commerce roles, as strings; [] when absent or malformed. */
|
|
23
|
+
export function commerceRoles(data) {
|
|
24
|
+
const roles = data["pagefront:commerceRole"];
|
|
25
|
+
if (!Array.isArray(roles))
|
|
26
|
+
return [];
|
|
27
|
+
return roles.filter((r) => typeof r === "string");
|
|
28
|
+
}
|
|
29
|
+
export function asRecord(value) {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
31
|
+
? value
|
|
32
|
+
: null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The seller-block fields whose presence implies the `seller` role
|
|
36
|
+
* (ORG-W-004) — deliberately excluding `location`, which a
|
|
37
|
+
* manufacturer legitimately carries. ORG-W-002's role-without-content
|
|
38
|
+
* check reads these plus `location`.
|
|
39
|
+
*/
|
|
40
|
+
export const SELLER_CONTENT_FIELDS = [
|
|
41
|
+
"hasMerchantReturnPolicy",
|
|
42
|
+
"pagefront:fulfillmentTerms",
|
|
43
|
+
"paymentAccepted",
|
|
44
|
+
];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* ORG-W-001 — brand_owner declared without a brand node.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* organization.md: "Each declared role SHOULD have its corresponding
|
|
7
|
+
* content present — a brand_owner SHOULD carry a Brand node." The
|
|
8
|
+
* Brand.@id is the identity Product Sheets resolve via Product.brand,
|
|
9
|
+
* so a brand_owner sheet without one is undereferenceable from the
|
|
10
|
+
* product side.
|
|
11
|
+
*/
|
|
12
|
+
export declare const orgW001: Rule;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { asRecord, commerceRoles, isPresent } from "./org-shared.js";
|
|
2
|
+
/**
|
|
3
|
+
* ORG-W-001 — brand_owner declared without a brand node.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* organization.md: "Each declared role SHOULD have its corresponding
|
|
7
|
+
* content present — a brand_owner SHOULD carry a Brand node." The
|
|
8
|
+
* Brand.@id is the identity Product Sheets resolve via Product.brand,
|
|
9
|
+
* so a brand_owner sheet without one is undereferenceable from the
|
|
10
|
+
* product side.
|
|
11
|
+
*/
|
|
12
|
+
export const orgW001 = {
|
|
13
|
+
id: "ORG-W-001",
|
|
14
|
+
title: "brand_owner declared without a brand node",
|
|
15
|
+
severity: "warning",
|
|
16
|
+
target: "$.data",
|
|
17
|
+
check: (match) => {
|
|
18
|
+
const data = asRecord(match.value);
|
|
19
|
+
if (data === null)
|
|
20
|
+
return null;
|
|
21
|
+
if (!commerceRoles(data).includes("brand_owner"))
|
|
22
|
+
return null;
|
|
23
|
+
if (isPresent(data, "brand"))
|
|
24
|
+
return null;
|
|
25
|
+
return {};
|
|
26
|
+
},
|
|
27
|
+
messageTemplate: "`pagefront:commerceRole` at `{path}` declares `brand_owner`, but the sheet carries no `brand` node.",
|
|
28
|
+
remediation: "Add the `Brand` node with `@id` and `name` — the `Brand.@id` is the identity Product Sheets resolve via `Product.brand`.",
|
|
29
|
+
introduced: "v0.9",
|
|
30
|
+
specReference: "organization.md#role-discriminator--pagefrontcommercerole",
|
|
31
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* ORG-W-002 — seller declared without seller content.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* A `seller` role promises the store-wide terms an agent needs before
|
|
7
|
+
* it reaches a specific offer; a sheet declaring the role with none of
|
|
8
|
+
* them present is a promise with no content. `location` counts here
|
|
9
|
+
* (a physical store is seller content) even though ORG-W-004's inverse
|
|
10
|
+
* deliberately excludes it.
|
|
11
|
+
*/
|
|
12
|
+
export declare const orgW002: Rule;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { asRecord, commerceRoles, isPresent, SELLER_CONTENT_FIELDS, } from "./org-shared.js";
|
|
2
|
+
/**
|
|
3
|
+
* ORG-W-002 — seller declared without seller content.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* A `seller` role promises the store-wide terms an agent needs before
|
|
7
|
+
* it reaches a specific offer; a sheet declaring the role with none of
|
|
8
|
+
* them present is a promise with no content. `location` counts here
|
|
9
|
+
* (a physical store is seller content) even though ORG-W-004's inverse
|
|
10
|
+
* deliberately excludes it.
|
|
11
|
+
*/
|
|
12
|
+
export const orgW002 = {
|
|
13
|
+
id: "ORG-W-002",
|
|
14
|
+
title: "seller declared without seller content",
|
|
15
|
+
severity: "warning",
|
|
16
|
+
target: "$.data",
|
|
17
|
+
check: (match) => {
|
|
18
|
+
const data = asRecord(match.value);
|
|
19
|
+
if (data === null)
|
|
20
|
+
return null;
|
|
21
|
+
if (!commerceRoles(data).includes("seller"))
|
|
22
|
+
return null;
|
|
23
|
+
const sellerFields = [...SELLER_CONTENT_FIELDS, "location"];
|
|
24
|
+
if (sellerFields.some((field) => isPresent(data, field)))
|
|
25
|
+
return null;
|
|
26
|
+
return {};
|
|
27
|
+
},
|
|
28
|
+
messageTemplate: "`pagefront:commerceRole` at `{path}` declares `seller`, but none of `hasMerchantReturnPolicy`, `pagefront:fulfillmentTerms`, `paymentAccepted`, or `location` is present.",
|
|
29
|
+
remediation: "Add the store-wide terms the seller role promises: the return policy, fulfillment terms, payment methods, or physical locations.",
|
|
30
|
+
introduced: "v0.9",
|
|
31
|
+
specReference: "organization.md#seller-block-seller",
|
|
32
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* ORG-W-003 — Brand node without brand_owner role.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* organization.md: "content SHOULD NOT appear without its role
|
|
7
|
+
* declared." A `brand` node on a sheet not claiming `brand_owner`
|
|
8
|
+
* either means the role declaration is incomplete or the node belongs
|
|
9
|
+
* on the brand's own sheet.
|
|
10
|
+
*/
|
|
11
|
+
export declare const orgW003: Rule;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { asRecord, commerceRoles, isPresent } from "./org-shared.js";
|
|
2
|
+
/**
|
|
3
|
+
* ORG-W-003 — Brand node without brand_owner role.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* organization.md: "content SHOULD NOT appear without its role
|
|
7
|
+
* declared." A `brand` node on a sheet not claiming `brand_owner`
|
|
8
|
+
* either means the role declaration is incomplete or the node belongs
|
|
9
|
+
* on the brand's own sheet.
|
|
10
|
+
*/
|
|
11
|
+
export const orgW003 = {
|
|
12
|
+
id: "ORG-W-003",
|
|
13
|
+
title: "Brand node without brand_owner role",
|
|
14
|
+
severity: "warning",
|
|
15
|
+
target: "$.data",
|
|
16
|
+
check: (match) => {
|
|
17
|
+
const data = asRecord(match.value);
|
|
18
|
+
if (data === null)
|
|
19
|
+
return null;
|
|
20
|
+
if (!isPresent(data, "brand"))
|
|
21
|
+
return null;
|
|
22
|
+
if (commerceRoles(data).includes("brand_owner"))
|
|
23
|
+
return null;
|
|
24
|
+
return {};
|
|
25
|
+
},
|
|
26
|
+
messageTemplate: "A `brand` node is present at `{path}`, but `pagefront:commerceRole` does not declare `brand_owner`.",
|
|
27
|
+
remediation: "Declare `brand_owner` in `pagefront:commerceRole`, or remove the `brand` node if this sheet is not the brand's sheet.",
|
|
28
|
+
introduced: "v0.9",
|
|
29
|
+
specReference: "organization.md#role-discriminator--pagefrontcommercerole",
|
|
30
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* ORG-W-004 — seller content without seller role.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* The inverse of ORG-W-002. `location` is deliberately not in this
|
|
7
|
+
* rule's trigger set: a manufacturer legitimately declares its
|
|
8
|
+
* facilities without being a seller. Store-wide commercial terms —
|
|
9
|
+
* return policy, fulfillment terms, payment methods — are what imply
|
|
10
|
+
* the role.
|
|
11
|
+
*/
|
|
12
|
+
export declare const orgW004: Rule;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { asRecord, commerceRoles, isPresent, SELLER_CONTENT_FIELDS, } from "./org-shared.js";
|
|
2
|
+
/**
|
|
3
|
+
* ORG-W-004 — seller content without seller role.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* The inverse of ORG-W-002. `location` is deliberately not in this
|
|
7
|
+
* rule's trigger set: a manufacturer legitimately declares its
|
|
8
|
+
* facilities without being a seller. Store-wide commercial terms —
|
|
9
|
+
* return policy, fulfillment terms, payment methods — are what imply
|
|
10
|
+
* the role.
|
|
11
|
+
*/
|
|
12
|
+
export const orgW004 = {
|
|
13
|
+
id: "ORG-W-004",
|
|
14
|
+
title: "Seller content without seller role",
|
|
15
|
+
severity: "warning",
|
|
16
|
+
target: "$.data",
|
|
17
|
+
check: (match) => {
|
|
18
|
+
const data = asRecord(match.value);
|
|
19
|
+
if (data === null)
|
|
20
|
+
return null;
|
|
21
|
+
if (commerceRoles(data).includes("seller"))
|
|
22
|
+
return null;
|
|
23
|
+
const present = SELLER_CONTENT_FIELDS.filter((field) => isPresent(data, field));
|
|
24
|
+
if (present.length === 0)
|
|
25
|
+
return null;
|
|
26
|
+
return { values: { fields: present.join("`, `") } };
|
|
27
|
+
},
|
|
28
|
+
messageTemplate: "Seller content (`{fields}`) is present at `{path}`, but `pagefront:commerceRole` does not declare `seller`.",
|
|
29
|
+
remediation: "Declare `seller` in `pagefront:commerceRole`, or move the store-wide terms to the seller's own sheet.",
|
|
30
|
+
introduced: "v0.9",
|
|
31
|
+
specReference: "organization.md#seller-block-seller",
|
|
32
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { asRecord } from "./org-shared.js";
|
|
2
|
+
/**
|
|
3
|
+
* ORG-W-005 — malformed registration machine key.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Checks PRESENT values only. organization.md's registration-identity
|
|
7
|
+
* rules forbid warning on absent registration identifiers — absence is
|
|
8
|
+
* jurisdiction-dependent, not a quality signal — so this rule (and the
|
|
9
|
+
* catalogue as a whole) has no presence check for `leiCode`,
|
|
10
|
+
* `iso6523Code`, `identifier`, or `sameAs`. Non-string values are
|
|
11
|
+
* Layer 1's concern (the schema types both fields as strings) and are
|
|
12
|
+
* skipped here.
|
|
13
|
+
*/
|
|
14
|
+
const ISO6523 = /^\d{4}:.+$/;
|
|
15
|
+
const LEI = /^[A-Z0-9]{18}[0-9]{2}$/;
|
|
16
|
+
export const orgW005 = {
|
|
17
|
+
id: "ORG-W-005",
|
|
18
|
+
title: "Malformed registration machine key",
|
|
19
|
+
severity: "warning",
|
|
20
|
+
target: "$.data",
|
|
21
|
+
check: (match) => {
|
|
22
|
+
const data = asRecord(match.value);
|
|
23
|
+
if (data === null)
|
|
24
|
+
return null;
|
|
25
|
+
const hits = [];
|
|
26
|
+
const iso = data["iso6523Code"];
|
|
27
|
+
if (typeof iso === "string" && !ISO6523.test(iso)) {
|
|
28
|
+
hits.push({
|
|
29
|
+
values: {
|
|
30
|
+
field: "iso6523Code",
|
|
31
|
+
value: iso,
|
|
32
|
+
form: "ISO 6523 shape (four-digit ICD, colon, register number)",
|
|
33
|
+
},
|
|
34
|
+
path: `${match.path}['iso6523Code']`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const lei = data["leiCode"];
|
|
38
|
+
if (typeof lei === "string" && !LEI.test(lei)) {
|
|
39
|
+
hits.push({
|
|
40
|
+
values: {
|
|
41
|
+
field: "leiCode",
|
|
42
|
+
value: lei,
|
|
43
|
+
form: "ISO 17442 shape (18 alphanumerics plus 2 check digits)",
|
|
44
|
+
},
|
|
45
|
+
path: `${match.path}['leiCode']`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return hits.length > 0 ? hits : null;
|
|
49
|
+
},
|
|
50
|
+
messageTemplate: "`{field}` at `{path}` carries `{value}`, which does not match the {form}.",
|
|
51
|
+
remediation: "Correct the machine key: `iso6523Code` is `NNNN:register-number` (e.g. `0002:808641435` for a SIREN), `leiCode` is a 20-character ISO 17442 LEI. If no well-formed machine key exists for this register, drop the field and keep the display-form `identifier` — absence is conformant.",
|
|
52
|
+
introduced: "v0.9",
|
|
53
|
+
specReference: "organization.md#company-registration-identity",
|
|
54
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* ORG-W-006 — authorized resellers without brand_owner role.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Content-without-role, in the ORG-W-003/ORG-W-004 family.
|
|
7
|
+
* `pagefront:authorizedResellers` is the brand exercising authority
|
|
8
|
+
* over its own distribution, so organization.md admits it only where
|
|
9
|
+
* `commerceRole` includes `brand_owner`; a reseller's own claim of
|
|
10
|
+
* authorization carries no weight. Presence-only: no rule warns on the
|
|
11
|
+
* field's absence — absence is no statement.
|
|
12
|
+
*/
|
|
13
|
+
export declare const orgW006: Rule;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { asRecord, commerceRoles, isPresent } from "./org-shared.js";
|
|
2
|
+
/**
|
|
3
|
+
* ORG-W-006 — authorized resellers without brand_owner role.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Content-without-role, in the ORG-W-003/ORG-W-004 family.
|
|
7
|
+
* `pagefront:authorizedResellers` is the brand exercising authority
|
|
8
|
+
* over its own distribution, so organization.md admits it only where
|
|
9
|
+
* `commerceRole` includes `brand_owner`; a reseller's own claim of
|
|
10
|
+
* authorization carries no weight. Presence-only: no rule warns on the
|
|
11
|
+
* field's absence — absence is no statement.
|
|
12
|
+
*/
|
|
13
|
+
export const orgW006 = {
|
|
14
|
+
id: "ORG-W-006",
|
|
15
|
+
title: "Authorized resellers without brand_owner role",
|
|
16
|
+
severity: "warning",
|
|
17
|
+
target: "$.data",
|
|
18
|
+
check: (match) => {
|
|
19
|
+
const data = asRecord(match.value);
|
|
20
|
+
if (data === null)
|
|
21
|
+
return null;
|
|
22
|
+
if (!isPresent(data, "pagefront:authorizedResellers"))
|
|
23
|
+
return null;
|
|
24
|
+
if (commerceRoles(data).includes("brand_owner"))
|
|
25
|
+
return null;
|
|
26
|
+
return {};
|
|
27
|
+
},
|
|
28
|
+
messageTemplate: "`pagefront:authorizedResellers` is present at `{path}`, but `pagefront:commerceRole` does not declare `brand_owner`.",
|
|
29
|
+
remediation: "Declare `brand_owner` in `pagefront:commerceRole` if this is the brand's own sheet; otherwise remove the field — only the brand's sheet can state whom the brand authorizes.",
|
|
30
|
+
introduced: "v0.9",
|
|
31
|
+
specReference: "organization.md#authorized-resellers",
|
|
32
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { collectDeclaredIds, isInlineDeclaration, isSameDocumentId, valueAtPath, } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-E-001 — Broken @id reference.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* The target matches every `@id` in the Sheet; the check fires only on
|
|
7
|
+
* *reference-form* usages (Definitions) inside the schema-derived
|
|
8
|
+
* reference-carrying fields (`itemOffered` on `offers` entries,
|
|
9
|
+
* `isVariantOf`), detected from the match path. The form test examines
|
|
10
|
+
* the object carrying the matched `@id` (parent of the match): an
|
|
11
|
+
* inline declaration there is not a reference — it declares its `@id`
|
|
12
|
+
* into the resolution candidate set instead (`collectDeclaredIds`, the
|
|
13
|
+
* form-based machinery shared with PDP-W-020). For a reference-form entry,
|
|
14
|
+
* a same-document `@id` (Definitions) must resolve by exact string
|
|
15
|
+
* match; only a non-same-document full URL is a cross-Sheet reference,
|
|
16
|
+
* exempt from internal resolution.
|
|
17
|
+
*/
|
|
18
|
+
const REFERENCE_SEGMENT = /\['itemOffered'\]|\['isVariantOf'\]/;
|
|
19
|
+
export const pdpE001 = {
|
|
20
|
+
id: "PDP-E-001",
|
|
21
|
+
title: "Broken @id reference",
|
|
22
|
+
severity: "error",
|
|
23
|
+
target: "$..['@id']",
|
|
24
|
+
check: (match, ctx) => {
|
|
25
|
+
if (!REFERENCE_SEGMENT.test(match.path))
|
|
26
|
+
return null;
|
|
27
|
+
const value = match.value;
|
|
28
|
+
if (typeof value !== "string")
|
|
29
|
+
return null;
|
|
30
|
+
const carrier = valueAtPath(ctx.sheet, match.path.replace(/\['@id'\]$/, ""));
|
|
31
|
+
if (isInlineDeclaration(carrier))
|
|
32
|
+
return null; // declares, not references
|
|
33
|
+
if (!isSameDocumentId(value, ctx.sheet))
|
|
34
|
+
return null; // cross-Sheet: URL shape is given
|
|
35
|
+
const resolves = collectDeclaredIds(ctx.sheet.data).has(value);
|
|
36
|
+
return resolves ? null : { values: { value } };
|
|
37
|
+
},
|
|
38
|
+
messageTemplate: "Reference `{value}` at `{path}` does not resolve to any entity in this Sheet.",
|
|
39
|
+
remediation: "Either correct the reference to match an existing `@id`, or add the referenced entity to the Sheet. Same-document references (fragments, bare tokens, or full URLs on this Sheet's own base) must resolve within the Sheet, by exact string match. A full-URL `@id` on another document's base in `isVariantOf` or a `hasVariant` entry is a legitimate cross-Sheet reference (multi-Sheet variant family) and is validated for URL shape only — see PDP-W-021 for the traversal-hint recommendation that accompanies it.",
|
|
40
|
+
introduced: "v0.6",
|
|
41
|
+
specReference: "product.md#itemoffered-references",
|
|
42
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-E-002 — Format version / schema mismatch.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* `$schema` is read via the rule context, per the target line. The
|
|
7
|
+
* version segment is the trailing `v<version>.json` of the `$schema`
|
|
8
|
+
* URL. Two quiet cases, both documented readings: `$schema` absent
|
|
9
|
+
* (optional at Layer 1 — nothing to compare), and a `$schema` with no
|
|
10
|
+
* parseable version segment (it declares no version, so no mismatch
|
|
11
|
+
* can be asserted; the message requires `{schema_version}`).
|
|
12
|
+
*/
|
|
13
|
+
export declare const pdpE002: Rule;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDP-E-002 — Format version / schema mismatch.
|
|
3
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
4
|
+
*
|
|
5
|
+
* `$schema` is read via the rule context, per the target line. The
|
|
6
|
+
* version segment is the trailing `v<version>.json` of the `$schema`
|
|
7
|
+
* URL. Two quiet cases, both documented readings: `$schema` absent
|
|
8
|
+
* (optional at Layer 1 — nothing to compare), and a `$schema` with no
|
|
9
|
+
* parseable version segment (it declares no version, so no mismatch
|
|
10
|
+
* can be asserted; the message requires `{schema_version}`).
|
|
11
|
+
*/
|
|
12
|
+
export const pdpE002 = {
|
|
13
|
+
id: "PDP-E-002",
|
|
14
|
+
title: "Format version / schema mismatch",
|
|
15
|
+
severity: "error",
|
|
16
|
+
target: "$.format_version",
|
|
17
|
+
check: (match, ctx) => {
|
|
18
|
+
const formatVersion = String(match.value);
|
|
19
|
+
const schema = ctx.sheet.$schema;
|
|
20
|
+
if (typeof schema !== "string")
|
|
21
|
+
return null;
|
|
22
|
+
const segment = /v(\d+(?:\.\d+)*)\.json$/.exec(schema);
|
|
23
|
+
if (segment === null)
|
|
24
|
+
return null;
|
|
25
|
+
return segment[1] === formatVersion
|
|
26
|
+
? null
|
|
27
|
+
: { values: { format_version: formatVersion, schema_version: segment[1] } };
|
|
28
|
+
},
|
|
29
|
+
messageTemplate: "`format_version` is `{format_version}` but `$schema` URL declares version `{schema_version}`.",
|
|
30
|
+
remediation: "Align both fields to the same version. Whichever version was intended, update the other field to match.",
|
|
31
|
+
introduced: "v0.6",
|
|
32
|
+
specReference: "product.md#the-envelope",
|
|
33
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DATA_BLOCK_PROPERTIES } from "../schema-properties.js";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-I-001 — Property outside Pagefront's modeled set.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* The modeled set is the JSON Schema's dataBlock property inventory
|
|
7
|
+
* (schema-properties.ts, drift-tested against product.json). The
|
|
8
|
+
* target `$.data.*` selects top-level data properties by design; any
|
|
9
|
+
* property outside the inventory fires, including unmodeled
|
|
10
|
+
* `pagefront:`-prefixed ones.
|
|
11
|
+
*/
|
|
12
|
+
const MODELED = new Set(DATA_BLOCK_PROPERTIES);
|
|
13
|
+
export const pdpI001 = {
|
|
14
|
+
id: "PDP-I-001",
|
|
15
|
+
title: "Property outside Pagefront's modeled set",
|
|
16
|
+
severity: "info",
|
|
17
|
+
target: "$.data.*",
|
|
18
|
+
check: (match) => {
|
|
19
|
+
const property = /\['([^']+)'\]$/.exec(match.path)?.[1];
|
|
20
|
+
if (property === undefined || MODELED.has(property))
|
|
21
|
+
return null;
|
|
22
|
+
return { values: { property } };
|
|
23
|
+
},
|
|
24
|
+
messageTemplate: "Property `{property}` at `{path}` is valid Schema.org but is not explicitly modeled by Pagefront.",
|
|
25
|
+
remediation: "No action required. This finding is informational. If you find yourself using a Schema.org property frequently and would value formal Pagefront modeling for it, consider opening a spec proposal.",
|
|
26
|
+
introduced: "v0.6",
|
|
27
|
+
specReference: "product.md#data--schemaorg-json-ld",
|
|
28
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-001 — Recommended field absent: image.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Spec check: "`data.image` is absent, or is an empty array."
|
|
7
|
+
* `fieldAbsentOrEmpty` additionally fires on null and empty/whitespace
|
|
8
|
+
* strings — inputs a Layer-1-valid Sheet cannot present for `image`
|
|
9
|
+
* (the schema types it as an array), so the check is equivalent on the
|
|
10
|
+
* linter's input domain.
|
|
11
|
+
*/
|
|
12
|
+
export declare const pdpW001: Rule;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { fieldAbsentOrEmpty } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-001 — Recommended field absent: image.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Spec check: "`data.image` is absent, or is an empty array."
|
|
7
|
+
* `fieldAbsentOrEmpty` additionally fires on null and empty/whitespace
|
|
8
|
+
* strings — inputs a Layer-1-valid Sheet cannot present for `image`
|
|
9
|
+
* (the schema types it as an array), so the check is equivalent on the
|
|
10
|
+
* linter's input domain.
|
|
11
|
+
*/
|
|
12
|
+
export const pdpW001 = {
|
|
13
|
+
id: "PDP-W-001",
|
|
14
|
+
title: "Recommended field absent: image",
|
|
15
|
+
severity: "warning",
|
|
16
|
+
target: "$.data",
|
|
17
|
+
check: fieldAbsentOrEmpty("image"),
|
|
18
|
+
messageTemplate: "No images declared at `{path}`.",
|
|
19
|
+
remediation: "Add at least one `ImageObject` to the `data.image` array. The primary image should typically declare role `hero` or `packshot`.",
|
|
20
|
+
introduced: "v0.6",
|
|
21
|
+
specReference: "product.md#image-roles",
|
|
22
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-002 — Recommended field absent: description.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Spec check: "`data.description` is absent or empty." The helper's
|
|
7
|
+
* empty-array and null cases are unreachable — the schema types
|
|
8
|
+
* `description` as a string. The helper also treats a whitespace-only
|
|
9
|
+
* string as empty; the catalogue says only "empty", so this is a
|
|
10
|
+
* deliberate reading (consistent with the catalogue rules that spell
|
|
11
|
+
* out "empty or whitespace-only").
|
|
12
|
+
*/
|
|
13
|
+
export declare const pdpW002: Rule;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { fieldAbsentOrEmpty } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-002 — Recommended field absent: description.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Spec check: "`data.description` is absent or empty." The helper's
|
|
7
|
+
* empty-array and null cases are unreachable — the schema types
|
|
8
|
+
* `description` as a string. The helper also treats a whitespace-only
|
|
9
|
+
* string as empty; the catalogue says only "empty", so this is a
|
|
10
|
+
* deliberate reading (consistent with the catalogue rules that spell
|
|
11
|
+
* out "empty or whitespace-only").
|
|
12
|
+
*/
|
|
13
|
+
export const pdpW002 = {
|
|
14
|
+
id: "PDP-W-002",
|
|
15
|
+
title: "Recommended field absent: description",
|
|
16
|
+
severity: "warning",
|
|
17
|
+
target: "$.data",
|
|
18
|
+
check: fieldAbsentOrEmpty("description"),
|
|
19
|
+
messageTemplate: "No description at `{path}`.",
|
|
20
|
+
remediation: "Add a `description` field with a 1–3 paragraph prose description of the product. The description should match the Sheet's `inLanguage`.",
|
|
21
|
+
introduced: "v0.6",
|
|
22
|
+
specReference: "product.md#required-and-recommended-core-fields",
|
|
23
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-003 — Recommended field absent: brand.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Spec check: "`data.brand` is absent." The helper additionally fires on
|
|
7
|
+
* null, empty arrays, and empty/whitespace strings — all unreachable for
|
|
8
|
+
* `brand`, which the schema types as an object; and it does not fire on
|
|
9
|
+
* an empty object. On Layer-1-valid input the helper is therefore
|
|
10
|
+
* exactly "absent".
|
|
11
|
+
*/
|
|
12
|
+
export declare const pdpW003: Rule;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { fieldAbsentOrEmpty } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-003 — Recommended field absent: brand.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Spec check: "`data.brand` is absent." The helper additionally fires on
|
|
7
|
+
* null, empty arrays, and empty/whitespace strings — all unreachable for
|
|
8
|
+
* `brand`, which the schema types as an object; and it does not fire on
|
|
9
|
+
* an empty object. On Layer-1-valid input the helper is therefore
|
|
10
|
+
* exactly "absent".
|
|
11
|
+
*/
|
|
12
|
+
export const pdpW003 = {
|
|
13
|
+
id: "PDP-W-003",
|
|
14
|
+
title: "Recommended field absent: brand",
|
|
15
|
+
severity: "warning",
|
|
16
|
+
target: "$.data",
|
|
17
|
+
check: fieldAbsentOrEmpty("brand"),
|
|
18
|
+
messageTemplate: "No brand declared at `{path}`.",
|
|
19
|
+
remediation: "Add a `brand` field with a typed `Brand` or `Organization` value, including at minimum `name` and optionally `url`.",
|
|
20
|
+
introduced: "v0.6",
|
|
21
|
+
specReference: "product.md#required-and-recommended-core-fields",
|
|
22
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* PDP-W-004 — Offer price without currency.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* Bespoke cross-field check: fires when the offer has `price` but
|
|
7
|
+
* neither `priceCurrency` nor `priceSpecification`. The target matches
|
|
8
|
+
* every `offers` entry (Offer or AggregateOffer); the check is
|
|
9
|
+
* type-agnostic, since a bare `price` is equally ambiguous on either.
|
|
10
|
+
*/
|
|
11
|
+
export declare const pdpW004: Rule;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDP-W-004 — Offer price without currency.
|
|
3
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
4
|
+
*
|
|
5
|
+
* Bespoke cross-field check: fires when the offer has `price` but
|
|
6
|
+
* neither `priceCurrency` nor `priceSpecification`. The target matches
|
|
7
|
+
* every `offers` entry (Offer or AggregateOffer); the check is
|
|
8
|
+
* type-agnostic, since a bare `price` is equally ambiguous on either.
|
|
9
|
+
*/
|
|
10
|
+
export const pdpW004 = {
|
|
11
|
+
id: "PDP-W-004",
|
|
12
|
+
title: "Offer price without currency",
|
|
13
|
+
severity: "warning",
|
|
14
|
+
target: "$.data.offers[*]",
|
|
15
|
+
check: (match) => {
|
|
16
|
+
const offer = match.value;
|
|
17
|
+
if (typeof offer !== "object" || offer === null)
|
|
18
|
+
return null;
|
|
19
|
+
const o = offer;
|
|
20
|
+
return o["price"] !== undefined &&
|
|
21
|
+
o["priceCurrency"] === undefined &&
|
|
22
|
+
o["priceSpecification"] === undefined
|
|
23
|
+
? {}
|
|
24
|
+
: null;
|
|
25
|
+
},
|
|
26
|
+
messageTemplate: "Offer at `{path}` has `price` but no `priceCurrency`.",
|
|
27
|
+
remediation: "Add a `priceCurrency` field with the ISO 4217 currency code matching the offer's currency.",
|
|
28
|
+
introduced: "v0.6",
|
|
29
|
+
specReference: "product.md#offers",
|
|
30
|
+
};
|