@pagefront/lint-commerce 0.11.0 → 0.12.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 +33 -13
- package/dist/catalogue.js +4 -2
- package/dist/generated/release-manifest.d.ts +8 -0
- package/dist/generated/release-manifest.js +9 -0
- package/dist/generated/schema-ids.js +2 -0
- package/dist/generated/schemas.js +2450 -7
- package/dist/organization-catalogue.js +3 -2
- package/dist/rules/org-e-001.js +2 -0
- package/dist/rules/org-e-002.js +2 -0
- package/dist/rules/org-e-003.js +2 -0
- package/dist/rules/org-e-004.d.ts +11 -0
- package/dist/rules/org-e-004.js +18 -0
- package/dist/rules/org-w-001.js +2 -0
- package/dist/rules/org-w-002.js +2 -0
- package/dist/rules/org-w-003.js +2 -0
- package/dist/rules/org-w-004.js +2 -0
- package/dist/rules/org-w-005.js +2 -0
- package/dist/rules/org-w-006.js +2 -0
- package/dist/rules/pdp-e-001.js +2 -0
- package/dist/rules/pdp-e-002.js +2 -0
- package/dist/rules/pdp-e-003.js +2 -0
- package/dist/rules/pdp-e-004.js +2 -0
- package/dist/rules/pdp-e-005.js +2 -0
- package/dist/rules/pdp-e-006.js +2 -0
- package/dist/rules/pdp-e-007.js +2 -0
- package/dist/rules/pdp-e-008.js +2 -0
- package/dist/rules/pdp-e-009.js +2 -0
- package/dist/rules/pdp-e-010.js +2 -0
- package/dist/rules/pdp-e-011.js +2 -0
- package/dist/rules/pdp-e-012.js +2 -0
- package/dist/rules/pdp-e-013.js +2 -0
- package/dist/rules/pdp-e-014.js +2 -0
- package/dist/rules/pdp-e-015.js +2 -0
- package/dist/rules/pdp-e-016.js +2 -0
- package/dist/rules/pdp-e-017.d.ts +2 -0
- package/dist/rules/pdp-e-017.js +80 -0
- package/dist/rules/pdp-e-018.d.ts +11 -0
- package/dist/rules/pdp-e-018.js +18 -0
- package/dist/rules/pdp-i-001.js +2 -0
- package/dist/rules/pdp-w-001.js +2 -0
- package/dist/rules/pdp-w-002.js +2 -0
- package/dist/rules/pdp-w-003.js +2 -0
- package/dist/rules/pdp-w-004.js +2 -0
- package/dist/rules/pdp-w-005.js +2 -0
- package/dist/rules/pdp-w-006.js +2 -0
- package/dist/rules/pdp-w-007.js +2 -0
- package/dist/rules/pdp-w-008.js +2 -0
- package/dist/rules/pdp-w-009.js +2 -0
- package/dist/rules/pdp-w-010.js +2 -0
- package/dist/rules/pdp-w-011.js +2 -0
- package/dist/rules/pdp-w-012.js +2 -0
- package/dist/rules/pdp-w-013.js +2 -0
- package/dist/rules/pdp-w-014.js +2 -0
- package/dist/rules/pdp-w-015.js +2 -0
- package/dist/rules/pdp-w-016.js +2 -0
- package/dist/rules/pdp-w-017.js +2 -0
- package/dist/rules/pdp-w-018.js +2 -0
- package/dist/rules/pdp-w-019.js +2 -0
- package/dist/rules/pdp-w-020.js +2 -0
- package/dist/rules/pdp-w-021.js +2 -0
- package/dist/rules/pdp-w-022.js +2 -0
- package/dist/rules/pdp-w-023.js +2 -0
- package/dist/rules/pdp-w-024.js +2 -0
- package/dist/rules/pdp-w-025.js +2 -0
- package/dist/rules/pdp-w-026.js +2 -0
- package/dist/rules/pdp-w-027.js +2 -0
- package/dist/rules/pdp-w-028.js +2 -0
- package/dist/rules/pdp-w-029.js +2 -0
- package/dist/rules/pdp-w-030.js +2 -0
- package/dist/rules/pdp-w-031.js +2 -0
- package/dist/rules/pdp-w-032.js +2 -0
- package/dist/rules/pdp-w-033.js +2 -0
- package/dist/rules/pdp-w-034.js +2 -0
- package/dist/rules/pdp-w-035.js +2 -0
- package/dist/rules/pdp-w-036.js +2 -0
- package/dist/rules/pdp-w-037.js +2 -0
- package/dist/rules/pdp-w-038.js +2 -0
- package/dist/rules/pdp-w-039.js +2 -0
- package/dist/rules/release-schema.d.ts +18 -7
- package/dist/rules/release-schema.js +55 -15
- package/dist/schema-registry.d.ts +27 -14
- package/dist/schema-registry.js +29 -12
- package/dist/schema-validators.js +12 -4
- package/dist/validate-and-lint.d.ts +13 -9
- package/dist/validate-and-lint.js +23 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/rules/pdp-w-023.js
CHANGED
|
@@ -35,5 +35,7 @@ export const pdpW023 = {
|
|
|
35
35
|
messageTemplate: "Recall safety notice at `{path}` coexists with offer at `{offerPath}` declaring `InStock`; the recall overrides commercial signals and the offer data likely needs updating.",
|
|
36
36
|
remediation: "Update the offer's `availability` to reflect the corrective measure (typically `Discontinued` or `OutOfStock`). If the recall has been resolved and the product legitimately relisted, remove the stale `pagefront:safetyNotice` entry instead.",
|
|
37
37
|
introduced: "v0.8",
|
|
38
|
+
appliesFrom: "0.3.0",
|
|
39
|
+
appliesToLegacy: true,
|
|
38
40
|
specReference: "product.md#safety-notices",
|
|
39
41
|
};
|
package/dist/rules/pdp-w-024.js
CHANGED
|
@@ -54,5 +54,7 @@ export const pdpW024 = {
|
|
|
54
54
|
messageTemplate: "`pagefront:legalEntity` entry with role `{role}` at `{path}` declares an organization without an `address`.",
|
|
55
55
|
remediation: "Add the entity's postal address to the `organization`. If no address is available, the entry still identifies the organization but should not be relied on as a regulatory responsible-person declaration.",
|
|
56
56
|
introduced: "v0.8",
|
|
57
|
+
appliesFrom: "0.3.0",
|
|
58
|
+
appliesToLegacy: true,
|
|
57
59
|
specReference: "product.md#legal-entities",
|
|
58
60
|
};
|
package/dist/rules/pdp-w-025.js
CHANGED
|
@@ -31,5 +31,7 @@ export const pdpW025 = {
|
|
|
31
31
|
messageTemplate: "Feature `{name}` at `{path}` carries value `{value}`, which parses as a number plus a unit.",
|
|
32
32
|
remediation: "Move the numeric part into `value`, express the unit as a `unitCode`, and if the merchant's display formatting matters, carry the original string in `pagefront:presentationValue`.",
|
|
33
33
|
introduced: "v0.8",
|
|
34
|
+
appliesFrom: "0.3.0",
|
|
35
|
+
appliesToLegacy: true,
|
|
34
36
|
specReference: "product.md#feature-groups",
|
|
35
37
|
};
|
package/dist/rules/pdp-w-026.js
CHANGED
|
@@ -27,5 +27,7 @@ export const pdpW026 = {
|
|
|
27
27
|
messageTemplate: "Safety data sheet at `{path}` declares no `inLanguage`.",
|
|
28
28
|
remediation: "Add the document's BCP 47 language tag as `inLanguage`. If the document is multilingual, declare the primary language.",
|
|
29
29
|
introduced: "v0.8",
|
|
30
|
+
appliesFrom: "0.3.0",
|
|
31
|
+
appliesToLegacy: true,
|
|
30
32
|
specReference: "product.md#documents",
|
|
31
33
|
};
|
package/dist/rules/pdp-w-027.js
CHANGED
|
@@ -43,5 +43,7 @@ export const pdpW027 = {
|
|
|
43
43
|
messageTemplate: "Question `{name}` at `{path}` has an `acceptedAnswer` with no text.",
|
|
44
44
|
remediation: "Populate `acceptedAnswer.text` with the answer prose, or remove the `acceptedAnswer` (and consider whether an unanswered question belongs in the sheet at all).",
|
|
45
45
|
introduced: "v0.8",
|
|
46
|
+
appliesFrom: "0.3.0",
|
|
47
|
+
appliesToLegacy: true,
|
|
46
48
|
specReference: "product.md#product-qa",
|
|
47
49
|
};
|
package/dist/rules/pdp-w-028.js
CHANGED
|
@@ -25,5 +25,7 @@ export const pdpW028 = {
|
|
|
25
25
|
messageTemplate: "Offer at `{path}` states a price but not whether value-added tax is included.",
|
|
26
26
|
remediation: "Add a `UnitPriceSpecification` entry mirroring `price` with `valueAddedTaxIncluded` set to `true` or `false`; on an Offer that already uses `priceSpecification`, set the flag on each entry.",
|
|
27
27
|
introduced: "v0.9",
|
|
28
|
+
appliesFrom: "0.3.0",
|
|
29
|
+
appliesToLegacy: true,
|
|
28
30
|
specReference: "product.md#tax-inclusion",
|
|
29
31
|
};
|
package/dist/rules/pdp-w-029.js
CHANGED
|
@@ -24,5 +24,7 @@ export const pdpW029 = {
|
|
|
24
24
|
messageTemplate: "Offer at `{path}` states a price but no region it applies to.",
|
|
25
25
|
remediation: "Declare `areaServed` (or `eligibleRegion`) for the market whose displayed price this is. Where the market cannot be determined from the storefront, stating the tax basis via `valueAddedTaxIncluded` is the higher priority; see \"Tax inclusion.\"",
|
|
26
26
|
introduced: "v0.9",
|
|
27
|
+
appliesFrom: "0.3.0",
|
|
28
|
+
appliesToLegacy: true,
|
|
27
29
|
specReference: "product.md#tax-inclusion",
|
|
28
30
|
};
|
package/dist/rules/pdp-w-030.js
CHANGED
|
@@ -48,5 +48,7 @@ export const pdpW030 = {
|
|
|
48
48
|
messageTemplate: "Warranty at `{path}` states neither a duration nor a description.",
|
|
49
49
|
remediation: "State the warranty as written in `description`, and add `durationOfWarranty` where a finite length is stated. A lifetime warranty carries `description` (and optionally `name`) with no `durationOfWarranty`.",
|
|
50
50
|
introduced: "v0.9",
|
|
51
|
+
appliesFrom: "0.3.0",
|
|
52
|
+
appliesToLegacy: true,
|
|
51
53
|
specReference: "product.md#warranty",
|
|
52
54
|
};
|
package/dist/rules/pdp-w-031.js
CHANGED
|
@@ -31,5 +31,7 @@ export const pdpW031 = {
|
|
|
31
31
|
messageTemplate: "Seller at `{path}` has an `@id` but no `pagefront:sheetUrl`, and the Offer states no return or shipping terms of its own.",
|
|
32
32
|
remediation: "Add `pagefront:sheetUrl` to the seller reference, or state the terms on the Offer.",
|
|
33
33
|
introduced: "v0.9",
|
|
34
|
+
appliesFrom: "0.3.0",
|
|
35
|
+
appliesToLegacy: true,
|
|
34
36
|
specReference: "organization.md#linking-contract-identity-only",
|
|
35
37
|
};
|
package/dist/rules/pdp-w-032.js
CHANGED
|
@@ -46,5 +46,7 @@ export const pdpW032 = {
|
|
|
46
46
|
messageTemplate: "Pricing block at `{path}` {problem}, and `pricing` is not `partial`.",
|
|
47
47
|
remediation: "State the modifier, zero included; for a text dimension, state it under the key `\"*\"`. If the dimension never changes the price, declare `affectsPrice: false` on it. If the price is not known, declare `pricing: \"partial\"`.",
|
|
48
48
|
introduced: "v0.9 (2026-10-01 revision)",
|
|
49
|
+
appliesFrom: "0.3.0",
|
|
50
|
+
appliesToLegacy: true,
|
|
49
51
|
specReference: "product.md#configurator-pricing-and-price-bands",
|
|
50
52
|
};
|
package/dist/rules/pdp-w-033.js
CHANGED
|
@@ -26,5 +26,7 @@ export const pdpW033 = {
|
|
|
26
26
|
messageTemplate: "Configurator at `{path}` has priced dimensions but no offer prices it.",
|
|
27
27
|
remediation: "Add an offer carrying `pagefront:configuratorPricing` for this configurator. If no option changes the price, declare `affectsPrice: false` on each dimension.",
|
|
28
28
|
introduced: "v0.9 (2026-10-01 revision)",
|
|
29
|
+
appliesFrom: "0.3.0",
|
|
30
|
+
appliesToLegacy: true,
|
|
29
31
|
specReference: "product.md#configurator-pricing-and-price-bands",
|
|
30
32
|
};
|
package/dist/rules/pdp-w-034.js
CHANGED
|
@@ -24,5 +24,7 @@ export const pdpW034 = {
|
|
|
24
24
|
messageTemplate: "Offer price is not linked to the configurator; state which configuration it prices.",
|
|
25
25
|
remediation: "Add `pagefront:configuratorPricing` to the offer: give the configurator an `@id`, reference it, and name the configuration the price is the total of. When only that one total is known, declare `pricing: \"partial\"`.",
|
|
26
26
|
introduced: "v0.9 (2026-10-01 revision)",
|
|
27
|
+
appliesFrom: "0.3.0",
|
|
28
|
+
appliesToLegacy: true,
|
|
27
29
|
specReference: "product.md#configurator-pricing-and-price-bands",
|
|
28
30
|
};
|
package/dist/rules/pdp-w-035.js
CHANGED
|
@@ -37,5 +37,7 @@ export const pdpW035 = {
|
|
|
37
37
|
messageTemplate: "basePrice may be a component price, not a configured total.",
|
|
38
38
|
remediation: "Confirm that `basePrice` is what a buyer pays for the whole of `baseConfiguration`. If it is the price of one part, replace it with the configured total and recompute the modifiers as differences from that total.",
|
|
39
39
|
introduced: "v0.9 (2026-10-01 revision)",
|
|
40
|
+
appliesFrom: "0.3.0",
|
|
41
|
+
appliesToLegacy: true,
|
|
40
42
|
specReference: "product.md#configurator-pricing-and-price-bands",
|
|
41
43
|
};
|
package/dist/rules/pdp-w-036.js
CHANGED
|
@@ -27,5 +27,7 @@ export const pdpW036 = {
|
|
|
27
27
|
messageTemplate: "Option value should be a stable identifier; put the label in description.",
|
|
28
28
|
remediation: "Replace the value with a lowercase identifier without spaces (`yellow_gold`, `0.5`) and move the label to `description`. Put a unit in the dimension's `note`. Update the pricing block's keys, `baseConfiguration`, `when` and `exclusions` to the new value.",
|
|
29
29
|
introduced: "v0.9 (2026-10-01 revision)",
|
|
30
|
+
appliesFrom: "0.3.0",
|
|
31
|
+
appliesToLegacy: true,
|
|
30
32
|
specReference: "product.md#configurator-pricing-and-price-bands",
|
|
31
33
|
};
|
package/dist/rules/pdp-w-037.js
CHANGED
|
@@ -33,5 +33,7 @@ export const pdpW037 = {
|
|
|
33
33
|
messageTemplate: "Option does not look like a size in the declared system.",
|
|
34
34
|
remediation: "Remove the entry if it is a service, and describe the service in the dimension's `note` or in `pagefront:madeToOrderTerms.aftermarketServices`. If it is a size, write it as the system writes it, without the system's name.",
|
|
35
35
|
introduced: "v0.9 (2026-10-01 revision)",
|
|
36
|
+
appliesFrom: "0.3.0",
|
|
37
|
+
appliesToLegacy: true,
|
|
36
38
|
specReference: "product.md#configurator-pricing-and-price-bands",
|
|
37
39
|
};
|
package/dist/rules/pdp-w-038.js
CHANGED
|
@@ -26,5 +26,7 @@ export const pdpW038 = {
|
|
|
26
26
|
messageTemplate: "Note reads as page copy; state the fact.",
|
|
27
27
|
remediation: "Restate the note as a fact in the third person: what the merchant offers or recommends, with no \"you\" and no instruction.",
|
|
28
28
|
introduced: "v0.9 (2026-10-01 revision)",
|
|
29
|
+
appliesFrom: "0.3.0",
|
|
30
|
+
appliesToLegacy: true,
|
|
29
31
|
specReference: "product.md#configurator-pricing-and-price-bands",
|
|
30
32
|
};
|
package/dist/rules/pdp-w-039.js
CHANGED
|
@@ -25,5 +25,7 @@ export const pdpW039 = {
|
|
|
25
25
|
messageTemplate: "Size advice at `{path}` could not be stated in the Sheet's sizing system (`{system}`); it is carried as prose only.",
|
|
26
26
|
remediation: "If the merchant states the advice in the sizing dimension's system, add a `sizeAdjustment` in that system. If it does not, no action: do not convert between systems.",
|
|
27
27
|
introduced: "v0.9 (2026-10-01 revision)",
|
|
28
|
+
appliesFrom: "0.3.0",
|
|
29
|
+
appliesToLegacy: true,
|
|
28
30
|
specReference: "product.md#size-guidance-pagefrontsizechart-and-pagefrontsizerecommendation",
|
|
29
31
|
};
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import type { CheckFn, Rule } from "@pagefront/lint-core";
|
|
2
2
|
import type { SheetType } from "../sheet-type.js";
|
|
3
3
|
/**
|
|
4
|
-
* Release / schema mismatch.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Release / schema mismatch. The rule reads `$schema` and is quiet
|
|
5
|
+
* whenever it is absent, on both lines: whether `$schema` is required is
|
|
6
|
+
* the schema's own statement (Product Sheet spec 0.10.0 and Organization
|
|
7
|
+
* Sheet spec 0.4.0 onward), not this rule's. On the release line a
|
|
8
|
+
* present `$schema` must be the URL of the sheet-spec schema the release
|
|
9
|
+
* manifest maps the declared `release` and the sheet type to; a release
|
|
10
|
+
* the manifest does not list has no mapping (the unknown-release rule
|
|
11
|
+
* reports it). On the legacy line the `$schema` version segment must
|
|
12
|
+
* equal `format_version`, as before. Quiet when the sheet carries both
|
|
13
|
+
* fields (the both-fields rule reports that).
|
|
11
14
|
*/
|
|
12
15
|
export declare function releaseSchemaMismatch(sheetType: SheetType): CheckFn;
|
|
13
16
|
export declare const RELEASE_SCHEMA_MISMATCH: Pick<Rule, "title" | "severity" | "target" | "messageTemplate" | "remediation">;
|
|
14
17
|
/** A sheet carrying both `format_version` and `release`. */
|
|
15
18
|
export declare const bothVersionFields: CheckFn;
|
|
16
19
|
export declare const BOTH_VERSION_FIELDS: Pick<Rule, "title" | "severity" | "target" | "messageTemplate" | "remediation">;
|
|
20
|
+
/**
|
|
21
|
+
* Unknown release: the sheet declares a `release` the release manifest
|
|
22
|
+
* does not list, so no sheet-spec version and no schema can be resolved
|
|
23
|
+
* for it. Only a string `release` is judged; another JSON type is
|
|
24
|
+
* Layer 1's concern.
|
|
25
|
+
*/
|
|
26
|
+
export declare const unknownRelease: CheckFn;
|
|
27
|
+
export declare const UNKNOWN_RELEASE: Pick<Rule, "title" | "severity" | "target" | "messageTemplate" | "remediation">;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sheetSpecVersionFor } from "../releases.js";
|
|
1
|
+
import { KNOWN_RELEASES, schemaUrlFor, sheetSpecVersionFor } from "../releases.js";
|
|
2
2
|
/**
|
|
3
3
|
* Shared checks for the envelope's version fields, used by the product
|
|
4
4
|
* and organization catalogues (PDP-E-002 / ORG-E-003, PDP-E-016 /
|
|
@@ -11,39 +11,59 @@ const SHEET_LABEL = {
|
|
|
11
11
|
organization: "Organization Sheet",
|
|
12
12
|
};
|
|
13
13
|
/**
|
|
14
|
-
* Release / schema mismatch.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
14
|
+
* Release / schema mismatch. The rule reads `$schema` and is quiet
|
|
15
|
+
* whenever it is absent, on both lines: whether `$schema` is required is
|
|
16
|
+
* the schema's own statement (Product Sheet spec 0.10.0 and Organization
|
|
17
|
+
* Sheet spec 0.4.0 onward), not this rule's. On the release line a
|
|
18
|
+
* present `$schema` must be the URL of the sheet-spec schema the release
|
|
19
|
+
* manifest maps the declared `release` and the sheet type to; a release
|
|
20
|
+
* the manifest does not list has no mapping (the unknown-release rule
|
|
21
|
+
* reports it). On the legacy line the `$schema` version segment must
|
|
22
|
+
* equal `format_version`, as before. Quiet when the sheet carries both
|
|
23
|
+
* fields (the both-fields rule reports that).
|
|
21
24
|
*/
|
|
22
25
|
export function releaseSchemaMismatch(sheetType) {
|
|
23
26
|
return (_match, ctx) => {
|
|
24
27
|
const { release, format_version: formatVersion, $schema: schema } = ctx.sheet;
|
|
25
28
|
if (typeof schema !== "string")
|
|
26
29
|
return null;
|
|
27
|
-
const schemaVersion = SCHEMA_VERSION.exec(schema)?.[1];
|
|
28
|
-
if (schemaVersion === undefined)
|
|
29
|
-
return null;
|
|
30
30
|
if (release !== undefined && formatVersion !== undefined)
|
|
31
31
|
return null;
|
|
32
|
+
const schemaVersion = SCHEMA_VERSION.exec(schema)?.[1];
|
|
32
33
|
if (typeof release === "string") {
|
|
33
34
|
const expected = sheetSpecVersionFor(release, sheetType);
|
|
34
|
-
if (expected === undefined
|
|
35
|
+
if (expected === undefined)
|
|
36
|
+
return null;
|
|
37
|
+
const expectedUrl = schemaUrlFor(sheetType, expected);
|
|
38
|
+
if (schema === expectedUrl)
|
|
35
39
|
return null;
|
|
40
|
+
const declared = `\`release\` \`${release}\` bundles ${SHEET_LABEL[sheetType]} spec \`${expected}\``;
|
|
41
|
+
if (schemaVersion !== undefined && schemaVersion !== expected) {
|
|
42
|
+
return {
|
|
43
|
+
path: "$['release']",
|
|
44
|
+
values: {
|
|
45
|
+
declared,
|
|
46
|
+
found: `\`$schema\` URL declares version \`${schemaVersion}\``,
|
|
47
|
+
release,
|
|
48
|
+
expected,
|
|
49
|
+
schema_version: schemaVersion,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
36
53
|
return {
|
|
37
54
|
path: "$['release']",
|
|
38
55
|
values: {
|
|
39
|
-
declared
|
|
56
|
+
declared,
|
|
57
|
+
found: `\`$schema\` is not that schema's URL, \`${expectedUrl}\``,
|
|
40
58
|
release,
|
|
41
59
|
expected,
|
|
42
|
-
|
|
60
|
+
expected_url: expectedUrl,
|
|
43
61
|
},
|
|
44
62
|
};
|
|
45
63
|
}
|
|
46
64
|
if (formatVersion !== undefined) {
|
|
65
|
+
if (schemaVersion === undefined)
|
|
66
|
+
return null;
|
|
47
67
|
const declared = String(formatVersion);
|
|
48
68
|
if (declared === schemaVersion)
|
|
49
69
|
return null;
|
|
@@ -51,6 +71,7 @@ export function releaseSchemaMismatch(sheetType) {
|
|
|
51
71
|
path: "$['format_version']",
|
|
52
72
|
values: {
|
|
53
73
|
declared: `\`format_version\` is \`${declared}\``,
|
|
74
|
+
found: `\`$schema\` URL declares version \`${schemaVersion}\``,
|
|
54
75
|
format_version: declared,
|
|
55
76
|
schema_version: schemaVersion,
|
|
56
77
|
},
|
|
@@ -63,7 +84,7 @@ export const RELEASE_SCHEMA_MISMATCH = {
|
|
|
63
84
|
title: "Release / schema mismatch",
|
|
64
85
|
severity: "error",
|
|
65
86
|
target: "$",
|
|
66
|
-
messageTemplate: "{declared} but
|
|
87
|
+
messageTemplate: "{declared} but {found}.",
|
|
67
88
|
remediation: "Point `$schema` at the sheet-spec version the declared release bundles (see releases.md), or declare the release that bundles the schema the sheet was written against. On a legacy sheet, align `format_version` and the `$schema` version.",
|
|
68
89
|
};
|
|
69
90
|
/** A sheet carrying both `format_version` and `release`. */
|
|
@@ -75,3 +96,22 @@ export const BOTH_VERSION_FIELDS = {
|
|
|
75
96
|
messageTemplate: "Sheet carries both `format_version` and `release`; a sheet declares one.",
|
|
76
97
|
remediation: "Keep `release` and remove `format_version`. A sheet on the legacy draft line keeps `format_version` alone and its legacy `$schema`.",
|
|
77
98
|
};
|
|
99
|
+
/**
|
|
100
|
+
* Unknown release: the sheet declares a `release` the release manifest
|
|
101
|
+
* does not list, so no sheet-spec version and no schema can be resolved
|
|
102
|
+
* for it. Only a string `release` is judged; another JSON type is
|
|
103
|
+
* Layer 1's concern.
|
|
104
|
+
*/
|
|
105
|
+
export const unknownRelease = (_match, ctx) => {
|
|
106
|
+
const { release } = ctx.sheet;
|
|
107
|
+
if (typeof release !== "string" || KNOWN_RELEASES.includes(release))
|
|
108
|
+
return null;
|
|
109
|
+
return { path: "$['release']", values: { release, known: KNOWN_RELEASES.join(", ") } };
|
|
110
|
+
};
|
|
111
|
+
export const UNKNOWN_RELEASE = {
|
|
112
|
+
title: "Unknown release",
|
|
113
|
+
severity: "error",
|
|
114
|
+
target: "$",
|
|
115
|
+
messageTemplate: "`release` at `{path}` is `{release}`, which is not a release this catalogue knows ({known}).",
|
|
116
|
+
remediation: "Declare a published commerce release (see releases.md). If the release is newer than this linter, update the linter.",
|
|
117
|
+
};
|
|
@@ -2,28 +2,41 @@ import type { Sheet } from "@pagefront/lint-core";
|
|
|
2
2
|
import type { SheetType } from "./sheet-type.js";
|
|
3
3
|
/**
|
|
4
4
|
* Which frozen schema a sheet is validated against. The package bundles
|
|
5
|
-
* every schema ever published (commerce/schemas/), keyed by `$id
|
|
6
|
-
* sheet is
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* every schema ever published (commerce/schemas/), keyed by `$id`. A
|
|
6
|
+
* sheet is judged by the release it declares: on the release line the
|
|
7
|
+
* schema is the one the release manifest gives for the declared
|
|
8
|
+
* `release` and the sheet's type, whatever `$schema` says (a `$schema`
|
|
9
|
+
* that disagrees is PDP-E-002's finding, at Layer 2). Legacy sheets,
|
|
10
|
+
* which declare no release, keep resolving through their `$schema`.
|
|
10
11
|
*/
|
|
11
12
|
export type SchemaResolution = {
|
|
12
13
|
kind: "schema";
|
|
13
14
|
id: string;
|
|
14
15
|
}
|
|
15
|
-
/**
|
|
16
|
+
/** `release` names no release in the manifest. */
|
|
16
17
|
| {
|
|
17
|
-
kind: "unknown";
|
|
18
|
+
kind: "unknown-release";
|
|
19
|
+
declared: string;
|
|
20
|
+
}
|
|
21
|
+
/** A legacy sheet's `$schema` names no schema this package bundles. */
|
|
22
|
+
| {
|
|
23
|
+
kind: "unknown-schema";
|
|
18
24
|
declared: string;
|
|
19
25
|
};
|
|
20
26
|
/**
|
|
21
|
-
* Resolve the schema for a document.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
27
|
+
* Resolve the schema for a document.
|
|
28
|
+
*
|
|
29
|
+
* - Release line (`release` present): the sheet-spec version the
|
|
30
|
+
* manifest bundles in that release for the sheet's type. The type
|
|
31
|
+
* comes from the usual dispatch (`$schema` path, else data-block
|
|
32
|
+
* shape), so it does not depend on `$schema` being present.
|
|
33
|
+
* - Legacy line (`format_version`, no `release`): the schema `$schema`
|
|
34
|
+
* names when it is bundled, the legacy schema for the pre-2026-09-08
|
|
35
|
+
* identifier or when `$schema` is absent.
|
|
36
|
+
* - Neither field: the schema `$schema` names when it is bundled, else
|
|
37
|
+
* the current release's, whose `required` then reports the missing
|
|
38
|
+
* `release`.
|
|
39
|
+
*
|
|
40
|
+
* `sheetType` forces the type for callers that validate one type only.
|
|
28
41
|
*/
|
|
29
42
|
export declare function resolveSchema(sheet: Sheet, sheetType?: SheetType): SchemaResolution;
|
package/dist/schema-registry.js
CHANGED
|
@@ -8,28 +8,45 @@ import { detectSheetLine, detectSheetType } from "./sheet-type.js";
|
|
|
8
8
|
*/
|
|
9
9
|
const FORMER_LEGACY_ID = /^https:\/\/themachineweb\.org\/pagefront\/commerce\/schemas\/(product|organization)\/v0\.9\.json$/;
|
|
10
10
|
/**
|
|
11
|
-
* Resolve the schema for a document.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
11
|
+
* Resolve the schema for a document.
|
|
12
|
+
*
|
|
13
|
+
* - Release line (`release` present): the sheet-spec version the
|
|
14
|
+
* manifest bundles in that release for the sheet's type. The type
|
|
15
|
+
* comes from the usual dispatch (`$schema` path, else data-block
|
|
16
|
+
* shape), so it does not depend on `$schema` being present.
|
|
17
|
+
* - Legacy line (`format_version`, no `release`): the schema `$schema`
|
|
18
|
+
* names when it is bundled, the legacy schema for the pre-2026-09-08
|
|
19
|
+
* identifier or when `$schema` is absent.
|
|
20
|
+
* - Neither field: the schema `$schema` names when it is bundled, else
|
|
21
|
+
* the current release's, whose `required` then reports the missing
|
|
22
|
+
* `release`.
|
|
23
|
+
*
|
|
24
|
+
* `sheetType` forces the type for callers that validate one type only.
|
|
18
25
|
*/
|
|
19
26
|
export function resolveSchema(sheet, sheetType) {
|
|
27
|
+
const type = sheetType ?? detectSheetType(sheet);
|
|
20
28
|
const declared = sheet.$schema;
|
|
29
|
+
if (sheet.release !== undefined) {
|
|
30
|
+
// A non-string release is validated against the current schema,
|
|
31
|
+
// whose `release` pattern rejects it.
|
|
32
|
+
if (typeof sheet.release !== "string") {
|
|
33
|
+
return { kind: "schema", id: schemaUrlFor(type, sheetSpecVersionFor(CURRENT_RELEASE, type)) };
|
|
34
|
+
}
|
|
35
|
+
const version = sheetSpecVersionFor(sheet.release, type);
|
|
36
|
+
if (version === undefined)
|
|
37
|
+
return { kind: "unknown-release", declared: sheet.release };
|
|
38
|
+
return { kind: "schema", id: schemaUrlFor(type, version) };
|
|
39
|
+
}
|
|
21
40
|
if (typeof declared === "string") {
|
|
22
41
|
if (SCHEMA_IDS.includes(declared))
|
|
23
42
|
return { kind: "schema", id: declared };
|
|
24
43
|
const former = FORMER_LEGACY_ID.exec(declared);
|
|
25
44
|
if (former !== null)
|
|
26
45
|
return { kind: "schema", id: legacySchemaUrl(former[1]) };
|
|
27
|
-
|
|
46
|
+
if (detectSheetLine(sheet) === "legacy")
|
|
47
|
+
return { kind: "unknown-schema", declared };
|
|
28
48
|
}
|
|
29
|
-
const type = sheetType ?? detectSheetType(sheet);
|
|
30
49
|
if (detectSheetLine(sheet) === "legacy")
|
|
31
50
|
return { kind: "schema", id: legacySchemaUrl(type) };
|
|
32
|
-
|
|
33
|
-
const version = sheetSpecVersionFor(release, type) ?? sheetSpecVersionFor(CURRENT_RELEASE, type);
|
|
34
|
-
return { kind: "schema", id: schemaUrlFor(type, version) };
|
|
51
|
+
return { kind: "schema", id: schemaUrlFor(type, sheetSpecVersionFor(CURRENT_RELEASE, type)) };
|
|
35
52
|
}
|