@_linked/core 2.31.0 → 2.33.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 (43) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +61 -61
  3. package/lib/esm/index.d.ts +1 -0
  4. package/lib/esm/index.js.map +1 -1
  5. package/lib/esm/ontologies/linked-core.d.ts +1 -0
  6. package/lib/esm/ontologies/linked-core.js +8 -2
  7. package/lib/esm/ontologies/linked-core.js.map +1 -1
  8. package/lib/esm/ontologies/shacl.d.ts +3 -0
  9. package/lib/esm/ontologies/shacl.js +6 -0
  10. package/lib/esm/ontologies/shacl.js.map +1 -1
  11. package/lib/esm/shapes/List.class.d.ts +3 -3
  12. package/lib/esm/shapes/List.class.js +3 -3
  13. package/lib/esm/shapes/List.class.js.map +1 -1
  14. package/lib/esm/shapes/PathList.d.ts +5 -8
  15. package/lib/esm/shapes/PathList.js +6 -9
  16. package/lib/esm/shapes/PathList.js.map +1 -1
  17. package/lib/esm/shapes/PathNode.d.ts +1 -3
  18. package/lib/esm/shapes/PathNode.js +6 -8
  19. package/lib/esm/shapes/PathNode.js.map +1 -1
  20. package/lib/esm/shapes/SHACL.d.ts +15 -13
  21. package/lib/esm/shapes/SHACL.js +8 -0
  22. package/lib/esm/shapes/SHACL.js.map +1 -1
  23. package/lib/esm/shapes/nodeShapeData.d.ts +4 -1
  24. package/lib/esm/shapes/nodeShapeData.js.map +1 -1
  25. package/lib/esm/shapes/singleOwnerConstraint.d.ts +5 -7
  26. package/lib/esm/shapes/singleOwnerConstraint.js +65 -21
  27. package/lib/esm/shapes/singleOwnerConstraint.js.map +1 -1
  28. package/lib/esm/shapes/syncShapes.d.ts +54 -3
  29. package/lib/esm/shapes/syncShapes.js +141 -13
  30. package/lib/esm/shapes/syncShapes.js.map +1 -1
  31. package/lib/esm/sparql/irToAlgebra.d.ts +15 -38
  32. package/lib/esm/sparql/irToAlgebra.js +406 -391
  33. package/lib/esm/sparql/irToAlgebra.js.map +1 -1
  34. package/lib/esm/sparql/ownedLinkCheck.d.ts +6 -10
  35. package/lib/esm/sparql/ownedLinkCheck.js +10 -11
  36. package/lib/esm/sparql/ownedLinkCheck.js.map +1 -1
  37. package/lib/esm/utils/Package.d.ts +3 -2
  38. package/lib/esm/utils/Package.js +21 -8
  39. package/lib/esm/utils/Package.js.map +1 -1
  40. package/lib/esm/utils/sha256.d.ts +8 -0
  41. package/lib/esm/utils/sha256.js +80 -0
  42. package/lib/esm/utils/sha256.js.map +1 -0
  43. package/package.json +1 -1
@@ -6,12 +6,18 @@
6
6
  import { getPropertyShapes } from './nodeShapeData.js';
7
7
  import { getSimplePathId } from '../paths/normalizePropertyPath.js';
8
8
  import { serializePathToNodeData } from './serializePathToNodeData.js';
9
+ import { getSubShapes } from '../utils/ShapeClass.js';
10
+ import { NodeShape } from './SHACL.js';
11
+ import { PathList } from './PathList.js';
12
+ import { rdf } from '../ontologies/rdf.js';
9
13
  /**
10
14
  * The single-owner SHACL constraints generated from `contains` properties.
11
15
  *
12
- * A `contains` property says the subject owns the value node. For every such property, shape
13
- * sync writes one generated NodeShape stating the matching store-side rule: every object of the
14
- * property's predicate has at most one subject pointing at it through that predicate.
16
+ * A `contains` property says the subject owns the value node, and only that property does: a
17
+ * plain property on the same predicate is a reference. For every such property, shape sync
18
+ * writes one generated NodeShape stating the matching store-side rule: every object of the
19
+ * property's predicate has at most one subject pointing at it through that predicate *that is
20
+ * an instance of the declaring class or of a registered sub-shape's class*.
15
21
  *
16
22
  * ```turtle
17
23
  * <…/Thing/image.singleOwner> a sh:NodeShape ;
@@ -19,19 +25,26 @@ import { serializePathToNodeData } from './serializePathToNodeData.js';
19
25
  * sh:targetObjectsOf schema:image ;
20
26
  * sh:property <…/Thing/image.singleOwner/owners> .
21
27
  * <…/Thing/image.singleOwner/owners> a sh:PropertyShape ;
22
- * sh:maxCount 1 ;
23
- * sh:path [ sh:inversePath schema:image ] .
28
+ * sh:path [ sh:inversePath schema:image ] ;
29
+ * sh:qualifiedValueShape [ sh:or ( [ sh:class schema:Thing ] [ sh:class ex:Project ] ) ] ;
30
+ * sh:qualifiedMaxCount 1 .
24
31
  * ```
25
32
  *
33
+ * The owner classes are enumerated rather than left to `sh:class` alone, because `sh:class`
34
+ * matches a subclass instance only through `rdfs:subClassOf` triples, which a dataset without
35
+ * inference does not have. With one class the qualified shape is `[ sh:class C ]`. Sub-shapes
36
+ * registered only in another process are missing until a process that knows them syncs.
37
+ *
26
38
  * One constraint per accessor, written only by the class that declares it: subclasses inherit
27
- * the property but add no constraint, since the rule targets the predicate's objects whoever the
28
- * subject is. The declaring shape owns its constraints through `core:generatedShape`
29
- * (`NodeShape.generatedShapes`, `contains`): shape sync writes them nested in the shape's
30
- * create and deletes them with the shape.
39
+ * the property and are counted as owners, but add no constraint. A declaring shape without a
40
+ * `targetClass` gets none (its owners cannot be named). The declaring shape owns its
41
+ * constraints through `core:generatedShape` (`NodeShape.generatedShapes`, `contains`): shape sync
42
+ * writes them nested in the shape's create and deletes them with the shape; their qualified
43
+ * shape and `sh:or` list are blank nodes, deleted with the property shape that holds them.
31
44
  *
32
- * Non-owning references through other predicates are not counted, and neither is ownership
33
- * through two different `contains` predicates (a documented limitation). The shapes are built
34
- * only at sync time and are never registered in the in-memory shape registry.
45
+ * Ownership through two different `contains` predicates is not counted (a documented
46
+ * limitation). The shapes are built only at sync time and are never registered in the
47
+ * in-memory shape registry.
35
48
  */
36
49
  /** Suffix that turns a property shape's IRI into its generated single-owner shape's IRI. */
37
50
  const SINGLE_OWNER_SUFFIX = '.singleOwner';
@@ -43,26 +56,56 @@ export function singleOwnerShapeIri(propertyShapeIri) {
43
56
  return `${propertyShapeIri}${SINGLE_OWNER_SUFFIX}`;
44
57
  }
45
58
  /**
46
- * The shape's OWN `contains` properties that get a single-owner constraint: those that declare
47
- * a value shape and have a simple path, in declaration order.
59
+ * The shape's OWN `contains` properties that get a single-owner constraint: those with a simple
60
+ * path (every decorated `contains` property has a value shape), in declaration order.
48
61
  *
49
62
  * Inherited properties are not included; their declaring shape writes their constraint.
50
- * Complex paths are skipped, as the containment cascade skips them. A `contains` property
51
- * without a value shape is a polymorphic slot (like `sh:path`) whose value may be a shared IRI,
52
- * so the replace, the subject delete and the class check do not treat it as owning that value,
53
- * and neither does the single-owner rule.
63
+ * Complex paths are skipped, as the owned walk skips them.
54
64
  */
55
65
  export function singleOwnerProperties(nodeShape) {
56
66
  return getPropertyShapes(nodeShape, false).filter((ps) => ps.contains && !!ps.valueShape && !!getSimplePathId(ps.path));
57
67
  }
68
+ /** The classes whose instances own through the shape's properties: its own and every sub-shape's. */
69
+ function ownerClasses(nodeShape) {
70
+ var _a;
71
+ const classes = new Set();
72
+ for (const shape of [nodeShape, ...getSubShapes(nodeShape)]) {
73
+ if ((_a = shape.targetClass) === null || _a === void 0 ? void 0 : _a.id)
74
+ classes.add(shape.targetClass.id);
75
+ }
76
+ return [...classes].sort();
77
+ }
78
+ /** `[ sh:class C ]`, or `[ sh:or ( [ sh:class A ] … ) ]` for several classes. Blank once written. */
79
+ function ownerShapeData(classes) {
80
+ const classShape = (id) => ({ shape: NodeShape, class: { id } });
81
+ if (classes.length === 1)
82
+ return classShape(classes[0]);
83
+ const list = classes.reduceRight((rest, id) => ({ shape: PathList, first: classShape(id), rest }), { id: rdf.nil.id });
84
+ return { shape: NodeShape, orShapes: list };
85
+ }
86
+ const warnedWithoutTargetClass = new Set();
58
87
  /**
59
88
  * Create-pipeline data for the generated single-owner NodeShapes of `nodeShape`, one per
60
89
  * {@link singleOwnerProperties} entry. Each is stored at
61
90
  * {@link singleOwnerShapeIri}`(${shapeIri}/${label})`: the property shape's IRI exactly as shape
62
- * sync writes it (`buildPropertyShapeData`).
91
+ * sync writes it (`buildPropertyShapeData`). Empty, with one warning per shape, when the shape
92
+ * has `contains` properties but no `targetClass`.
63
93
  */
64
94
  export function buildSingleOwnerConstraints(nodeShape, shapeIri) {
65
- return singleOwnerProperties(nodeShape).flatMap((ps) => {
95
+ var _a;
96
+ const properties = singleOwnerProperties(nodeShape);
97
+ if (properties.length === 0)
98
+ return [];
99
+ if (!((_a = nodeShape.targetClass) === null || _a === void 0 ? void 0 : _a.id)) {
100
+ if (!warnedWithoutTargetClass.has(nodeShape.id)) {
101
+ warnedWithoutTargetClass.add(nodeShape.id);
102
+ console.warn(`[linked] Shape '${nodeShape.id}' has contains properties but no targetClass, so shape ` +
103
+ 'sync writes no single-owner constraint for them: its owners cannot be named.');
104
+ }
105
+ return [];
106
+ }
107
+ const owners = ownerShapeData(ownerClasses(nodeShape));
108
+ return properties.flatMap((ps) => {
66
109
  // Always set: `singleOwnerProperties` keeps simple paths only. The guard narrows the type.
67
110
  const predicate = getSimplePathId(ps.path);
68
111
  if (!predicate)
@@ -78,8 +121,9 @@ export function buildSingleOwnerConstraints(nodeShape, shapeIri) {
78
121
  {
79
122
  __id: psIri,
80
123
  label: 'owners',
81
- maxCount: 1,
82
124
  path: serializePathToNodeData({ inv: { id: predicate } }),
125
+ qualifiedValueShape: owners,
126
+ qualifiedMaxCount: 1,
83
127
  },
84
128
  ],
85
129
  },
@@ -1 +1 @@
1
- {"version":3,"file":"singleOwnerConstraint.js","sourceRoot":"","sources":["../../../src/shapes/singleOwnerConstraint.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAC,iBAAiB,EAA6C,MAAM,oBAAoB,CAAC;AACjG,OAAO,EAAC,eAAe,EAAC,MAAM,mCAAmC,CAAC;AAClE,OAAO,EAAC,uBAAuB,EAAC,MAAM,8BAA8B,CAAC;AAErE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,4FAA4F;AAC5F,MAAM,mBAAmB,GAAG,cAAc,CAAC;AAE3C;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,gBAAwB;IAC1D,OAAO,GAAG,gBAAgB,GAAG,mBAAmB,EAAE,CAAC;AACrD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CAAC,SAAwB;IAC5D,OAAO,iBAAiB,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,MAAM,CAC/C,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,QAAQ,IAAI,CAAC,CAAC,EAAE,CAAC,UAAU,IAAI,CAAC,CAAC,eAAe,CAAC,EAAE,CAAC,IAAI,CAAC,CACrE,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,2BAA2B,CACzC,SAAwB,EACxB,QAAgB;IAEhB,OAAO,qBAAqB,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE;QACrD,2FAA2F;QAC3F,MAAM,SAAS,GAAG,eAAe,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QAC3C,IAAI,CAAC,SAAS;YAAE,OAAO,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,mBAAmB,CAAC,GAAG,QAAQ,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC;QAC3D,MAAM,KAAK,GAAG,GAAG,GAAG,SAAS,CAAC;QAC9B,OAAO,CAAC;gBACN,GAAG;gBACH,IAAI,EAAE;oBACJ,SAAS,EAAE,IAAI;oBACf,eAAe,EAAE,CAAC,EAAC,EAAE,EAAE,SAAS,EAAC,CAAC;oBAClC,UAAU,EAAE;wBACV;4BACE,IAAI,EAAE,KAAK;4BACX,KAAK,EAAE,QAAQ;4BACf,QAAQ,EAAE,CAAC;4BACX,IAAI,EAAE,uBAAuB,CAAC,EAAC,GAAG,EAAE,EAAC,EAAE,EAAE,SAAS,EAAC,EAAC,CAAC;yBACtD;qBACF;iBACF;aACF,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/*\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at https://mozilla.org/MPL/2.0/.\n */\nimport {getPropertyShapes, type NodeShapeData, type PropertyShapeData} from './nodeShapeData.js';\nimport {getSimplePathId} from '../paths/normalizePropertyPath.js';\nimport {serializePathToNodeData} from './serializePathToNodeData.js';\n\n/**\n * The single-owner SHACL constraints generated from `contains` properties.\n *\n * A `contains` property says the subject owns the value node. For every such property, shape\n * sync writes one generated NodeShape stating the matching store-side rule: every object of the\n * property's predicate has at most one subject pointing at it through that predicate.\n *\n * ```turtle\n * <…/Thing/image.singleOwner> a sh:NodeShape ;\n * core:generated true ;\n * sh:targetObjectsOf schema:image ;\n * sh:property <…/Thing/image.singleOwner/owners> .\n * <…/Thing/image.singleOwner/owners> a sh:PropertyShape ;\n * sh:maxCount 1 ;\n * sh:path [ sh:inversePath schema:image ] .\n * ```\n *\n * One constraint per accessor, written only by the class that declares it: subclasses inherit\n * the property but add no constraint, since the rule targets the predicate's objects whoever the\n * subject is. The declaring shape owns its constraints through `core:generatedShape`\n * (`NodeShape.generatedShapes`, `contains`): shape sync writes them nested in the shape's\n * create and deletes them with the shape.\n *\n * Non-owning references through other predicates are not counted, and neither is ownership\n * through two different `contains` predicates (a documented limitation). The shapes are built\n * only at sync time and are never registered in the in-memory shape registry.\n */\n\n/** Suffix that turns a property shape's IRI into its generated single-owner shape's IRI. */\nconst SINGLE_OWNER_SUFFIX = '.singleOwner';\n\n/**\n * The deterministic IRI of the generated single-owner NodeShape of one property shape:\n * `${propertyShapeIri}.singleOwner`, e.g. `…/shape/my-pkg/Thing/image.singleOwner`.\n */\nexport function singleOwnerShapeIri(propertyShapeIri: string): string {\n return `${propertyShapeIri}${SINGLE_OWNER_SUFFIX}`;\n}\n\n/**\n * The shape's OWN `contains` properties that get a single-owner constraint: those that declare\n * a value shape and have a simple path, in declaration order.\n *\n * Inherited properties are not included; their declaring shape writes their constraint.\n * Complex paths are skipped, as the containment cascade skips them. A `contains` property\n * without a value shape is a polymorphic slot (like `sh:path`) whose value may be a shared IRI,\n * so the replace, the subject delete and the class check do not treat it as owning that value,\n * and neither does the single-owner rule.\n */\nexport function singleOwnerProperties(nodeShape: NodeShapeData): PropertyShapeData[] {\n return getPropertyShapes(nodeShape, false).filter(\n (ps) => ps.contains && !!ps.valueShape && !!getSimplePathId(ps.path),\n );\n}\n\n/**\n * Create-pipeline data for the generated single-owner NodeShapes of `nodeShape`, one per\n * {@link singleOwnerProperties} entry. Each is stored at\n * {@link singleOwnerShapeIri}`(${shapeIri}/${label})`: the property shape's IRI exactly as shape\n * sync writes it (`buildPropertyShapeData`).\n */\nexport function buildSingleOwnerConstraints(\n nodeShape: NodeShapeData,\n shapeIri: string,\n): Array<{iri: string; data: Record<string, unknown>}> {\n return singleOwnerProperties(nodeShape).flatMap((ps) => {\n // Always set: `singleOwnerProperties` keeps simple paths only. The guard narrows the type.\n const predicate = getSimplePathId(ps.path);\n if (!predicate) return [];\n const iri = singleOwnerShapeIri(`${shapeIri}/${ps.label}`);\n const psIri = `${iri}/owners`;\n return [{\n iri,\n data: {\n generated: true,\n targetObjectsOf: [{id: predicate}],\n properties: [\n {\n __id: psIri,\n label: 'owners',\n maxCount: 1,\n path: serializePathToNodeData({inv: {id: predicate}}),\n },\n ],\n },\n }];\n });\n}\n"]}
1
+ {"version":3,"file":"singleOwnerConstraint.js","sourceRoot":"","sources":["../../../src/shapes/singleOwnerConstraint.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAC,iBAAiB,EAA6C,MAAM,oBAAoB,CAAC;AACjG,OAAO,EAAC,eAAe,EAAC,MAAM,mCAAmC,CAAC;AAClE,OAAO,EAAC,uBAAuB,EAAC,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAC,YAAY,EAAC,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAC,SAAS,EAAC,MAAM,YAAY,CAAC;AACrC,OAAO,EAAC,QAAQ,EAAC,MAAM,eAAe,CAAC;AACvC,OAAO,EAAC,GAAG,EAAC,MAAM,sBAAsB,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,4FAA4F;AAC5F,MAAM,mBAAmB,GAAG,cAAc,CAAC;AAE3C;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,gBAAwB;IAC1D,OAAO,GAAG,gBAAgB,GAAG,mBAAmB,EAAE,CAAC;AACrD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,SAAwB;IAC5D,OAAO,iBAAiB,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,MAAM,CAC/C,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,QAAQ,IAAI,CAAC,CAAC,EAAE,CAAC,UAAU,IAAI,CAAC,CAAC,eAAe,CAAC,EAAE,CAAC,IAAI,CAAC,CACrE,CAAC;AACJ,CAAC;AAED,qGAAqG;AACrG,SAAS,YAAY,CAAC,SAAwB;;IAC5C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,KAAK,MAAM,KAAK,IAAI,CAAC,SAAS,EAAE,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC;QAC5D,IAAI,MAAA,KAAK,CAAC,WAAW,0CAAE,EAAE;YAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;AAC7B,CAAC;AAED,qGAAqG;AACrG,SAAS,cAAc,CAAC,OAAiB;IACvC,MAAM,UAAU,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,CAAC,EAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,EAAC,EAAE,EAAC,EAAC,CAAC,CAAC;IACrE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACxD,MAAM,IAAI,GAAG,OAAO,CAAC,WAAW,CAC9B,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,CAAC,EAAE,CAAC,EAAE,IAAI,EAAC,CAAC,EAC9D,EAAC,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,EAAC,CACjB,CAAC;IACF,OAAO,EAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAC,CAAC;AAC5C,CAAC;AAED,MAAM,wBAAwB,GAAG,IAAI,GAAG,EAAU,CAAC;AAEnD;;;;;;GAMG;AACH,MAAM,UAAU,2BAA2B,CACzC,SAAwB,EACxB,QAAgB;;IAEhB,MAAM,UAAU,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC;IACpD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACvC,IAAI,CAAC,CAAA,MAAA,SAAS,CAAC,WAAW,0CAAE,EAAE,CAAA,EAAE,CAAC;QAC/B,IAAI,CAAC,wBAAwB,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC;YAChD,wBAAwB,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;YAC3C,OAAO,CAAC,IAAI,CACV,mBAAmB,SAAS,CAAC,EAAE,yDAAyD;gBACtF,8EAA8E,CACjF,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,MAAM,GAAG,cAAc,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC;IACvD,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE;QAC/B,2FAA2F;QAC3F,MAAM,SAAS,GAAG,eAAe,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QAC3C,IAAI,CAAC,SAAS;YAAE,OAAO,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,mBAAmB,CAAC,GAAG,QAAQ,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC;QAC3D,MAAM,KAAK,GAAG,GAAG,GAAG,SAAS,CAAC;QAC9B,OAAO,CAAC;gBACN,GAAG;gBACH,IAAI,EAAE;oBACJ,SAAS,EAAE,IAAI;oBACf,eAAe,EAAE,CAAC,EAAC,EAAE,EAAE,SAAS,EAAC,CAAC;oBAClC,UAAU,EAAE;wBACV;4BACE,IAAI,EAAE,KAAK;4BACX,KAAK,EAAE,QAAQ;4BACf,IAAI,EAAE,uBAAuB,CAAC,EAAC,GAAG,EAAE,EAAC,EAAE,EAAE,SAAS,EAAC,EAAC,CAAC;4BACrD,mBAAmB,EAAE,MAAM;4BAC3B,iBAAiB,EAAE,CAAC;yBACrB;qBACF;iBACF;aACF,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/*\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at https://mozilla.org/MPL/2.0/.\n */\nimport {getPropertyShapes, type NodeShapeData, type PropertyShapeData} from './nodeShapeData.js';\nimport {getSimplePathId} from '../paths/normalizePropertyPath.js';\nimport {serializePathToNodeData} from './serializePathToNodeData.js';\nimport {getSubShapes} from '../utils/ShapeClass.js';\nimport {NodeShape} from './SHACL.js';\nimport {PathList} from './PathList.js';\nimport {rdf} from '../ontologies/rdf.js';\n\n/**\n * The single-owner SHACL constraints generated from `contains` properties.\n *\n * A `contains` property says the subject owns the value node, and only that property does: a\n * plain property on the same predicate is a reference. For every such property, shape sync\n * writes one generated NodeShape stating the matching store-side rule: every object of the\n * property's predicate has at most one subject pointing at it through that predicate *that is\n * an instance of the declaring class or of a registered sub-shape's class*.\n *\n * ```turtle\n * <…/Thing/image.singleOwner> a sh:NodeShape ;\n * core:generated true ;\n * sh:targetObjectsOf schema:image ;\n * sh:property <…/Thing/image.singleOwner/owners> .\n * <…/Thing/image.singleOwner/owners> a sh:PropertyShape ;\n * sh:path [ sh:inversePath schema:image ] ;\n * sh:qualifiedValueShape [ sh:or ( [ sh:class schema:Thing ] [ sh:class ex:Project ] ) ] ;\n * sh:qualifiedMaxCount 1 .\n * ```\n *\n * The owner classes are enumerated rather than left to `sh:class` alone, because `sh:class`\n * matches a subclass instance only through `rdfs:subClassOf` triples, which a dataset without\n * inference does not have. With one class the qualified shape is `[ sh:class C ]`. Sub-shapes\n * registered only in another process are missing until a process that knows them syncs.\n *\n * One constraint per accessor, written only by the class that declares it: subclasses inherit\n * the property and are counted as owners, but add no constraint. A declaring shape without a\n * `targetClass` gets none (its owners cannot be named). The declaring shape owns its\n * constraints through `core:generatedShape` (`NodeShape.generatedShapes`, `contains`): shape sync\n * writes them nested in the shape's create and deletes them with the shape; their qualified\n * shape and `sh:or` list are blank nodes, deleted with the property shape that holds them.\n *\n * Ownership through two different `contains` predicates is not counted (a documented\n * limitation). The shapes are built only at sync time and are never registered in the\n * in-memory shape registry.\n */\n\n/** Suffix that turns a property shape's IRI into its generated single-owner shape's IRI. */\nconst SINGLE_OWNER_SUFFIX = '.singleOwner';\n\n/**\n * The deterministic IRI of the generated single-owner NodeShape of one property shape:\n * `${propertyShapeIri}.singleOwner`, e.g. `…/shape/my-pkg/Thing/image.singleOwner`.\n */\nexport function singleOwnerShapeIri(propertyShapeIri: string): string {\n return `${propertyShapeIri}${SINGLE_OWNER_SUFFIX}`;\n}\n\n/**\n * The shape's OWN `contains` properties that get a single-owner constraint: those with a simple\n * path (every decorated `contains` property has a value shape), in declaration order.\n *\n * Inherited properties are not included; their declaring shape writes their constraint.\n * Complex paths are skipped, as the owned walk skips them.\n */\nexport function singleOwnerProperties(nodeShape: NodeShapeData): PropertyShapeData[] {\n return getPropertyShapes(nodeShape, false).filter(\n (ps) => ps.contains && !!ps.valueShape && !!getSimplePathId(ps.path),\n );\n}\n\n/** The classes whose instances own through the shape's properties: its own and every sub-shape's. */\nfunction ownerClasses(nodeShape: NodeShapeData): string[] {\n const classes = new Set<string>();\n for (const shape of [nodeShape, ...getSubShapes(nodeShape)]) {\n if (shape.targetClass?.id) classes.add(shape.targetClass.id);\n }\n return [...classes].sort();\n}\n\n/** `[ sh:class C ]`, or `[ sh:or ( [ sh:class A ] … ) ]` for several classes. Blank once written. */\nfunction ownerShapeData(classes: string[]): Record<string, unknown> {\n const classShape = (id: string) => ({shape: NodeShape, class: {id}});\n if (classes.length === 1) return classShape(classes[0]);\n const list = classes.reduceRight<unknown>(\n (rest, id) => ({shape: PathList, first: classShape(id), rest}),\n {id: rdf.nil.id},\n );\n return {shape: NodeShape, orShapes: list};\n}\n\nconst warnedWithoutTargetClass = new Set<string>();\n\n/**\n * Create-pipeline data for the generated single-owner NodeShapes of `nodeShape`, one per\n * {@link singleOwnerProperties} entry. Each is stored at\n * {@link singleOwnerShapeIri}`(${shapeIri}/${label})`: the property shape's IRI exactly as shape\n * sync writes it (`buildPropertyShapeData`). Empty, with one warning per shape, when the shape\n * has `contains` properties but no `targetClass`.\n */\nexport function buildSingleOwnerConstraints(\n nodeShape: NodeShapeData,\n shapeIri: string,\n): Array<{iri: string; data: Record<string, unknown>}> {\n const properties = singleOwnerProperties(nodeShape);\n if (properties.length === 0) return [];\n if (!nodeShape.targetClass?.id) {\n if (!warnedWithoutTargetClass.has(nodeShape.id)) {\n warnedWithoutTargetClass.add(nodeShape.id);\n console.warn(\n `[linked] Shape '${nodeShape.id}' has contains properties but no targetClass, so shape ` +\n 'sync writes no single-owner constraint for them: its owners cannot be named.',\n );\n }\n return [];\n }\n const owners = ownerShapeData(ownerClasses(nodeShape));\n return properties.flatMap((ps) => {\n // Always set: `singleOwnerProperties` keeps simple paths only. The guard narrows the type.\n const predicate = getSimplePathId(ps.path);\n if (!predicate) return [];\n const iri = singleOwnerShapeIri(`${shapeIri}/${ps.label}`);\n const psIri = `${iri}/owners`;\n return [{\n iri,\n data: {\n generated: true,\n targetObjectsOf: [{id: predicate}],\n properties: [\n {\n __id: psIri,\n label: 'owners',\n path: serializePathToNodeData({inv: {id: predicate}}),\n qualifiedValueShape: owners,\n qualifiedMaxCount: 1,\n },\n ],\n },\n }];\n });\n}\n"]}
@@ -7,6 +7,33 @@ import type { IDataset } from '../interfaces/IDataset.js';
7
7
  * @internal Exported for tests only — not part of the public API.
8
8
  */
9
9
  export declare function buildPropertyShapeData(ps: PropertyShapeData, shapeIri: string): Record<string, unknown>;
10
+ /**
11
+ * Version of the {@link shapeSyncHash} input. Bump it whenever core changes the SHACL it writes
12
+ * for a shape (a serialization change that the create data itself does not show), so every stored
13
+ * shape is rewritten once by the next sync. Without a bump, stores keep the old triples: their
14
+ * stored hashes still match, so the sync skips them.
15
+ *
16
+ * Enforced by `shape-sync-output-equivalence.test.ts`: its golden SHACL output is recorded in
17
+ * `SHAPE_HASH_LEDGER` with the version it was generated for, and regenerating the golden without
18
+ * bumping this constant fails that test. To change the output: bump this by one, update the
19
+ * golden, and append `{version, goldenDigest}` to the ledger (never edit an existing entry).
20
+ *
21
+ * @internal Exported for tests only — not part of the public API.
22
+ */
23
+ export declare const SHAPE_HASH_VERSION = 1;
24
+ /**
25
+ * The content hash shape sync stores on a NodeShape as `core:shapeHash`: SHA-256 (hex) of the
26
+ * canonical form of the shape IRI and the exact create data the sync writes for it — the
27
+ * NodeShape, its flattened own + inherited property shapes, their `sh:in` lists and paths, and
28
+ * the generated single-owner shapes it owns (whose owner classes come from the registered
29
+ * sub-shapes, so registering a new sub-shape changes the hash of the declaring shape).
30
+ *
31
+ * Deterministic across processes, runtimes and Node versions: no blank-node labels, no object key
32
+ * order, no array order (see {@link canonicalize}).
33
+ *
34
+ * @internal Exported for tests only — not part of the public API.
35
+ */
36
+ export declare function shapeSyncHash(data: Record<string, unknown>, shapeIri: string): string;
10
37
  /** Options controlling how {@link syncShapes} treats store shapes that are not in code. */
11
38
  export interface SyncShapesOptions {
12
39
  /**
@@ -27,6 +54,18 @@ export interface SyncShapesOptions {
27
54
  * `'all'` and `'ownedNamespaces'` are destructive and must be requested explicitly.
28
55
  */
29
56
  orphanScope?: 'all' | 'ownedNamespaces' | 'none';
57
+ /**
58
+ * Rewrite every code shape, even one whose stored `core:shapeHash` matches. Default `false`:
59
+ * a shape is rewritten only when its hash differs or is missing. Use it to repair a shape
60
+ * whose stored subtree was edited or partly deleted outside shape sync (the hash describes
61
+ * what sync last wrote, not what is in the store now).
62
+ */
63
+ force?: boolean;
64
+ }
65
+ /** Options for {@link syncShape}. */
66
+ export interface SyncShapeOptions {
67
+ /** Rewrite the shape even when its stored `core:shapeHash` matches. See {@link SyncShapesOptions.force}. */
68
+ force?: boolean;
30
69
  }
31
70
  /**
32
71
  * Plan an idempotent sync of all code-registered (non-framework) NodeShapes into the store as
@@ -39,6 +78,15 @@ export interface SyncShapesOptions {
39
78
  * Each in-code shape's thunk runs `delete → create` in order (the delete cascade-cleans the old
40
79
  * property shapes / list / path subtrees via the containment cascade, the create rebuilds them).
41
80
  *
81
+ * **Unchanged shapes are skipped.** The create stores a content hash of everything it writes for
82
+ * the shape on the NodeShape (`core:shapeHash`, see {@link shapeSyncHash}). The plan reads the
83
+ * stored hashes of all code shapes in one select and builds a thunk only for a shape whose hash
84
+ * is missing (never synced, or synced before hashes existed) or differs. Syncing an unchanged
85
+ * app therefore returns no write thunks, and the length of the plan is the number of shapes
86
+ * actually written plus the pruned orphans. The hash records what sync last wrote, not what the
87
+ * store holds now: a subtree edited or partly deleted by another writer while the NodeShape and
88
+ * its hash survive is not detected. `options.force` rewrites every code shape.
89
+ *
42
90
  * **Shapes in the store but not registered in this process are left alone by default**
43
91
  * (`orphanScope: 'none'`). The registry only holds what this process happened to import, and
44
92
  * that is routinely a subset of what the dataset holds: shapes are deep-importable one file at a
@@ -55,8 +103,8 @@ export interface SyncShapesOptions {
55
103
  * with `'none'` no read is made.
56
104
  *
57
105
  * `ds` (optional) targets an explicit dataset instead of the global router. It is a **plan-time**
58
- * parameter: it feeds both the orphan-detection read (so orphans are computed against the same
59
- * store they'll be pruned from) and every delete/create thunk. Omitted → the global router;
106
+ * parameter: it feeds the stored-hash read and the orphan-detection read (so both are computed
107
+ * against the same store they'll be written to) and every delete/create thunk. Omitted → the global router;
60
108
  * the returned thunks stay nullary either way.
61
109
  */
62
110
  export declare function syncShapes(ds?: IDataset, options?: SyncShapesOptions): Promise<Array<() => Promise<void>>>;
@@ -71,9 +119,12 @@ export declare function syncShapes(ds?: IDataset, options?: SyncShapesOptions):
71
119
  * its registered class via `getShapeClass`; the shape must be code-registered.
72
120
  * @param ds optional explicit dataset to materialize into (a store or a router) — both the delete
73
121
  * and the create run against it instead of the global router. Omitted → global behavior.
122
+ * @param options `force: true` rewrites the shape even when its stored hash matches.
74
123
  * @returns a single unexecuted thunk (consistent with {@link syncShapes}), so callers can batch
75
124
  * several together — e.g. a shape plus its referenced object-property shapes:
76
125
  * `await Promise.all([syncShape(Person), syncShape(Address)].map((run) => run()))`.
126
+ * Unless `force` is set, the thunk first reads the shape's stored `core:shapeHash` and writes
127
+ * nothing when it equals the hash of what it would write (see {@link syncShapes}).
77
128
  * @throws if `target` is a framework/meta shape, is not registered, or has no `.shape`.
78
129
  */
79
- export declare function syncShape(target: typeof Shape | string, ds?: IDataset): () => Promise<void>;
130
+ export declare function syncShape(target: typeof Shape | string, ds?: IDataset, options?: SyncShapeOptions): () => Promise<void>;
@@ -19,6 +19,8 @@ import { DeleteBuilder } from '../queries/DeleteBuilder.js';
19
19
  import { rdfList } from './List.js';
20
20
  import { serializePathToNodeData } from './serializePathToNodeData.js';
21
21
  import { buildSingleOwnerConstraints } from './singleOwnerConstraint.js';
22
+ import { QueryBuilder } from '../queries/QueryBuilder.js';
23
+ import { sha256Hex } from '../utils/sha256.js';
22
24
  /**
23
25
  * The npm scope of framework shapes (NodeShape/PropertyShape/List/PathNode/Shape) that
24
26
  * describe the meta-model itself and must never be synced as user data.
@@ -129,16 +131,112 @@ function buildNodeShapeData(nodeShape, shapeIri) {
129
131
  }
130
132
  return d;
131
133
  }
134
+ /**
135
+ * Version of the {@link shapeSyncHash} input. Bump it whenever core changes the SHACL it writes
136
+ * for a shape (a serialization change that the create data itself does not show), so every stored
137
+ * shape is rewritten once by the next sync. Without a bump, stores keep the old triples: their
138
+ * stored hashes still match, so the sync skips them.
139
+ *
140
+ * Enforced by `shape-sync-output-equivalence.test.ts`: its golden SHACL output is recorded in
141
+ * `SHAPE_HASH_LEDGER` with the version it was generated for, and regenerating the golden without
142
+ * bumping this constant fails that test. To change the output: bump this by one, update the
143
+ * golden, and append `{version, goldenDigest}` to the ledger (never edit an existing entry).
144
+ *
145
+ * @internal Exported for tests only — not part of the public API.
146
+ */
147
+ export const SHAPE_HASH_VERSION = 1;
148
+ /**
149
+ * A canonical string for create-pipeline data: equal for equal data, independent of object key
150
+ * order and of the order of array values.
151
+ *
152
+ * Arrays are sorted because in create data an array is always a multi-valued property (a set of
153
+ * RDF values: `properties`, `generatedShapes`, `targetObjectsOf`, …). Ordered `rdf:List`s
154
+ * (`sh:in`, sequence and alternative paths, `sh:or`) are nested `first`/`rest` objects, so their
155
+ * order is kept by the structure. A shape class value (`shape: List`) is written as its NodeShape
156
+ * IRI. Values without ids (blank nodes once written) are compared by structure, so no blank-node
157
+ * label is ever part of the input.
158
+ */
159
+ function canonicalize(value) {
160
+ var _a, _b;
161
+ if (value === null || value === undefined)
162
+ return 'null';
163
+ switch (typeof value) {
164
+ case 'string':
165
+ return JSON.stringify(value);
166
+ case 'number':
167
+ return `n:${Object.is(value, -0) ? '0' : String(value)}`;
168
+ case 'boolean':
169
+ return `b:${value}`;
170
+ case 'bigint':
171
+ return `i:${value.toString()}`;
172
+ case 'function': {
173
+ const shapeId = (_a = value.shape) === null || _a === void 0 ? void 0 : _a.id;
174
+ return `c:${JSON.stringify((_b = shapeId !== null && shapeId !== void 0 ? shapeId : value.name) !== null && _b !== void 0 ? _b : '')}`;
175
+ }
176
+ }
177
+ if (Array.isArray(value)) {
178
+ return `[${value.map(canonicalize).sort().join(',')}]`;
179
+ }
180
+ if (value instanceof Date)
181
+ return `d:${value.toISOString()}`;
182
+ if (value instanceof RegExp)
183
+ return `r:${JSON.stringify(value.source)}/${value.flags}`;
184
+ const record = value;
185
+ const keys = Object.keys(record)
186
+ .filter((k) => record[k] !== undefined)
187
+ .sort();
188
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(record[k])}`).join(',')}}`;
189
+ }
190
+ /**
191
+ * The content hash shape sync stores on a NodeShape as `core:shapeHash`: SHA-256 (hex) of the
192
+ * canonical form of the shape IRI and the exact create data the sync writes for it — the
193
+ * NodeShape, its flattened own + inherited property shapes, their `sh:in` lists and paths, and
194
+ * the generated single-owner shapes it owns (whose owner classes come from the registered
195
+ * sub-shapes, so registering a new sub-shape changes the hash of the declaring shape).
196
+ *
197
+ * Deterministic across processes, runtimes and Node versions: no blank-node labels, no object key
198
+ * order, no array order (see {@link canonicalize}).
199
+ *
200
+ * @internal Exported for tests only — not part of the public API.
201
+ */
202
+ export function shapeSyncHash(data, shapeIri) {
203
+ return sha256Hex(`linked-shape-sync/${SHAPE_HASH_VERSION}\n${canonicalize({ iri: shapeIri, data })}`);
204
+ }
205
+ /** The create data and its content hash for one shape, as the sync writes it. */
206
+ function buildSyncData(nodeShape, iri) {
207
+ const data = buildNodeShapeData(nodeShape, iri);
208
+ return { data, hash: shapeSyncHash(data, iri) };
209
+ }
210
+ /**
211
+ * The stored `core:shapeHash` of each of `iris` that exists as a NodeShape, in one select. A shape
212
+ * that is absent, or stored without a hash (written before hashes existed), is missing from the
213
+ * map. `ds` targets an explicit dataset, as for the writes.
214
+ */
215
+ function readStoredHashes(iris, ds) {
216
+ return __awaiter(this, void 0, void 0, function* () {
217
+ var _a;
218
+ const stored = new Map();
219
+ if (iris.length === 0)
220
+ return stored;
221
+ const rows = (_a = (yield QueryBuilder.from(NodeShape).select(['shapeHash']).forAll(iris).exec(ds))) !== null && _a !== void 0 ? _a : [];
222
+ for (const row of rows) {
223
+ if (row && typeof row.shapeHash === 'string')
224
+ stored.set(row.id, row.shapeHash);
225
+ }
226
+ return stored;
227
+ });
228
+ }
132
229
  /**
133
230
  * The per-shape `delete → create` thunk shared by `syncShapes()` and `syncShape()`: exactly two
134
231
  * requests. The delete cascade-cleans the shape's old property-shape / list / path subtrees; the
135
- * create rebuilds them.
232
+ * create rebuilds them, and stores the content hash (`core:shapeHash`) of what it wrote on the
233
+ * NodeShape so a later sync can skip the shape while it is unchanged.
136
234
  *
137
235
  * The single-owner constraints generated from the shape's own `contains` properties
138
236
  * (`${iri}/${label}.singleOwner`, see `singleOwnerConstraint.ts`) are owned by the shape through
139
237
  * `core:generatedShape` (a `contains` property with value shape NodeShape). The delete removes
140
- * each one's own triples (the immediate owned-child block) and their property shapes and path
141
- * nodes (the dependent cascade); the create writes the current ones nested, in the same request
238
+ * them and their property shapes through the owned walk, and their blank path and qualified-shape
239
+ * nodes through the blank-node cascade; the create writes the current ones nested, in the same request
142
240
  * as the shape. A removed or renamed accessor therefore loses its constraint on the next sync,
143
241
  * and a NodeShape the shape does not own is never touched.
144
242
  *
@@ -146,12 +244,16 @@ function buildNodeShapeData(nodeShape, shapeIri) {
146
244
  * global router. Omitted → today's global behavior.
147
245
  */
148
246
  function buildSyncThunk(nodeShape, iri, ds) {
149
- return () => __awaiter(this, void 0, void 0, function* () {
150
- // Build the data fresh on each invocation, so every run writes from the current
151
- // shape metadata.
152
- const data = buildNodeShapeData(nodeShape, iri);
247
+ // `writeShape` builds the data fresh on each invocation, so every run writes from the current
248
+ // shape metadata, and the hash stored with it always describes what was written.
249
+ return () => writeShape(nodeShape, iri, ds);
250
+ }
251
+ /** Delete (cascade) then create one shape, storing the hash of the written data. */
252
+ function writeShape(nodeShape, iri, ds) {
253
+ return __awaiter(this, void 0, void 0, function* () {
254
+ const { data, hash } = buildSyncData(nodeShape, iri);
153
255
  yield DeleteBuilder.from(NodeShape, { id: iri }).exec(ds);
154
- yield NodeShape.create(data).withId(iri).exec(ds);
256
+ yield NodeShape.create(Object.assign(Object.assign({}, data), { shapeHash: hash })).withId(iri).exec(ds);
155
257
  });
156
258
  }
157
259
  /** The generated single-owner shape IRIs a code shape writes. */
@@ -182,6 +284,15 @@ function shapeNamespace(iri) {
182
284
  * Each in-code shape's thunk runs `delete → create` in order (the delete cascade-cleans the old
183
285
  * property shapes / list / path subtrees via the containment cascade, the create rebuilds them).
184
286
  *
287
+ * **Unchanged shapes are skipped.** The create stores a content hash of everything it writes for
288
+ * the shape on the NodeShape (`core:shapeHash`, see {@link shapeSyncHash}). The plan reads the
289
+ * stored hashes of all code shapes in one select and builds a thunk only for a shape whose hash
290
+ * is missing (never synced, or synced before hashes existed) or differs. Syncing an unchanged
291
+ * app therefore returns no write thunks, and the length of the plan is the number of shapes
292
+ * actually written plus the pruned orphans. The hash records what sync last wrote, not what the
293
+ * store holds now: a subtree edited or partly deleted by another writer while the NodeShape and
294
+ * its hash survive is not detected. `options.force` rewrites every code shape.
295
+ *
185
296
  * **Shapes in the store but not registered in this process are left alone by default**
186
297
  * (`orphanScope: 'none'`). The registry only holds what this process happened to import, and
187
298
  * that is routinely a subset of what the dataset holds: shapes are deep-importable one file at a
@@ -198,8 +309,8 @@ function shapeNamespace(iri) {
198
309
  * with `'none'` no read is made.
199
310
  *
200
311
  * `ds` (optional) targets an explicit dataset instead of the global router. It is a **plan-time**
201
- * parameter: it feeds both the orphan-detection read (so orphans are computed against the same
202
- * store they'll be pruned from) and every delete/create thunk. Omitted → the global router;
312
+ * parameter: it feeds the stored-hash read and the orphan-detection read (so both are computed
313
+ * against the same store they'll be written to) and every delete/create thunk. Omitted → the global router;
203
314
  * the returned thunks stay nullary either way.
204
315
  */
205
316
  export function syncShapes(ds, options) {
@@ -233,8 +344,15 @@ export function syncShapes(ds, options) {
233
344
  // Namespaces this process actually writes — the ownership scope for `'ownedNamespaces'` pruning.
234
345
  const ownedNamespaces = new Set(userShapes.map((s) => shapeNamespace(s.iri)));
235
346
  const thunks = [];
236
- // 2. Per in-code shape: delete (cascade) then recreate.
347
+ // 2. Per in-code shape whose stored content hash is missing or differs: delete (cascade) then
348
+ // recreate. An unchanged shape gets no thunk, so syncing an unchanged app writes nothing.
349
+ // The stored hashes are read in one select, against the same `ds` the thunks target.
350
+ const storedHashes = (options === null || options === void 0 ? void 0 : options.force)
351
+ ? new Map()
352
+ : yield readStoredHashes(userShapes.map((s) => s.iri), ds);
237
353
  for (const { iri, nodeShape } of userShapes) {
354
+ if (!(options === null || options === void 0 ? void 0 : options.force) && storedHashes.get(iri) === buildSyncData(nodeShape, iri).hash)
355
+ continue;
238
356
  thunks.push(buildSyncThunk(nodeShape, iri, ds));
239
357
  }
240
358
  // 3. Orphan sweep (unless disabled). Identity-read existing NodeShape IRIs (ids only). Must hit
@@ -267,12 +385,15 @@ export function syncShapes(ds, options) {
267
385
  * its registered class via `getShapeClass`; the shape must be code-registered.
268
386
  * @param ds optional explicit dataset to materialize into (a store or a router) — both the delete
269
387
  * and the create run against it instead of the global router. Omitted → global behavior.
388
+ * @param options `force: true` rewrites the shape even when its stored hash matches.
270
389
  * @returns a single unexecuted thunk (consistent with {@link syncShapes}), so callers can batch
271
390
  * several together — e.g. a shape plus its referenced object-property shapes:
272
391
  * `await Promise.all([syncShape(Person), syncShape(Address)].map((run) => run()))`.
392
+ * Unless `force` is set, the thunk first reads the shape's stored `core:shapeHash` and writes
393
+ * nothing when it equals the hash of what it would write (see {@link syncShapes}).
273
394
  * @throws if `target` is a framework/meta shape, is not registered, or has no `.shape`.
274
395
  */
275
- export function syncShape(target, ds) {
396
+ export function syncShape(target, ds, options) {
276
397
  // Normalize target → its registered shape class.
277
398
  const cls = typeof target === 'string' ? getShapeClass(target) : target;
278
399
  if (typeof target === 'string' && !cls) {
@@ -292,6 +413,13 @@ export function syncShape(target, ds) {
292
413
  throw new Error(`syncShape: refusing to sync framework/meta shape ${iri}`);
293
414
  }
294
415
  assertNoGeneratedIriCollision(nodeShape, iri, (id) => !!getShapeClass(id));
295
- return buildSyncThunk(nodeShape, iri, ds);
416
+ if (options === null || options === void 0 ? void 0 : options.force)
417
+ return buildSyncThunk(nodeShape, iri, ds);
418
+ return () => __awaiter(this, void 0, void 0, function* () {
419
+ const stored = yield readStoredHashes([iri], ds);
420
+ if (stored.get(iri) === buildSyncData(nodeShape, iri).hash)
421
+ return;
422
+ yield writeShape(nodeShape, iri, ds);
423
+ });
296
424
  }
297
425
  //# sourceMappingURL=syncShapes.js.map