@pantheon-systems/p1-content-validator 1.0.0 → 2.0.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/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { validateOps } from './validator.js';
2
2
  export { fetchRegistry, listRegistryVersions, snapshotToComponentSchema } from './registry.js';
3
- export type { EditOperation, ComponentSchema, ComponentField, FieldOption, ValidationError, ValidateInput, FetchRegistryOpts, } from './types.js';
3
+ export { validateDocumentStructure } from './structure-validator.js';
4
+ export type { EditOperation, ComponentSchema, ComponentField, FieldOption, ValidationError, ValidateInput, FetchRegistryOpts, TemplateComponent, TemplateSnapshot, StructuralConformanceError, ValidateStructureInput, } from './types.js';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC/F,YAAY,EACV,aAAa,EACb,eAAe,EACf,cAAc,EACd,WAAW,EACX,eAAe,EACf,aAAa,EACb,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC/F,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AACrE,YAAY,EACV,aAAa,EACb,eAAe,EACf,cAAc,EACd,WAAW,EACX,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,0BAA0B,EAC1B,sBAAsB,GACvB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { validateOps } from './validator.js';
2
2
  export { fetchRegistry, listRegistryVersions, snapshotToComponentSchema } from './registry.js';
3
+ export { validateDocumentStructure } from './structure-validator.js';
@@ -0,0 +1,26 @@
1
+ import type { StructuralConformanceError, ValidateStructureInput } from './types.js';
2
+ /**
3
+ * Validates that a document snapshot conforms to a template's structural skeleton.
4
+ *
5
+ * A template component is pinned when `root.props._pinMap[props.id]` is `true`;
6
+ * pinned types are checked in template `content` order.
7
+ *
8
+ * Conformance rules:
9
+ * 1. All pinned components must be present in the document
10
+ * 2. Pinned components must appear in the same relative order as in the template
11
+ * 3. Non-pinned components are allowed and do not affect conformance
12
+ *
13
+ * This implements partial conformance: documents may have additional components
14
+ * beyond the pinned skeleton without failing validation.
15
+ *
16
+ * DEFENSIVE DESIGN: This function is designed to never crash, even with malformed
17
+ * inputs. All property accesses use optional chaining, and all array operations
18
+ * verify types before use.
19
+ *
20
+ * @param input - Document snapshot and template snapshot to validate against
21
+ * @returns Object with array of structural conformance errors (empty if valid)
22
+ */
23
+ export declare function validateDocumentStructure(input: ValidateStructureInput): {
24
+ errors: StructuralConformanceError[];
25
+ };
26
+ //# sourceMappingURL=structure-validator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structure-validator.d.ts","sourceRoot":"","sources":["../src/structure-validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAC1B,sBAAsB,EACvB,MAAM,YAAY,CAAC;AASpB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,sBAAsB,GAC5B;IAAE,MAAM,EAAE,0BAA0B,EAAE,CAAA;CAAE,CAoG1C"}
@@ -0,0 +1,118 @@
1
+ /** Returns the value when it is a plain object, otherwise undefined. */
2
+ function asRecord(value) {
3
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
4
+ ? value
5
+ : undefined;
6
+ }
7
+ /**
8
+ * Validates that a document snapshot conforms to a template's structural skeleton.
9
+ *
10
+ * A template component is pinned when `root.props._pinMap[props.id]` is `true`;
11
+ * pinned types are checked in template `content` order.
12
+ *
13
+ * Conformance rules:
14
+ * 1. All pinned components must be present in the document
15
+ * 2. Pinned components must appear in the same relative order as in the template
16
+ * 3. Non-pinned components are allowed and do not affect conformance
17
+ *
18
+ * This implements partial conformance: documents may have additional components
19
+ * beyond the pinned skeleton without failing validation.
20
+ *
21
+ * DEFENSIVE DESIGN: This function is designed to never crash, even with malformed
22
+ * inputs. All property accesses use optional chaining, and all array operations
23
+ * verify types before use.
24
+ *
25
+ * @param input - Document snapshot and template snapshot to validate against
26
+ * @returns Object with array of structural conformance errors (empty if valid)
27
+ */
28
+ export function validateDocumentStructure(input) {
29
+ const { documentSnapshot, templateSnapshot } = input;
30
+ const errors = [];
31
+ // Puck snapshots store the component array at top-level `content`, but some
32
+ // wrappers nest it under `root.props.content`. Check both, fall back to empty.
33
+ const topLevelContent = documentSnapshot?.content;
34
+ const contentRaw = Array.isArray(topLevelContent)
35
+ ? topLevelContent
36
+ : asRecord(asRecord(documentSnapshot?.root)?.props)?.content;
37
+ const content = Array.isArray(contentRaw)
38
+ ? contentRaw
39
+ : [];
40
+ const templateContent = Array.isArray(templateSnapshot?.content)
41
+ ? templateSnapshot.content
42
+ : [];
43
+ // A component is pinned only when root.props._pinMap[props.id] is true.
44
+ const pinMap = asRecord(asRecord(asRecord(templateSnapshot?.root)?.props)?._pinMap) ?? {};
45
+ // Pinned component types in template content order.
46
+ // A component without a string type or string id is never pinned.
47
+ const pinnedTypes = [];
48
+ for (const templateComponent of templateContent) {
49
+ if (!templateComponent || typeof templateComponent.type !== 'string') {
50
+ continue;
51
+ }
52
+ const id = asRecord(templateComponent.props)?.id;
53
+ if (typeof id === 'string' && pinMap[id] === true) {
54
+ pinnedTypes.push(templateComponent.type);
55
+ }
56
+ }
57
+ // If template has no pinned components, document always conforms
58
+ if (pinnedTypes.length === 0) {
59
+ return { errors };
60
+ }
61
+ // Build a map of pinned component types to their indices in the document
62
+ // Defensively handle components without type field or with non-string types
63
+ const pinnedIndices = new Map();
64
+ content.forEach((component, index) => {
65
+ // Skip components without a valid type field
66
+ if (!component || typeof component.type !== 'string') {
67
+ return;
68
+ }
69
+ if (pinnedTypes.includes(component.type)) {
70
+ const indices = pinnedIndices.get(component.type) || [];
71
+ indices.push(index);
72
+ pinnedIndices.set(component.type, indices);
73
+ }
74
+ });
75
+ // Track the last index we successfully matched to ensure ordering
76
+ let lastFoundIndex = -1;
77
+ // Check each pinned component in template order
78
+ for (let i = 0; i < pinnedTypes.length; i++) {
79
+ const expectedType = pinnedTypes[i];
80
+ const indices = pinnedIndices.get(expectedType) || [];
81
+ // Find the first occurrence of this component type after lastFoundIndex
82
+ const foundIndex = indices.find((idx) => idx > lastFoundIndex);
83
+ if (foundIndex === undefined) {
84
+ // No unconsumed occurrence appears after the last match. If the document
85
+ // holds fewer instances of this type than the template pins up to here,
86
+ // a required instance is absent; otherwise the instances that exist sit
87
+ // before where this one must appear.
88
+ let requiredSoFar = 0;
89
+ for (let j = 0; j <= i; j++) {
90
+ if (pinnedTypes[j] === expectedType)
91
+ requiredSoFar++;
92
+ }
93
+ if (indices.length < requiredSoFar) {
94
+ errors.push({
95
+ code: 'missing_pinned_component',
96
+ componentType: expectedType,
97
+ message: `Required component "${expectedType}" is missing from the document.`,
98
+ });
99
+ }
100
+ else {
101
+ // Component exists but is out of order (appears before lastFoundIndex)
102
+ errors.push({
103
+ code: 'pinned_component_out_of_order',
104
+ componentType: expectedType,
105
+ expectedIndex: i,
106
+ actualIndex: indices[0],
107
+ message: `Pinned component "${expectedType}" appears out of order. ` +
108
+ `Expected after index ${lastFoundIndex} but found at index ${indices[0]}.`,
109
+ });
110
+ }
111
+ }
112
+ else {
113
+ // Component found in correct relative order
114
+ lastFoundIndex = foundIndex;
115
+ }
116
+ }
117
+ return { errors };
118
+ }
package/dist/types.d.ts CHANGED
@@ -44,4 +44,36 @@ export interface ValidateInput {
44
44
  warnOnZonesUsage?: boolean;
45
45
  };
46
46
  }
47
+ export interface TemplateComponent {
48
+ type: string;
49
+ /** Component props are the template's default props. `id` keys into `_pinMap`. */
50
+ props: {
51
+ id?: string;
52
+ [key: string]: unknown;
53
+ };
54
+ }
55
+ /** A template's version snapshot: Puck data, identical in shape to a page. */
56
+ export interface TemplateSnapshot {
57
+ content: TemplateComponent[];
58
+ root: {
59
+ props: {
60
+ _template?: Record<string, unknown>;
61
+ /** Component id to pinned flag. A component is pinned only when its entry is `true`. */
62
+ _pinMap?: Record<string, boolean>;
63
+ [key: string]: unknown;
64
+ };
65
+ };
66
+ zones?: Record<string, unknown>;
67
+ }
68
+ export interface StructuralConformanceError {
69
+ code: 'missing_pinned_component' | 'pinned_component_out_of_order' | 'unexpected_component_at_pinned_slot';
70
+ message: string;
71
+ componentType: string;
72
+ expectedIndex?: number;
73
+ actualIndex?: number;
74
+ }
75
+ export interface ValidateStructureInput {
76
+ documentSnapshot: Record<string, unknown>;
77
+ templateSnapshot: TemplateSnapshot;
78
+ }
47
79
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;IAClC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EACA,wBAAwB,GACxB,kBAAkB,GAClB,oBAAoB,GACpB,uBAAuB,GACvB,sBAAsB,GACtB,wBAAwB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC1C,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,gBAAgB,CAAC,EAAE,OAAO,CAAC;KAC5B,CAAC;CACH"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pantheon-systems/p1-content-validator",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Validates Puck component edit operations against the CSS component registry",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,6 +28,11 @@
28
28
  "engines": {
29
29
  "node": ">=20.0.0"
30
30
  },
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "registry": "https://registry.npmjs.org/",
34
+ "provenance": false
35
+ },
31
36
  "license": "MIT",
32
37
  "scripts": {
33
38
  "build": "tsc",