@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.
Files changed (101) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +118 -0
  3. package/dist/catalogue.d.ts +9 -0
  4. package/dist/catalogue.js +43 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +144 -0
  7. package/dist/index.d.ts +22 -0
  8. package/dist/index.js +18 -0
  9. package/dist/lint-sheet.d.ts +13 -0
  10. package/dist/lint-sheet.js +25 -0
  11. package/dist/organization-catalogue.d.ts +11 -0
  12. package/dist/organization-catalogue.js +21 -0
  13. package/dist/organization-schema.d.ts +9 -0
  14. package/dist/organization-schema.js +441 -0
  15. package/dist/product-schema.d.ts +9 -0
  16. package/dist/product-schema.js +1853 -0
  17. package/dist/rules/org-shared.d.ts +21 -0
  18. package/dist/rules/org-shared.js +44 -0
  19. package/dist/rules/org-w-001.d.ts +12 -0
  20. package/dist/rules/org-w-001.js +31 -0
  21. package/dist/rules/org-w-002.d.ts +12 -0
  22. package/dist/rules/org-w-002.js +32 -0
  23. package/dist/rules/org-w-003.d.ts +11 -0
  24. package/dist/rules/org-w-003.js +30 -0
  25. package/dist/rules/org-w-004.d.ts +12 -0
  26. package/dist/rules/org-w-004.js +32 -0
  27. package/dist/rules/org-w-005.d.ts +2 -0
  28. package/dist/rules/org-w-005.js +54 -0
  29. package/dist/rules/org-w-006.d.ts +13 -0
  30. package/dist/rules/org-w-006.js +32 -0
  31. package/dist/rules/pdp-e-001.d.ts +2 -0
  32. package/dist/rules/pdp-e-001.js +42 -0
  33. package/dist/rules/pdp-e-002.d.ts +13 -0
  34. package/dist/rules/pdp-e-002.js +33 -0
  35. package/dist/rules/pdp-i-001.d.ts +2 -0
  36. package/dist/rules/pdp-i-001.js +28 -0
  37. package/dist/rules/pdp-w-001.d.ts +12 -0
  38. package/dist/rules/pdp-w-001.js +22 -0
  39. package/dist/rules/pdp-w-002.d.ts +13 -0
  40. package/dist/rules/pdp-w-002.js +23 -0
  41. package/dist/rules/pdp-w-003.d.ts +12 -0
  42. package/dist/rules/pdp-w-003.js +22 -0
  43. package/dist/rules/pdp-w-004.d.ts +11 -0
  44. package/dist/rules/pdp-w-004.js +30 -0
  45. package/dist/rules/pdp-w-005.d.ts +18 -0
  46. package/dist/rules/pdp-w-005.js +44 -0
  47. package/dist/rules/pdp-w-006.d.ts +10 -0
  48. package/dist/rules/pdp-w-006.js +31 -0
  49. package/dist/rules/pdp-w-007.d.ts +9 -0
  50. package/dist/rules/pdp-w-007.js +29 -0
  51. package/dist/rules/pdp-w-008.d.ts +19 -0
  52. package/dist/rules/pdp-w-008.js +43 -0
  53. package/dist/rules/pdp-w-009.d.ts +9 -0
  54. package/dist/rules/pdp-w-009.js +29 -0
  55. package/dist/rules/pdp-w-010.d.ts +24 -0
  56. package/dist/rules/pdp-w-010.js +85 -0
  57. package/dist/rules/pdp-w-011.d.ts +2 -0
  58. package/dist/rules/pdp-w-011.js +44 -0
  59. package/dist/rules/pdp-w-012.d.ts +18 -0
  60. package/dist/rules/pdp-w-012.js +51 -0
  61. package/dist/rules/pdp-w-013.d.ts +13 -0
  62. package/dist/rules/pdp-w-013.js +25 -0
  63. package/dist/rules/pdp-w-014.d.ts +26 -0
  64. package/dist/rules/pdp-w-014.js +69 -0
  65. package/dist/rules/pdp-w-015.d.ts +10 -0
  66. package/dist/rules/pdp-w-015.js +33 -0
  67. package/dist/rules/pdp-w-016.d.ts +9 -0
  68. package/dist/rules/pdp-w-016.js +30 -0
  69. package/dist/rules/pdp-w-017.d.ts +2 -0
  70. package/dist/rules/pdp-w-017.js +55 -0
  71. package/dist/rules/pdp-w-018.d.ts +2 -0
  72. package/dist/rules/pdp-w-018.js +32 -0
  73. package/dist/rules/pdp-w-019.d.ts +14 -0
  74. package/dist/rules/pdp-w-019.js +49 -0
  75. package/dist/rules/pdp-w-020.d.ts +2 -0
  76. package/dist/rules/pdp-w-020.js +64 -0
  77. package/dist/rules/pdp-w-021.d.ts +2 -0
  78. package/dist/rules/pdp-w-021.js +53 -0
  79. package/dist/rules/pdp-w-022.d.ts +11 -0
  80. package/dist/rules/pdp-w-022.js +25 -0
  81. package/dist/rules/pdp-w-023.d.ts +2 -0
  82. package/dist/rules/pdp-w-023.js +39 -0
  83. package/dist/rules/pdp-w-024.d.ts +24 -0
  84. package/dist/rules/pdp-w-024.js +58 -0
  85. package/dist/rules/pdp-w-025.d.ts +2 -0
  86. package/dist/rules/pdp-w-025.js +35 -0
  87. package/dist/rules/pdp-w-026.d.ts +10 -0
  88. package/dist/rules/pdp-w-026.js +31 -0
  89. package/dist/rules/pdp-w-027.d.ts +14 -0
  90. package/dist/rules/pdp-w-027.js +47 -0
  91. package/dist/schema-properties.d.ts +10 -0
  92. package/dist/schema-properties.js +67 -0
  93. package/dist/sheet-type.d.ts +16 -0
  94. package/dist/sheet-type.js +20 -0
  95. package/dist/validate-and-lint.d.ts +47 -0
  96. package/dist/validate-and-lint.js +53 -0
  97. package/dist/validate-organization.d.ts +27 -0
  98. package/dist/validate-organization.js +30 -0
  99. package/dist/version.d.ts +8 -0
  100. package/dist/version.js +8 -0
  101. package/package.json +50 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * PDP-W-027 — Empty accepted answer.
3
+ * Transcribed from commerce/spec/rules.md (normative).
4
+ *
5
+ * The target selects `acceptedAnswer` objects, so presence is given by
6
+ * the match; the check tests `text` (absent, empty, or whitespace-only
7
+ * — the schema requires non-empty `text`, so only the whitespace arm
8
+ * is reachable on Layer-1-valid input; the others are defensive).
9
+ * `{name}` is the parent question's `name` (schema-required), read
10
+ * from the sheet via the match's path index; `(unnamed)` defensively
11
+ * on invalid input.
12
+ */
13
+ export const pdpW027 = {
14
+ id: "PDP-W-027",
15
+ title: "Empty accepted answer",
16
+ severity: "warning",
17
+ target: "$.data.question[*].acceptedAnswer",
18
+ check: (match, ctx) => {
19
+ const answer = match.value;
20
+ if (typeof answer !== "object" || answer === null)
21
+ return null;
22
+ const text = answer["text"];
23
+ const empty = text === undefined ||
24
+ text === null ||
25
+ (typeof text === "string" && text.trim() === "");
26
+ if (!empty)
27
+ return null;
28
+ let name = "(unnamed)";
29
+ const index = /\['question'\]\[(\d+)\]/.exec(match.path)?.[1];
30
+ const data = typeof ctx.sheet.data === "object" && ctx.sheet.data !== null
31
+ ? ctx.sheet.data
32
+ : {};
33
+ if (index !== undefined && Array.isArray(data["question"])) {
34
+ const question = data["question"][Number(index)];
35
+ if (typeof question === "object" &&
36
+ question !== null &&
37
+ typeof question["name"] === "string") {
38
+ name = question["name"];
39
+ }
40
+ }
41
+ return { values: { name } };
42
+ },
43
+ messageTemplate: "Question `{name}` at `{path}` has an `acceptedAnswer` with no text.",
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
+ introduced: "v0.8",
46
+ specReference: "product.md#product-qa",
47
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The data-block property inventory for PDP-I-001, derived from the
3
+ * Pagefront JSON Schema (commerce/spec/product.json, format 0.9) —
4
+ * the normative property inventory per the catalogue's Definitions
5
+ * section. Generated from `$defs.dataBlock.properties`; the
6
+ * inventory drift test (test/inventory.test.mjs) fails when the
7
+ * schema and this list diverge. Regenerate on schema change, in the
8
+ * same catalogue-versioned change.
9
+ */
10
+ export declare const DATA_BLOCK_PROPERTIES: readonly string[];
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The data-block property inventory for PDP-I-001, derived from the
3
+ * Pagefront JSON Schema (commerce/spec/product.json, format 0.9) —
4
+ * the normative property inventory per the catalogue's Definitions
5
+ * section. Generated from `$defs.dataBlock.properties`; the
6
+ * inventory drift test (test/inventory.test.mjs) fails when the
7
+ * schema and this list diverge. Regenerate on schema change, in the
8
+ * same catalogue-versioned change.
9
+ */
10
+ export const DATA_BLOCK_PROPERTIES = [
11
+ "@context",
12
+ "@id",
13
+ "@type",
14
+ "additionalProperty",
15
+ "aggregateRating",
16
+ "brand",
17
+ "category",
18
+ "color",
19
+ "countryOfOrigin",
20
+ "depth",
21
+ "description",
22
+ "gtin",
23
+ "gtin13",
24
+ "hasCertification",
25
+ "hasPart",
26
+ "hasVariant",
27
+ "height",
28
+ "image",
29
+ "inLanguage",
30
+ "isPartOf",
31
+ "isRelatedTo",
32
+ "isSimilarTo",
33
+ "isVariantOf",
34
+ "manufacturer",
35
+ "material",
36
+ "mpn",
37
+ "name",
38
+ "offers",
39
+ "pagefront:availableLanguages",
40
+ "pagefront:careInstructions",
41
+ "pagefront:configurator",
42
+ "pagefront:construction",
43
+ "pagefront:document",
44
+ "pagefront:fabricComposition",
45
+ "pagefront:featureGroup",
46
+ "pagefront:gemstone",
47
+ "pagefront:legalEntity",
48
+ "pagefront:madeToOrderTerms",
49
+ "pagefront:manufacturingChain",
50
+ "pagefront:materialComposition",
51
+ "pagefront:productHighlights",
52
+ "pagefront:relatedProduct",
53
+ "pagefront:safetyNotice",
54
+ "pagefront:safetyWarning",
55
+ "pagefront:sizeChart",
56
+ "pagefront:sizeRecommendation",
57
+ "productGroupID",
58
+ "productID",
59
+ "question",
60
+ "review",
61
+ "sku",
62
+ "url",
63
+ "variesBy",
64
+ "video",
65
+ "weight",
66
+ "width",
67
+ ];
@@ -0,0 +1,16 @@
1
+ import type { Sheet } from "@pagefront/lint-core";
2
+ /**
3
+ * Sheet-type detection for page-type dispatch: organization sheets are
4
+ * a different page type from product sheets, and each page type's
5
+ * rules must never run on the other's sheets.
6
+ *
7
+ * The envelope `$schema` is authoritative when it names a page type
8
+ * (`/schemas/organization/` or `/schemas/product/` in its path). The
9
+ * data-block fallback — `@type` of `Organization` / `OnlineStore` /
10
+ * `OnlineBusiness` with `pagefront:commerceRole` present — applies
11
+ * only when `$schema` is absent or names neither, so a declared schema
12
+ * always wins over shape heuristics. The default is `product`, the
13
+ * page type the catalogue predates dispatch with.
14
+ */
15
+ export type SheetType = "product" | "organization";
16
+ export declare function detectSheetType(sheet: Sheet): SheetType;
@@ -0,0 +1,20 @@
1
+ const ORG_DATA_TYPES = new Set(["Organization", "OnlineStore", "OnlineBusiness"]);
2
+ export function detectSheetType(sheet) {
3
+ const schema = sheet.$schema;
4
+ if (typeof schema === "string") {
5
+ if (schema.includes("/schemas/organization/"))
6
+ return "organization";
7
+ if (schema.includes("/schemas/product/"))
8
+ return "product";
9
+ }
10
+ const data = sheet.data;
11
+ if (typeof data === "object" && data !== null && !Array.isArray(data)) {
12
+ const d = data;
13
+ if (typeof d["@type"] === "string" &&
14
+ ORG_DATA_TYPES.has(d["@type"]) &&
15
+ d["pagefront:commerceRole"] !== undefined) {
16
+ return "organization";
17
+ }
18
+ }
19
+ return "product";
20
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The Layer 1 → Layer 2 sequencing as a library function — the same
3
+ * pipeline the CLI runs, importable by CI scripts and production
4
+ * validation layers. Ajv is an implementation detail behind this
5
+ * boundary: callers see `SchemaError`, never Ajv's own error type.
6
+ */
7
+ import type { LintOptions, Report } from "@pagefront/lint-core";
8
+ /**
9
+ * One Layer 1 (JSON Schema) validation failure, in a shape this
10
+ * package owns. `instancePath` is a JSON Pointer to the offending
11
+ * location (`""` for the document root); `keyword` is the schema
12
+ * keyword that failed (e.g. `"required"`, `"format"`).
13
+ */
14
+ export interface SchemaError {
15
+ instancePath: string;
16
+ message: string;
17
+ keyword: string;
18
+ }
19
+ export type ValidateAndLintResult = {
20
+ layer: 1;
21
+ schemaErrors: SchemaError[];
22
+ } | {
23
+ layer: 2;
24
+ report: Report;
25
+ };
26
+ /**
27
+ * Validate a parsed-but-unvalidated document against the bundled
28
+ * product JSON Schema (Layer 1), and — only if it passes — lint it
29
+ * against the commerce rule catalogue (Layer 2). The engine assumes a
30
+ * schema-valid Sheet, so a failing document never reaches the
31
+ * catalogue; Layer 1 success is also what makes the internal cast to
32
+ * `Sheet` sound (the schema requires the envelope fields).
33
+ *
34
+ * The bundled schemas are the current catalogue's format version. A
35
+ * document declaring some other `format_version` failing Layer 1 is
36
+ * the intended behavior, not a gap: this package lints exactly one
37
+ * format version, and there is no separate "unsupported version"
38
+ * outcome.
39
+ *
40
+ * Dispatches by page type (see sheet-type.ts): product sheets validate
41
+ * against the product schema and lint with the product catalogue;
42
+ * Commerce Organization Sheets validate against the organization
43
+ * schema and lint with the organization catalogue. Detection reads the
44
+ * envelope `$schema` first, falling back to data-block shape, so a
45
+ * document of either type routes to its own schema's errors.
46
+ */
47
+ export declare function validateAndLint(input: unknown, options?: LintOptions): ValidateAndLintResult;
@@ -0,0 +1,53 @@
1
+ import { lintSheet } from "./lint-sheet.js";
2
+ import { PRODUCT_SCHEMA } from "./product-schema.js";
3
+ import { ORGANIZATION_SCHEMA } from "./organization-schema.js";
4
+ import { detectSheetType } from "./sheet-type.js";
5
+ // ajv ships CJS with `module.exports === exports.default`; under
6
+ // NodeNext the default import binding is the CJS namespace, so the
7
+ // class/function is reached via `.default` (identical at runtime).
8
+ import Ajv2020Module from "ajv/dist/2020.js";
9
+ import addFormatsModule from "ajv-formats";
10
+ const Ajv2020 = Ajv2020Module.default;
11
+ const addFormats = addFormatsModule.default;
12
+ // Compiled once at module scope: pipeline consumers call
13
+ // validateAndLint in a loop, and schema compilation is the expensive
14
+ // step. `allowUnionTypes` because the product schema legitimately
15
+ // uses union `type` keywords.
16
+ const ajv = new Ajv2020({ allErrors: true, allowUnionTypes: true });
17
+ addFormats(ajv);
18
+ const validateProduct = ajv.compile(PRODUCT_SCHEMA);
19
+ const validateOrganization = ajv.compile(ORGANIZATION_SCHEMA);
20
+ /**
21
+ * Validate a parsed-but-unvalidated document against the bundled
22
+ * product JSON Schema (Layer 1), and — only if it passes — lint it
23
+ * against the commerce rule catalogue (Layer 2). The engine assumes a
24
+ * schema-valid Sheet, so a failing document never reaches the
25
+ * catalogue; Layer 1 success is also what makes the internal cast to
26
+ * `Sheet` sound (the schema requires the envelope fields).
27
+ *
28
+ * The bundled schemas are the current catalogue's format version. A
29
+ * document declaring some other `format_version` failing Layer 1 is
30
+ * the intended behavior, not a gap: this package lints exactly one
31
+ * format version, and there is no separate "unsupported version"
32
+ * outcome.
33
+ *
34
+ * Dispatches by page type (see sheet-type.ts): product sheets validate
35
+ * against the product schema and lint with the product catalogue;
36
+ * Commerce Organization Sheets validate against the organization
37
+ * schema and lint with the organization catalogue. Detection reads the
38
+ * envelope `$schema` first, falling back to data-block shape, so a
39
+ * document of either type routes to its own schema's errors.
40
+ */
41
+ export function validateAndLint(input, options) {
42
+ const sheetType = detectSheetType((typeof input === "object" && input !== null ? input : {}));
43
+ const validate = sheetType === "organization" ? validateOrganization : validateProduct;
44
+ if (!validate(input)) {
45
+ const schemaErrors = (validate.errors ?? []).map((error) => ({
46
+ instancePath: error.instancePath,
47
+ message: error.message ?? "invalid",
48
+ keyword: error.keyword,
49
+ }));
50
+ return { layer: 1, schemaErrors };
51
+ }
52
+ return { layer: 2, report: lintSheet(input, options) };
53
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Layer 1 validation for the Commerce Organization Sheet — the
3
+ * organization analogue of validateAndLint's first stage. There is
4
+ * deliberately no Layer 2 here: the commerce rule catalogue is
5
+ * product-scoped, and an organization catalogue does not exist yet
6
+ * (dispatching per-page-type schemas and catalogues is tracked as
7
+ * "Rule namespaces" in core/spec/linter.md), so a schema-valid
8
+ * document simply passes.
9
+ *
10
+ * As with the product pipeline, Ajv is an implementation detail:
11
+ * callers see `SchemaError`, never Ajv's own error type.
12
+ */
13
+ import type { SchemaError } from "./validate-and-lint.js";
14
+ export type ValidateOrganizationResult = {
15
+ valid: true;
16
+ } | {
17
+ valid: false;
18
+ schemaErrors: SchemaError[];
19
+ };
20
+ /**
21
+ * Validate a parsed-but-unvalidated document against the bundled
22
+ * Commerce Organization Sheet JSON Schema. The bundled schema is the
23
+ * current format version; a document declaring some other
24
+ * `format_version` failing Layer 1 is the intended behavior, exactly
25
+ * as in the product pipeline.
26
+ */
27
+ export declare function validateOrganizationSheet(input: unknown): ValidateOrganizationResult;
@@ -0,0 +1,30 @@
1
+ import { ORGANIZATION_SCHEMA } from "./organization-schema.js";
2
+ // ajv ships CJS with `module.exports === exports.default`; under
3
+ // NodeNext the default import binding is the CJS namespace, so the
4
+ // class/function is reached via `.default` (identical at runtime).
5
+ import Ajv2020Module from "ajv/dist/2020.js";
6
+ import addFormatsModule from "ajv-formats";
7
+ const Ajv2020 = Ajv2020Module.default;
8
+ const addFormats = addFormatsModule.default;
9
+ // Compiled once at module scope, mirroring validate-and-lint.
10
+ const ajv = new Ajv2020({ allErrors: true, allowUnionTypes: true });
11
+ addFormats(ajv);
12
+ const validate = ajv.compile(ORGANIZATION_SCHEMA);
13
+ /**
14
+ * Validate a parsed-but-unvalidated document against the bundled
15
+ * Commerce Organization Sheet JSON Schema. The bundled schema is the
16
+ * current format version; a document declaring some other
17
+ * `format_version` failing Layer 1 is the intended behavior, exactly
18
+ * as in the product pipeline.
19
+ */
20
+ export function validateOrganizationSheet(input) {
21
+ if (!validate(input)) {
22
+ const schemaErrors = (validate.errors ?? []).map((error) => ({
23
+ instancePath: error.instancePath,
24
+ message: error.message ?? "invalid",
25
+ keyword: error.keyword,
26
+ }));
27
+ return { valid: false, schemaErrors };
28
+ }
29
+ return { valid: true };
30
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This package's version, as a generated constant so the library
3
+ * entry stays free of Node built-ins (browser consumers bundle it).
4
+ * Must match package.json's `version` — the drift test
5
+ * (test/linter-version.test.mjs) fails when they diverge. Regenerate
6
+ * on version bump.
7
+ */
8
+ export declare const PACKAGE_VERSION = "0.1.0";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This package's version, as a generated constant so the library
3
+ * entry stays free of Node built-ins (browser consumers bundle it).
4
+ * Must match package.json's `version` — the drift test
5
+ * (test/linter-version.test.mjs) fails when they diverge. Regenerate
6
+ * on version bump.
7
+ */
8
+ export const PACKAGE_VERSION = "0.1.0";
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@pagefront/lint-commerce",
3
+ "version": "0.1.0",
4
+ "description": "Pagefront commerce linter: the commerce rule catalogue and CLI, on the @pagefront/lint-core engine.",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/themachineweb/pagefront.git",
9
+ "directory": "commerce/lint"
10
+ },
11
+ "homepage": "https://github.com/themachineweb/pagefront/tree/main/commerce/lint#readme",
12
+ "bugs": "https://github.com/themachineweb/pagefront/issues",
13
+ "keywords": [
14
+ "pagefront",
15
+ "linter",
16
+ "commerce",
17
+ "product",
18
+ "schema.org",
19
+ "json-ld",
20
+ "structured-data"
21
+ ],
22
+ "type": "module",
23
+ "main": "dist/index.js",
24
+ "types": "dist/index.d.ts",
25
+ "bin": {
26
+ "pagefront-lint-commerce": "dist/cli.js"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc",
38
+ "test": "tsc && node --test test/*.test.mjs",
39
+ "prepublishOnly": "npm test"
40
+ },
41
+ "dependencies": {
42
+ "@pagefront/lint-core": "^0.1.0",
43
+ "ajv": "^8.20.0",
44
+ "ajv-formats": "^3.0.1"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^26.2.0",
48
+ "typescript": "^5.0.0"
49
+ }
50
+ }