@pagefront/lint-commerce 0.12.1 → 0.14.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 +72 -13
- package/dist/catalogue.js +8 -2
- package/dist/collection-catalogue.d.ts +15 -0
- package/dist/collection-catalogue.js +31 -0
- package/dist/generated/release-manifest.d.ts +9 -0
- package/dist/generated/release-manifest.js +10 -0
- package/dist/generated/schema-ids.js +3 -0
- package/dist/generated/schemas.js +2744 -23
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/lint-sheet.d.ts +4 -3
- package/dist/lint-sheet.js +9 -6
- package/dist/organization-catalogue.d.ts +4 -4
- package/dist/organization-catalogue.js +8 -6
- package/dist/overrides.d.ts +36 -0
- package/dist/overrides.js +5 -0
- package/dist/releases.d.ts +21 -6
- package/dist/releases.js +30 -8
- package/dist/rules/checkout-template.d.ts +69 -0
- package/dist/rules/checkout-template.js +165 -0
- package/dist/rules/col-e-001.d.ts +11 -0
- package/dist/rules/col-e-001.js +31 -0
- package/dist/rules/col-e-002.d.ts +2 -0
- package/dist/rules/col-e-002.js +37 -0
- package/dist/rules/col-e-003.d.ts +11 -0
- package/dist/rules/col-e-003.js +18 -0
- package/dist/rules/col-e-004.d.ts +12 -0
- package/dist/rules/col-e-004.js +19 -0
- package/dist/rules/col-e-005.d.ts +16 -0
- package/dist/rules/col-e-005.js +64 -0
- package/dist/rules/col-shared.d.ts +25 -0
- package/dist/rules/col-shared.js +43 -0
- package/dist/rules/col-w-001.d.ts +11 -0
- package/dist/rules/col-w-001.js +21 -0
- package/dist/rules/col-w-002.d.ts +12 -0
- package/dist/rules/col-w-002.js +31 -0
- package/dist/rules/col-w-004.d.ts +12 -0
- package/dist/rules/col-w-004.js +37 -0
- package/dist/rules/col-w-005.d.ts +12 -0
- package/dist/rules/col-w-005.js +41 -0
- package/dist/rules/col-w-006.d.ts +11 -0
- package/dist/rules/col-w-006.js +37 -0
- package/dist/rules/col-w-007.d.ts +13 -0
- package/dist/rules/col-w-007.js +32 -0
- package/dist/rules/org-w-007.d.ts +15 -0
- package/dist/rules/org-w-007.js +47 -0
- package/dist/rules/org-w-008.d.ts +15 -0
- package/dist/rules/org-w-008.js +59 -0
- package/dist/rules/pdp-e-005.d.ts +3 -1
- package/dist/rules/pdp-e-005.js +7 -5
- package/dist/rules/pdp-e-019.d.ts +2 -0
- package/dist/rules/pdp-e-019.js +50 -0
- package/dist/rules/pdp-e-020.d.ts +13 -0
- package/dist/rules/pdp-e-020.js +31 -0
- package/dist/rules/pdp-e-021.d.ts +11 -0
- package/dist/rules/pdp-e-021.js +32 -0
- package/dist/rules/pdp-e-022.d.ts +13 -0
- package/dist/rules/pdp-e-022.js +33 -0
- package/dist/rules/pdp-w-016.d.ts +4 -0
- package/dist/rules/pdp-w-016.js +7 -0
- package/dist/rules/pdp-w-021.js +1 -1
- package/dist/rules/pdp-w-031.d.ts +4 -4
- package/dist/rules/pdp-w-031.js +9 -7
- package/dist/rules/pdp-w-040.d.ts +12 -0
- package/dist/rules/pdp-w-040.js +37 -0
- package/dist/rules/pdp-w-041.d.ts +15 -0
- package/dist/rules/pdp-w-041.js +53 -0
- package/dist/rules/release-schema.d.ts +3 -2
- package/dist/rules/release-schema.js +11 -11
- package/dist/rules/unavailable-offer.d.ts +2 -0
- package/dist/rules/unavailable-offer.js +13 -0
- package/dist/schema-registry.d.ts +11 -0
- package/dist/schema-registry.js +24 -9
- package/dist/schema-validators.js +14 -5
- package/dist/sheet-type.d.ts +17 -12
- package/dist/sheet-type.js +11 -0
- package/dist/test-hooks.d.ts +15 -0
- package/dist/test-hooks.js +28 -0
- package/dist/validate-and-lint.d.ts +9 -5
- package/dist/validate-and-lint.js +33 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { UNKNOWN_RELEASE, unknownRelease } from "./release-schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* COL-E-004 — Unknown release.
|
|
4
|
+
* The collection-sheet counterpart of PDP-E-018 and ORG-E-004.
|
|
5
|
+
* Transcribed from commerce/spec/rules.md (normative); the check is
|
|
6
|
+
* shared (release-schema.ts).
|
|
7
|
+
*
|
|
8
|
+
* `appliesFrom` is "0.0.0": the rule judges the declared release
|
|
9
|
+
* itself, so it cannot be gated by it. It never fires on a sheet of a
|
|
10
|
+
* published release.
|
|
11
|
+
*/
|
|
12
|
+
export const colE004 = {
|
|
13
|
+
id: "COL-E-004",
|
|
14
|
+
...UNKNOWN_RELEASE,
|
|
15
|
+
check: unknownRelease,
|
|
16
|
+
introduced: "release 0.5.0",
|
|
17
|
+
appliesFrom: "0.0.0",
|
|
18
|
+
specReference: "collection.md#the-envelope",
|
|
19
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* COL-E-005 — Referenced sheet URL off its edge's host.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* A collection sheet and the sheets it references in `hasPart` and
|
|
7
|
+
* `isPartOf` share a host (collection.md, "Identity"): a member's
|
|
8
|
+
* `pagefront:sheetUrl` is checked against the *canonical host*. The
|
|
9
|
+
* `brand` reference of a brand collection resolves under the brand edge
|
|
10
|
+
* of the organization linking contract, as PDP-E-006 checks it on a
|
|
11
|
+
* product sheet: against the host of the brand's `@id`. A hint with no
|
|
12
|
+
* host (a blank string among them) is treated as absent and skipped:
|
|
13
|
+
* a blank member hint is COL-W-002's finding. One finding per hint, at
|
|
14
|
+
* the `pagefront:sheetUrl` path.
|
|
15
|
+
*/
|
|
16
|
+
export declare const colE005: Rule;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { asRecord, matchedField, memberEntries } from "./col-shared.js";
|
|
2
|
+
import { hostOf, renderHost } from "./sheet-url-host.js";
|
|
3
|
+
/**
|
|
4
|
+
* COL-E-005 — Referenced sheet URL off its edge's host.
|
|
5
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
6
|
+
*
|
|
7
|
+
* A collection sheet and the sheets it references in `hasPart` and
|
|
8
|
+
* `isPartOf` share a host (collection.md, "Identity"): a member's
|
|
9
|
+
* `pagefront:sheetUrl` is checked against the *canonical host*. The
|
|
10
|
+
* `brand` reference of a brand collection resolves under the brand edge
|
|
11
|
+
* of the organization linking contract, as PDP-E-006 checks it on a
|
|
12
|
+
* product sheet: against the host of the brand's `@id`. A hint with no
|
|
13
|
+
* host (a blank string among them) is treated as absent and skipped:
|
|
14
|
+
* a blank member hint is COL-W-002's finding. One finding per hint, at
|
|
15
|
+
* the `pagefront:sheetUrl` path.
|
|
16
|
+
*/
|
|
17
|
+
export const colE005 = {
|
|
18
|
+
id: "COL-E-005",
|
|
19
|
+
title: "Referenced sheet URL off its edge's host",
|
|
20
|
+
severity: "error",
|
|
21
|
+
target: "$.data['hasPart','isPartOf','brand']",
|
|
22
|
+
check: (match, ctx) => {
|
|
23
|
+
const field = matchedField(match.path);
|
|
24
|
+
const hits = [];
|
|
25
|
+
if (field === "brand") {
|
|
26
|
+
const brand = asRecord(match.value);
|
|
27
|
+
if (brand === null)
|
|
28
|
+
return null;
|
|
29
|
+
// A value with no host (a blank string among them) is treated as absent.
|
|
30
|
+
const urlHost = hostOf(brand["pagefront:sheetUrl"]);
|
|
31
|
+
if (urlHost === null)
|
|
32
|
+
return null;
|
|
33
|
+
const idHost = hostOf(brand["@id"]);
|
|
34
|
+
if (urlHost === idHost)
|
|
35
|
+
return null;
|
|
36
|
+
return {
|
|
37
|
+
path: `${match.path}['pagefront:sheetUrl']`,
|
|
38
|
+
values: {
|
|
39
|
+
sheetUrlHost: renderHost(urlHost),
|
|
40
|
+
expected: `the brand's \`@id\` is on \`${renderHost(idHost)}\``,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const canonicalHost = hostOf(ctx.sheet.canonical_url);
|
|
45
|
+
for (const { entry, path } of memberEntries(match.value, match.path)) {
|
|
46
|
+
const urlHost = hostOf(entry["pagefront:sheetUrl"]);
|
|
47
|
+
if (urlHost === null || urlHost === canonicalHost)
|
|
48
|
+
continue;
|
|
49
|
+
hits.push({
|
|
50
|
+
path: `${path}['pagefront:sheetUrl']`,
|
|
51
|
+
values: {
|
|
52
|
+
sheetUrlHost: renderHost(urlHost),
|
|
53
|
+
expected: `the canonical page is on \`${renderHost(canonicalHost)}\``,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return hits;
|
|
58
|
+
},
|
|
59
|
+
messageTemplate: "Sheet URL at `{path}` is on host `{sheetUrlHost}`; {expected}.",
|
|
60
|
+
remediation: "Point `pagefront:sheetUrl` at the sheet on the host the edge requires: a member or parent sheet on the storefront host, a brand sheet on the host of the brand's `@id`. A consumer rejects a hint off that host and falls back to the entry's `url`.",
|
|
61
|
+
introduced: "release 0.5.0",
|
|
62
|
+
appliesFrom: "0.5.0",
|
|
63
|
+
specReference: "collection.md#identity",
|
|
64
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the Commerce Collection Sheet rules
|
|
3
|
+
* (COL-E-001 … COL-W-006). Kept local to the collection rules: the
|
|
4
|
+
* member-entry walk and the kind enum are collection-catalogue
|
|
5
|
+
* vocabulary, not engine machinery.
|
|
6
|
+
*/
|
|
7
|
+
/** The closed enum of `pagefront:collectionKind`, in vocabulary order. */
|
|
8
|
+
export declare const COLLECTION_KINDS: readonly ["category", "collection", "brand"];
|
|
9
|
+
export declare function asRecord(value: unknown): Record<string, unknown> | null;
|
|
10
|
+
/** A string value that is present and not empty or whitespace-only. */
|
|
11
|
+
export declare function nonBlank(value: unknown): value is string;
|
|
12
|
+
/**
|
|
13
|
+
* The entries of a member array (`hasPart`, `isPartOf`,
|
|
14
|
+
* `itemListElement`) with their normalized paths. A matched value that
|
|
15
|
+
* is a single object is one entry at the match path; a non-object
|
|
16
|
+
* entry is skipped, since the schema types entries as objects.
|
|
17
|
+
*/
|
|
18
|
+
export declare function memberEntries(value: unknown, path: string): Array<{
|
|
19
|
+
entry: Record<string, unknown>;
|
|
20
|
+
path: string;
|
|
21
|
+
}>;
|
|
22
|
+
/** The entry's `name` for a message, or the fixed fallback. */
|
|
23
|
+
export declare function entryName(entry: Record<string, unknown>): string;
|
|
24
|
+
/** The field a match path ends in: `$['data']['hasPart']` → `hasPart`. */
|
|
25
|
+
export declare function matchedField(path: string): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the Commerce Collection Sheet rules
|
|
3
|
+
* (COL-E-001 … COL-W-006). Kept local to the collection rules: the
|
|
4
|
+
* member-entry walk and the kind enum are collection-catalogue
|
|
5
|
+
* vocabulary, not engine machinery.
|
|
6
|
+
*/
|
|
7
|
+
/** The closed enum of `pagefront:collectionKind`, in vocabulary order. */
|
|
8
|
+
export const COLLECTION_KINDS = ["category", "collection", "brand"];
|
|
9
|
+
export function asRecord(value) {
|
|
10
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
11
|
+
? value
|
|
12
|
+
: null;
|
|
13
|
+
}
|
|
14
|
+
/** A string value that is present and not empty or whitespace-only. */
|
|
15
|
+
export function nonBlank(value) {
|
|
16
|
+
return typeof value === "string" && value.trim() !== "";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The entries of a member array (`hasPart`, `isPartOf`,
|
|
20
|
+
* `itemListElement`) with their normalized paths. A matched value that
|
|
21
|
+
* is a single object is one entry at the match path; a non-object
|
|
22
|
+
* entry is skipped, since the schema types entries as objects.
|
|
23
|
+
*/
|
|
24
|
+
export function memberEntries(value, path) {
|
|
25
|
+
const raw = Array.isArray(value)
|
|
26
|
+
? value.map((entry, i) => ({ entry, path: `${path}[${i}]` }))
|
|
27
|
+
: [{ entry: value, path }];
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const { entry, path: p } of raw) {
|
|
30
|
+
const record = asRecord(entry);
|
|
31
|
+
if (record !== null)
|
|
32
|
+
out.push({ entry: record, path: p });
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
/** The entry's `name` for a message, or the fixed fallback. */
|
|
37
|
+
export function entryName(entry) {
|
|
38
|
+
return typeof entry["name"] === "string" ? entry["name"] : "(unnamed)";
|
|
39
|
+
}
|
|
40
|
+
/** The field a match path ends in: `$['data']['hasPart']` → `hasPart`. */
|
|
41
|
+
export function matchedField(path) {
|
|
42
|
+
return /\['([^']+)'\]$/.exec(path)?.[1] ?? "";
|
|
43
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* COL-W-001 — Empty `hasPart`.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* An empty array is valid (a node whose assignments all live in its
|
|
7
|
+
* children, or that holds nothing); the warning asks for review, since
|
|
8
|
+
* an emission pipeline that lost the members produces the same shape.
|
|
9
|
+
* An absent `hasPart` is Layer 1's concern.
|
|
10
|
+
*/
|
|
11
|
+
export declare const colW001: Rule;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COL-W-001 — Empty `hasPart`.
|
|
3
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
4
|
+
*
|
|
5
|
+
* An empty array is valid (a node whose assignments all live in its
|
|
6
|
+
* children, or that holds nothing); the warning asks for review, since
|
|
7
|
+
* an emission pipeline that lost the members produces the same shape.
|
|
8
|
+
* An absent `hasPart` is Layer 1's concern.
|
|
9
|
+
*/
|
|
10
|
+
export const colW001 = {
|
|
11
|
+
id: "COL-W-001",
|
|
12
|
+
title: "Empty hasPart",
|
|
13
|
+
severity: "warning",
|
|
14
|
+
target: "$.data.hasPart",
|
|
15
|
+
check: (match) => (Array.isArray(match.value) && match.value.length === 0 ? {} : null),
|
|
16
|
+
messageTemplate: "`hasPart` at `{path}` is empty: the set declares no direct members.",
|
|
17
|
+
remediation: "Confirm the set has no direct members (its products all live in subcategories, or it holds nothing). If the storefront assigns products or subcategories to this node, list them.",
|
|
18
|
+
introduced: "release 0.5.0",
|
|
19
|
+
appliesFrom: "0.5.0",
|
|
20
|
+
specReference: "collection.md#members",
|
|
21
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* COL-W-002 — Member entry without `pagefront:sheetUrl`.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* The hint is recommended on every member entry: a producer that emits
|
|
7
|
+
* both sheets knows the URL, and without it a consumer reaches the
|
|
8
|
+
* member sheet through its page. The hint is an optimisation that MAY
|
|
9
|
+
* be stale; the rule asks for its presence and never checks that it
|
|
10
|
+
* resolves. One finding per entry, at the entry's path.
|
|
11
|
+
*/
|
|
12
|
+
export declare const colW002: Rule;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { entryName, memberEntries, nonBlank } from "./col-shared.js";
|
|
2
|
+
/**
|
|
3
|
+
* COL-W-002 — Member entry without `pagefront:sheetUrl`.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* The hint is recommended on every member entry: a producer that emits
|
|
7
|
+
* both sheets knows the URL, and without it a consumer reaches the
|
|
8
|
+
* member sheet through its page. The hint is an optimisation that MAY
|
|
9
|
+
* be stale; the rule asks for its presence and never checks that it
|
|
10
|
+
* resolves. One finding per entry, at the entry's path.
|
|
11
|
+
*/
|
|
12
|
+
export const colW002 = {
|
|
13
|
+
id: "COL-W-002",
|
|
14
|
+
title: "Member entry without pagefront:sheetUrl",
|
|
15
|
+
severity: "warning",
|
|
16
|
+
target: "$.data.hasPart",
|
|
17
|
+
check: (match) => {
|
|
18
|
+
const hits = [];
|
|
19
|
+
for (const { entry, path } of memberEntries(match.value, match.path)) {
|
|
20
|
+
if (nonBlank(entry["pagefront:sheetUrl"]))
|
|
21
|
+
continue;
|
|
22
|
+
hits.push({ path, values: { name: entryName(entry) } });
|
|
23
|
+
}
|
|
24
|
+
return hits;
|
|
25
|
+
},
|
|
26
|
+
messageTemplate: "Member `{name}` at `{path}` carries no `pagefront:sheetUrl`; consumers must reach its sheet via `url` + `alternate`.",
|
|
27
|
+
remediation: "Add `pagefront:sheetUrl` with the member's sheet URL when you emit that sheet. If the member has no sheet, the entry is complete as it stands.",
|
|
28
|
+
introduced: "release 0.5.0",
|
|
29
|
+
appliesFrom: "0.5.0",
|
|
30
|
+
specReference: "collection.md#members",
|
|
31
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* COL-W-004 — Brand reference and brand kind disagree.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* A `brand` collection is keyed on a `Brand` node and carries `brand`;
|
|
7
|
+
* a `category` or `collection` is not. Either half missing is the
|
|
8
|
+
* kind-without-content / content-without-kind pattern the organization
|
|
9
|
+
* catalogue checks for roles (ORG-W-001 … ORG-W-004). The finding sits
|
|
10
|
+
* at the data block either way.
|
|
11
|
+
*/
|
|
12
|
+
export declare const colW004: Rule;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { asRecord } from "./col-shared.js";
|
|
2
|
+
/**
|
|
3
|
+
* COL-W-004 — Brand reference and brand kind disagree.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* A `brand` collection is keyed on a `Brand` node and carries `brand`;
|
|
7
|
+
* a `category` or `collection` is not. Either half missing is the
|
|
8
|
+
* kind-without-content / content-without-kind pattern the organization
|
|
9
|
+
* catalogue checks for roles (ORG-W-001 … ORG-W-004). The finding sits
|
|
10
|
+
* at the data block either way.
|
|
11
|
+
*/
|
|
12
|
+
export const colW004 = {
|
|
13
|
+
id: "COL-W-004",
|
|
14
|
+
title: "Brand reference and brand kind disagree",
|
|
15
|
+
severity: "warning",
|
|
16
|
+
target: "$.data",
|
|
17
|
+
check: (match) => {
|
|
18
|
+
const data = asRecord(match.value);
|
|
19
|
+
if (data === null)
|
|
20
|
+
return null;
|
|
21
|
+
const kind = data["pagefront:collectionKind"];
|
|
22
|
+
const brand = data["brand"];
|
|
23
|
+
const hasBrand = brand !== undefined && brand !== null;
|
|
24
|
+
if (kind === "brand" && !hasBrand) {
|
|
25
|
+
return { values: { problem: "declares `pagefront:collectionKind` `brand` but carries no `brand`" } };
|
|
26
|
+
}
|
|
27
|
+
if (kind !== "brand" && hasBrand && typeof kind === "string") {
|
|
28
|
+
return { values: { problem: `carries \`brand\` but declares \`pagefront:collectionKind\` \`${kind}\`` } };
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
},
|
|
32
|
+
messageTemplate: "Collection at `{path}` {problem}.",
|
|
33
|
+
remediation: "On a brand collection, add `brand`, the reference to the `Brand` node the set is keyed on (`@type`, `@id` and `pagefront:sheetUrl`). On a category or a collection, remove `brand`, or declare the kind `brand` if the set is the products sold under one label.",
|
|
34
|
+
introduced: "release 0.5.0",
|
|
35
|
+
appliesFrom: "0.5.0",
|
|
36
|
+
specReference: "collection.md#kind-discriminator-pagefrontcollectionkind",
|
|
37
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* COL-W-005 — Parent of another kind.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* A category's parents are categories (the tree), and a collection
|
|
7
|
+
* never sits in the tree: its parent, when it has one, is a collection.
|
|
8
|
+
* The rule reads the kind the `isPartOf` entry declares and is quiet on
|
|
9
|
+
* an entry that declares none, since it never fetches the parent sheet.
|
|
10
|
+
* One finding per offending entry, at the entry's path.
|
|
11
|
+
*/
|
|
12
|
+
export declare const colW005: Rule;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { asRecord, entryName, memberEntries } from "./col-shared.js";
|
|
2
|
+
/**
|
|
3
|
+
* COL-W-005 — Parent of another kind.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* A category's parents are categories (the tree), and a collection
|
|
7
|
+
* never sits in the tree: its parent, when it has one, is a collection.
|
|
8
|
+
* The rule reads the kind the `isPartOf` entry declares and is quiet on
|
|
9
|
+
* an entry that declares none, since it never fetches the parent sheet.
|
|
10
|
+
* One finding per offending entry, at the entry's path.
|
|
11
|
+
*/
|
|
12
|
+
export const colW005 = {
|
|
13
|
+
id: "COL-W-005",
|
|
14
|
+
title: "Parent of another kind",
|
|
15
|
+
severity: "warning",
|
|
16
|
+
target: "$.data",
|
|
17
|
+
check: (match) => {
|
|
18
|
+
const data = asRecord(match.value);
|
|
19
|
+
if (data === null)
|
|
20
|
+
return null;
|
|
21
|
+
const kind = data["pagefront:collectionKind"];
|
|
22
|
+
if (kind !== "category" && kind !== "collection")
|
|
23
|
+
return null;
|
|
24
|
+
const hits = [];
|
|
25
|
+
for (const { entry, path } of memberEntries(data["isPartOf"], `${match.path}['isPartOf']`)) {
|
|
26
|
+
const parentKind = entry["pagefront:collectionKind"];
|
|
27
|
+
if (typeof parentKind !== "string")
|
|
28
|
+
continue;
|
|
29
|
+
const offends = kind === "category" ? parentKind !== "category" : parentKind === "category";
|
|
30
|
+
if (!offends)
|
|
31
|
+
continue;
|
|
32
|
+
hits.push({ path, values: { kind, name: entryName(entry), parentKind } });
|
|
33
|
+
}
|
|
34
|
+
return hits;
|
|
35
|
+
},
|
|
36
|
+
messageTemplate: "This `{kind}` sheet names parent `{name}` at `{path}`, whose entry declares kind `{parentKind}`.",
|
|
37
|
+
remediation: "A category's `isPartOf` entries are categories: the tree is made of categories only. A collection nests in a collection and never in a category. Correct the parent's declared kind, or move the set to the kind its parent implies.",
|
|
38
|
+
introduced: "release 0.5.0",
|
|
39
|
+
appliesFrom: "0.5.0",
|
|
40
|
+
specReference: "collection.md#kind-discriminator-pagefrontcollectionkind",
|
|
41
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* COL-W-006 — Duplicate member.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* `hasPart` is the set's direct membership, read as exhaustive; the
|
|
7
|
+
* same `@id` twice is a duplicated tile or a merge error. The first
|
|
8
|
+
* occurrence stands; every later one fires, at its own path, naming the
|
|
9
|
+
* index of the first.
|
|
10
|
+
*/
|
|
11
|
+
export declare const colW006: Rule;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { memberEntries, nonBlank } from "./col-shared.js";
|
|
2
|
+
/**
|
|
3
|
+
* COL-W-006 — Duplicate member.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* `hasPart` is the set's direct membership, read as exhaustive; the
|
|
7
|
+
* same `@id` twice is a duplicated tile or a merge error. The first
|
|
8
|
+
* occurrence stands; every later one fires, at its own path, naming the
|
|
9
|
+
* index of the first.
|
|
10
|
+
*/
|
|
11
|
+
export const colW006 = {
|
|
12
|
+
id: "COL-W-006",
|
|
13
|
+
title: "Duplicate member",
|
|
14
|
+
severity: "warning",
|
|
15
|
+
target: "$.data.hasPart",
|
|
16
|
+
check: (match) => {
|
|
17
|
+
const seen = new Map();
|
|
18
|
+
const hits = [];
|
|
19
|
+
for (const { entry, path } of memberEntries(match.value, match.path)) {
|
|
20
|
+
const id = entry["@id"];
|
|
21
|
+
if (!nonBlank(id))
|
|
22
|
+
continue;
|
|
23
|
+
const first = seen.get(id);
|
|
24
|
+
if (first === undefined) {
|
|
25
|
+
seen.set(id, path);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
hits.push({ path, values: { id, first } });
|
|
29
|
+
}
|
|
30
|
+
return hits;
|
|
31
|
+
},
|
|
32
|
+
messageTemplate: "Member `{id}` at `{path}` repeats the entry at `{first}`.",
|
|
33
|
+
remediation: "Keep one entry per member. A product assigned to the set once appears once; a product in several sets appears once in each of them.",
|
|
34
|
+
introduced: "release 0.5.0",
|
|
35
|
+
appliesFrom: "0.5.0",
|
|
36
|
+
specReference: "collection.md#members",
|
|
37
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* COL-W-007 — Parent entry without `pagefront:sheetUrl`.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* A category's parents are category sheets and a collection's parent
|
|
7
|
+
* is a collection sheet, so the producer that emits the sheet has
|
|
8
|
+
* emitted its parents and knows their addresses. The hint is an
|
|
9
|
+
* optimisation that MAY be stale; the rule asks for its presence and
|
|
10
|
+
* never checks that it resolves. One finding per entry, at the entry's
|
|
11
|
+
* path.
|
|
12
|
+
*/
|
|
13
|
+
export declare const colW007: Rule;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { entryName, memberEntries, nonBlank } from "./col-shared.js";
|
|
2
|
+
/**
|
|
3
|
+
* COL-W-007 — Parent entry without `pagefront:sheetUrl`.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* A category's parents are category sheets and a collection's parent
|
|
7
|
+
* is a collection sheet, so the producer that emits the sheet has
|
|
8
|
+
* emitted its parents and knows their addresses. The hint is an
|
|
9
|
+
* optimisation that MAY be stale; the rule asks for its presence and
|
|
10
|
+
* never checks that it resolves. One finding per entry, at the entry's
|
|
11
|
+
* path.
|
|
12
|
+
*/
|
|
13
|
+
export const colW007 = {
|
|
14
|
+
id: "COL-W-007",
|
|
15
|
+
title: "Parent entry without pagefront:sheetUrl",
|
|
16
|
+
severity: "warning",
|
|
17
|
+
target: "$.data.isPartOf",
|
|
18
|
+
check: (match) => {
|
|
19
|
+
const hits = [];
|
|
20
|
+
for (const { entry, path } of memberEntries(match.value, match.path)) {
|
|
21
|
+
if (nonBlank(entry["pagefront:sheetUrl"]))
|
|
22
|
+
continue;
|
|
23
|
+
hits.push({ path, values: { name: entryName(entry) } });
|
|
24
|
+
}
|
|
25
|
+
return hits;
|
|
26
|
+
},
|
|
27
|
+
messageTemplate: "Parent `{name}` at `{path}` carries no `pagefront:sheetUrl`; consumers must reach its sheet via `url` + `alternate`.",
|
|
28
|
+
remediation: "Emit the parent's sheet and add `pagefront:sheetUrl` with its address. A category's parents are category sheets, and a collection nests only in a collection that has a sheet.",
|
|
29
|
+
introduced: "catalogue 0.20.0",
|
|
30
|
+
appliesFrom: "0.5.0",
|
|
31
|
+
specReference: "collection.md#hierarchy",
|
|
32
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* ORG-W-007 — Catalogue entry off the sheet's host.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* `hasOfferCatalog` is the catalogue entry point: its entries reference
|
|
7
|
+
* Commerce Collection Sheets on the organization sheet's own host
|
|
8
|
+
* (organization.md, "Catalogue entry point"). An entry whose
|
|
9
|
+
* `pagefront:sheetUrl` is on another host leads a consumer off the
|
|
10
|
+
* publisher's storefront, where the collection sheets it would find
|
|
11
|
+
* are not this publisher's statement. A hint with no host is treated as
|
|
12
|
+
* absent and skipped. One finding per entry, at the `pagefront:sheetUrl`
|
|
13
|
+
* path.
|
|
14
|
+
*/
|
|
15
|
+
export declare const orgW007: Rule;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { asRecord } from "./org-shared.js";
|
|
2
|
+
import { hostOf, renderHost } from "./sheet-url-host.js";
|
|
3
|
+
/**
|
|
4
|
+
* ORG-W-007 — Catalogue entry off the sheet's host.
|
|
5
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
6
|
+
*
|
|
7
|
+
* `hasOfferCatalog` is the catalogue entry point: its entries reference
|
|
8
|
+
* Commerce Collection Sheets on the organization sheet's own host
|
|
9
|
+
* (organization.md, "Catalogue entry point"). An entry whose
|
|
10
|
+
* `pagefront:sheetUrl` is on another host leads a consumer off the
|
|
11
|
+
* publisher's storefront, where the collection sheets it would find
|
|
12
|
+
* are not this publisher's statement. A hint with no host is treated as
|
|
13
|
+
* absent and skipped. One finding per entry, at the `pagefront:sheetUrl`
|
|
14
|
+
* path.
|
|
15
|
+
*/
|
|
16
|
+
export const orgW007 = {
|
|
17
|
+
id: "ORG-W-007",
|
|
18
|
+
title: "Catalogue entry off the sheet's host",
|
|
19
|
+
severity: "warning",
|
|
20
|
+
target: "$.data.hasOfferCatalog",
|
|
21
|
+
check: (match, ctx) => {
|
|
22
|
+
const catalog = asRecord(match.value);
|
|
23
|
+
if (catalog === null || !Array.isArray(catalog["itemListElement"]))
|
|
24
|
+
return null;
|
|
25
|
+
const canonicalHost = hostOf(ctx.sheet.canonical_url);
|
|
26
|
+
const hits = [];
|
|
27
|
+
catalog["itemListElement"].forEach((item, i) => {
|
|
28
|
+
const entry = asRecord(item);
|
|
29
|
+
if (entry === null)
|
|
30
|
+
return;
|
|
31
|
+
// A value with no host (a blank string among them) is treated as absent.
|
|
32
|
+
const urlHost = hostOf(entry["pagefront:sheetUrl"]);
|
|
33
|
+
if (urlHost === null || urlHost === canonicalHost)
|
|
34
|
+
return;
|
|
35
|
+
hits.push({
|
|
36
|
+
path: `${match.path}['itemListElement'][${i}]['pagefront:sheetUrl']`,
|
|
37
|
+
values: { sheetUrlHost: renderHost(urlHost), host: renderHost(canonicalHost) },
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
return hits;
|
|
41
|
+
},
|
|
42
|
+
messageTemplate: "Catalogue entry sheet URL at `{path}` is on host `{sheetUrlHost}`; the canonical page is on `{host}`.",
|
|
43
|
+
remediation: "Point `pagefront:sheetUrl` at the collection sheet on this organization sheet's own host. A root in another language is reached through `pagefront:availableLanguages` on the root sheet, never through a second host here.",
|
|
44
|
+
introduced: "release 0.5.0",
|
|
45
|
+
appliesFrom: "0.5.0",
|
|
46
|
+
specReference: "organization.md#catalogue-entry-point-any-role",
|
|
47
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Rule } from "@pagefront/lint-core";
|
|
2
|
+
/**
|
|
3
|
+
* ORG-W-008 — Checkout hosts without seller role, or not a bare host.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* `pagefront:checkoutHosts` lists the hosts outside the canonical host
|
|
7
|
+
* on which the seller's offers are bought or checked out
|
|
8
|
+
* (organization.md, "Seller block"); it is admitted only where
|
|
9
|
+
* `commerceRole` includes `seller`, and each entry is a bare host name.
|
|
10
|
+
* One finding on the field when the role is absent, and one per entry
|
|
11
|
+
* that is not a bare host: a value that is not a string, is empty or
|
|
12
|
+
* whitespace-only, or carries a scheme, a path or a port. The entries
|
|
13
|
+
* are read as written; the rule compares them with nothing.
|
|
14
|
+
*/
|
|
15
|
+
export declare const orgW008: Rule;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { asRecord, commerceRoles } from "./org-shared.js";
|
|
2
|
+
/**
|
|
3
|
+
* ORG-W-008 — Checkout hosts without seller role, or not a bare host.
|
|
4
|
+
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
|
+
*
|
|
6
|
+
* `pagefront:checkoutHosts` lists the hosts outside the canonical host
|
|
7
|
+
* on which the seller's offers are bought or checked out
|
|
8
|
+
* (organization.md, "Seller block"); it is admitted only where
|
|
9
|
+
* `commerceRole` includes `seller`, and each entry is a bare host name.
|
|
10
|
+
* One finding on the field when the role is absent, and one per entry
|
|
11
|
+
* that is not a bare host: a value that is not a string, is empty or
|
|
12
|
+
* whitespace-only, or carries a scheme, a path or a port. The entries
|
|
13
|
+
* are read as written; the rule compares them with nothing.
|
|
14
|
+
*/
|
|
15
|
+
export const orgW008 = {
|
|
16
|
+
id: "ORG-W-008",
|
|
17
|
+
title: "Checkout hosts without seller role, or not a bare host",
|
|
18
|
+
severity: "warning",
|
|
19
|
+
target: "$.data",
|
|
20
|
+
check: (match) => {
|
|
21
|
+
const data = asRecord(match.value);
|
|
22
|
+
if (data === null || !Object.prototype.hasOwnProperty.call(data, "pagefront:checkoutHosts"))
|
|
23
|
+
return null;
|
|
24
|
+
const field = `${match.path}['pagefront:checkoutHosts']`;
|
|
25
|
+
const hits = [];
|
|
26
|
+
if (!commerceRoles(data).includes("seller")) {
|
|
27
|
+
hits.push({ path: field, values: { problem: "the sheet does not declare the `seller` role" } });
|
|
28
|
+
}
|
|
29
|
+
const hosts = data["pagefront:checkoutHosts"];
|
|
30
|
+
if (Array.isArray(hosts)) {
|
|
31
|
+
hosts.forEach((entry, i) => {
|
|
32
|
+
const problem = notBareHost(entry);
|
|
33
|
+
if (problem !== null)
|
|
34
|
+
hits.push({ path: `${field}[${i}]`, values: { problem } });
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return hits.length > 0 ? hits : null;
|
|
38
|
+
},
|
|
39
|
+
messageTemplate: "`pagefront:checkoutHosts` at `{path}`: {problem}.",
|
|
40
|
+
remediation: "List bare host names only (`checkout.merchant.example`), and only on the seller's own sheet, one whose `pagefront:commerceRole` includes `seller`; otherwise remove the field.",
|
|
41
|
+
introduced: "release 0.6.0",
|
|
42
|
+
appliesFrom: "0.6.0",
|
|
43
|
+
specReference: "organization.md#seller-block-seller",
|
|
44
|
+
};
|
|
45
|
+
/** Why `entry` is not a bare host, or `null` when it is one. */
|
|
46
|
+
function notBareHost(entry) {
|
|
47
|
+
if (typeof entry !== "string")
|
|
48
|
+
return `entry ${JSON.stringify(entry)} is not a string`;
|
|
49
|
+
if (entry.trim() === "")
|
|
50
|
+
return "an entry is empty or whitespace-only";
|
|
51
|
+
const shown = `entry \`${entry}\``;
|
|
52
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(entry))
|
|
53
|
+
return `${shown} carries a scheme`;
|
|
54
|
+
if (entry.includes("/"))
|
|
55
|
+
return `${shown} carries a path`;
|
|
56
|
+
if (entry.includes(":"))
|
|
57
|
+
return `${shown} carries a port`;
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
@@ -4,7 +4,9 @@ import type { Rule } from "@pagefront/lint-core";
|
|
|
4
4
|
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
5
|
*
|
|
6
6
|
* Fires when an offer's `seller` carries `pagefront:sheetUrl` whose host
|
|
7
|
-
* differs from the host
|
|
7
|
+
* differs from the canonical host, or has no host. The canonical host is
|
|
8
|
+
* the host of the envelope `canonical_url`, the page the sheet describes
|
|
9
|
+
* (the catalogue's Definitions); the optional `sheet_url` plays no part.
|
|
8
10
|
* The seller's `@id` plays no part: the linking contract lets it be
|
|
9
11
|
* minted on another domain the organization controls. The finding sits
|
|
10
12
|
* at the `pagefront:sheetUrl` path.
|
package/dist/rules/pdp-e-005.js
CHANGED
|
@@ -4,7 +4,9 @@ import { hostOf, renderHost } from "./sheet-url-host.js";
|
|
|
4
4
|
* Transcribed from commerce/spec/rules.md (normative).
|
|
5
5
|
*
|
|
6
6
|
* Fires when an offer's `seller` carries `pagefront:sheetUrl` whose host
|
|
7
|
-
* differs from the host
|
|
7
|
+
* differs from the canonical host, or has no host. The canonical host is
|
|
8
|
+
* the host of the envelope `canonical_url`, the page the sheet describes
|
|
9
|
+
* (the catalogue's Definitions); the optional `sheet_url` plays no part.
|
|
8
10
|
* The seller's `@id` plays no part: the linking contract lets it be
|
|
9
11
|
* minted on another domain the organization controls. The finding sits
|
|
10
12
|
* at the `pagefront:sheetUrl` path.
|
|
@@ -25,15 +27,15 @@ export const pdpE005 = {
|
|
|
25
27
|
if (s["pagefront:sheetUrl"] === undefined)
|
|
26
28
|
return null;
|
|
27
29
|
const urlHost = hostOf(s["pagefront:sheetUrl"]);
|
|
28
|
-
const
|
|
29
|
-
if (urlHost !== null && urlHost ===
|
|
30
|
+
const canonicalHost = hostOf(ctx.sheet.canonical_url);
|
|
31
|
+
if (urlHost !== null && urlHost === canonicalHost)
|
|
30
32
|
return null;
|
|
31
33
|
return {
|
|
32
34
|
path: `${match.path}['seller']['pagefront:sheetUrl']`,
|
|
33
|
-
values: { sheetUrlHost: renderHost(urlHost),
|
|
35
|
+
values: { sheetUrlHost: renderHost(urlHost), host: renderHost(canonicalHost) },
|
|
34
36
|
};
|
|
35
37
|
},
|
|
36
|
-
messageTemplate: "Seller sheet URL at `{path}` is on host `{sheetUrlHost}`; the
|
|
38
|
+
messageTemplate: "Seller sheet URL at `{path}` is on host `{sheetUrlHost}`; the canonical page is on `{host}`.",
|
|
37
39
|
remediation: "Publish the seller's organization sheet on the storefront host.",
|
|
38
40
|
introduced: "v0.9",
|
|
39
41
|
appliesFrom: "0.3.0",
|