@_linked/core 2.28.4 → 2.30.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 (59) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +131 -1
  3. package/lib/esm/index.d.ts +1 -0
  4. package/lib/esm/index.js +1 -0
  5. package/lib/esm/index.js.map +1 -1
  6. package/lib/esm/ontologies/linked-core.d.ts +2 -0
  7. package/lib/esm/ontologies/linked-core.js +10 -0
  8. package/lib/esm/ontologies/linked-core.js.map +1 -1
  9. package/lib/esm/ontologies/shacl.d.ts +1 -0
  10. package/lib/esm/ontologies/shacl.js +2 -0
  11. package/lib/esm/ontologies/shacl.js.map +1 -1
  12. package/lib/esm/queries/IRMutation.js +1 -1
  13. package/lib/esm/queries/IRMutation.js.map +1 -1
  14. package/lib/esm/queries/IntermediateRepresentation.d.ts +5 -0
  15. package/lib/esm/queries/IntermediateRepresentation.js.map +1 -1
  16. package/lib/esm/queries/MutationQuery.js +15 -0
  17. package/lib/esm/queries/MutationQuery.js.map +1 -1
  18. package/lib/esm/queries/MutationSerialization.d.ts +5 -4
  19. package/lib/esm/queries/MutationSerialization.js +9 -1
  20. package/lib/esm/queries/MutationSerialization.js.map +1 -1
  21. package/lib/esm/queries/QueryFactory.d.ts +6 -0
  22. package/lib/esm/queries/QueryFactory.js.map +1 -1
  23. package/lib/esm/queries/SelectQuery.js +3 -1
  24. package/lib/esm/queries/SelectQuery.js.map +1 -1
  25. package/lib/esm/queries/lowerMutationJSON.js +6 -1
  26. package/lib/esm/queries/lowerMutationJSON.js.map +1 -1
  27. package/lib/esm/shapes/List.d.ts +1 -0
  28. package/lib/esm/shapes/List.js.map +1 -1
  29. package/lib/esm/shapes/PathNode.d.ts +5 -2
  30. package/lib/esm/shapes/PathNode.js +5 -2
  31. package/lib/esm/shapes/PathNode.js.map +1 -1
  32. package/lib/esm/shapes/SHACL.d.ts +15 -2
  33. package/lib/esm/shapes/SHACL.js.map +1 -1
  34. package/lib/esm/shapes/serializePathToNodeData.d.ts +17 -5
  35. package/lib/esm/shapes/serializePathToNodeData.js +41 -27
  36. package/lib/esm/shapes/serializePathToNodeData.js.map +1 -1
  37. package/lib/esm/shapes/singleOwnerConstraint.d.ts +27 -0
  38. package/lib/esm/shapes/singleOwnerConstraint.js +89 -0
  39. package/lib/esm/shapes/singleOwnerConstraint.js.map +1 -0
  40. package/lib/esm/shapes/syncShapes.js +49 -10
  41. package/lib/esm/shapes/syncShapes.js.map +1 -1
  42. package/lib/esm/shapes/validation.js +2 -2
  43. package/lib/esm/shapes/validation.js.map +1 -1
  44. package/lib/esm/sparql/SparqlAlgebra.d.ts +5 -0
  45. package/lib/esm/sparql/SparqlAlgebra.js.map +1 -1
  46. package/lib/esm/sparql/SparqlDataset.d.ts +9 -1
  47. package/lib/esm/sparql/SparqlDataset.js +19 -0
  48. package/lib/esm/sparql/SparqlDataset.js.map +1 -1
  49. package/lib/esm/sparql/algebraToString.js +2 -0
  50. package/lib/esm/sparql/algebraToString.js.map +1 -1
  51. package/lib/esm/sparql/irToAlgebra.d.ts +34 -6
  52. package/lib/esm/sparql/irToAlgebra.js +384 -59
  53. package/lib/esm/sparql/irToAlgebra.js.map +1 -1
  54. package/lib/esm/sparql/ownedLinkCheck.d.ts +50 -0
  55. package/lib/esm/sparql/ownedLinkCheck.js +142 -0
  56. package/lib/esm/sparql/ownedLinkCheck.js.map +1 -0
  57. package/lib/esm/utils/Package.js +8 -0
  58. package/lib/esm/utils/Package.js.map +1 -1
  59. package/package.json +2 -2
@@ -6,7 +6,7 @@ import { rdf } from '../ontologies/rdf.js';
6
6
  import { shacl } from '../ontologies/shacl.js';
7
7
  import { xsd } from '../ontologies/xsd.js';
8
8
  import { getSimplePathId } from '../paths/normalizePropertyPath.js';
9
- import { getAllNodeShapes, getNodeShape, getRegistryVersion, getShapeClass, getTargetClassId, } from '../utils/ShapeClass.js';
9
+ import { getAllNodeShapes, getNodeShape, getRegistryVersion, getShapeClass, getSubShapes, getTargetClassId, } from '../utils/ShapeClass.js';
10
10
  import { UnresolvedContextError } from '../queries/QueryContext.js';
11
11
  // ---------------------------------------------------------------------------
12
12
  // Constants
@@ -1421,18 +1421,65 @@ function fieldValueToTerms(value, options, datatype) {
1421
1421
  }
1422
1422
  return [];
1423
1423
  }
1424
+ /**
1425
+ * The RDF-collection and SHACL-path predicates the cascade's blank-node block walks inside a
1426
+ * blank structure: the syntax core writes as blank nodes (`serializePathToNodeData`).
1427
+ */
1428
+ const BLANK_SYNTAX_PREDICATES = [
1429
+ rdf.first.id,
1430
+ rdf.rest.id,
1431
+ shacl.inversePath.id,
1432
+ shacl.alternativePath.id,
1433
+ shacl.zeroOrMorePath.id,
1434
+ shacl.oneOrMorePath.id,
1435
+ shacl.zeroOrOnePath.id,
1436
+ ];
1437
+ /**
1438
+ * A blank node is removed only by the cascade's blank-node block, which reaches it through a
1439
+ * `contains` edge, or, inside a blank structure, through a list / path syntax predicate
1440
+ * (`rdf:first` of a blank list cell). A blank node held any other way could never be deleted:
1441
+ * refuse to write one.
1442
+ */
1443
+ function assertBlankIsOwned(data, propertyId, propertyTerm, parentIsBlank) {
1444
+ var _a;
1445
+ if (!data.blank || ((_a = findPropertyShapeById(propertyId)) === null || _a === void 0 ? void 0 : _a.contains))
1446
+ return;
1447
+ const syntaxEdge = parentIsBlank && propertyTerm.kind === 'iri' && BLANK_SYNTAX_PREDICATES.includes(propertyTerm.value);
1448
+ if (syntaxEdge)
1449
+ return;
1450
+ throw new Error(`A blank nested node (__blank) must be the value of a \`contains\` property, or of a list / ` +
1451
+ `path syntax predicate inside a blank node; ${propertyId} is neither, so nothing would ` +
1452
+ `ever delete it.`);
1453
+ }
1424
1454
  /**
1425
1455
  * Recursively generate triples for an IRNodeData (used in create and nested creates).
1426
- * Returns the URI used for this node and all generated triples.
1456
+ * Returns the term used for this node and all generated triples.
1457
+ *
1458
+ * A node marked `blank` (nested only, see `NodeDescriptionValue.__blank`) becomes a fresh
1459
+ * blank node with its field triples and NO `rdf:type`: it is RDF syntax, like a SHACL path
1460
+ * operator, which the spec defines as a blank node that is the subject of exactly one triple.
1461
+ * Without a `blanks` minter (the update paths) a blank node throws.
1427
1462
  */
1428
- function generateNodeDataTriples(data, options) {
1429
- const uri = data.id || generateEntityUri(data.shape, options);
1463
+ function generateNodeDataTriples(data, options, blanks) {
1430
1464
  const triples = [];
1431
- const subjectTerm = iriTerm(uri);
1432
- // Type triple — resolve the SHACL NodeShapeData id in the IR to the ontology
1433
- // targetClass URI (sibling of PR #77's SELECT-side resolveShapeScanIri).
1434
- // Mutations were not covered by #77; see mutation-uri-fidelity.test.ts.
1435
- triples.push(tripleOf(subjectTerm, iriTerm(RDF_TYPE), iriTerm(resolveShapeScanIri(data.shape))));
1465
+ let subjectTerm;
1466
+ if (data.blank) {
1467
+ if (data.id) {
1468
+ throw new Error(`A blank nested node (__blank) cannot carry an id (${data.id}).`);
1469
+ }
1470
+ if (!blanks) {
1471
+ throw new Error('A blank nested node (__blank) can only be written by a create. An update writes its ' +
1472
+ 'INSERT once per WHERE solution, which would duplicate the blank node.');
1473
+ }
1474
+ subjectTerm = { kind: 'blank', label: `b${blanks.next++}` };
1475
+ }
1476
+ else {
1477
+ subjectTerm = iriTerm(data.id || generateEntityUri(data.shape, options));
1478
+ // Type triple — resolve the SHACL NodeShapeData id in the IR to the ontology
1479
+ // targetClass URI (sibling of PR #77's SELECT-side resolveShapeScanIri).
1480
+ // Mutations were not covered by #77; see mutation-uri-fidelity.test.ts.
1481
+ triples.push(tripleOf(subjectTerm, iriTerm(RDF_TYPE), iriTerm(resolveShapeScanIri(data.shape))));
1482
+ }
1436
1483
  // Field triples
1437
1484
  for (const field of data.fields) {
1438
1485
  // Resolve the SHACL PropertyShapeData id to its declared `path` URI.
@@ -1445,8 +1492,9 @@ function generateNodeDataTriples(data, options) {
1445
1492
  for (const item of field.value) {
1446
1493
  if (item && typeof item === 'object' && 'shape' in item && 'fields' in item) {
1447
1494
  // Nested create
1448
- const nested = generateNodeDataTriples(item, options);
1449
- triples.push(tripleOf(subjectTerm, propertyTerm, iriTerm(nested.uri)));
1495
+ assertBlankIsOwned(item, field.property, propertyTerm, !!data.blank);
1496
+ const nested = generateNodeDataTriples(item, options, blanks);
1497
+ triples.push(tripleOf(subjectTerm, propertyTerm, nested.term));
1450
1498
  triples.push(...nested.triples);
1451
1499
  }
1452
1500
  else {
@@ -1460,8 +1508,9 @@ function generateNodeDataTriples(data, options) {
1460
1508
  }
1461
1509
  // Handle nested IRNodeData
1462
1510
  if (typeof field.value === 'object' && 'shape' in field.value && 'fields' in field.value) {
1463
- const nested = generateNodeDataTriples(field.value, options);
1464
- triples.push(tripleOf(subjectTerm, propertyTerm, iriTerm(nested.uri)));
1511
+ assertBlankIsOwned(field.value, field.property, propertyTerm, !!data.blank);
1512
+ const nested = generateNodeDataTriples(field.value, options, blanks);
1513
+ triples.push(tripleOf(subjectTerm, propertyTerm, nested.term));
1465
1514
  triples.push(...nested.triples);
1466
1515
  continue;
1467
1516
  }
@@ -1471,13 +1520,16 @@ function generateNodeDataTriples(data, options) {
1471
1520
  triples.push(tripleOf(subjectTerm, propertyTerm, term));
1472
1521
  }
1473
1522
  }
1474
- return { uri, triples };
1523
+ return { term: subjectTerm, triples };
1475
1524
  }
1476
1525
  /**
1477
1526
  * Converts an IRCreateMutation to a SparqlInsertDataPlan.
1478
1527
  */
1479
1528
  export function createToAlgebra(query, options) {
1480
- const { triples } = generateNodeDataTriples(query.data, options);
1529
+ if (query.data.blank) {
1530
+ throw new Error('The root node of a create cannot be blank (__blank): it must be addressable.');
1531
+ }
1532
+ const { triples } = generateNodeDataTriples(query.data, options, { next: 0 });
1481
1533
  return {
1482
1534
  type: 'insert_data',
1483
1535
  triples,
@@ -1486,11 +1538,31 @@ export function createToAlgebra(query, options) {
1486
1538
  // ---------------------------------------------------------------------------
1487
1539
  // Shared update field processing
1488
1540
  // ---------------------------------------------------------------------------
1541
+ /** A bare `{id}` reference: a link to an existing node, not a nested create. */
1542
+ export function isBareNodeReference(value) {
1543
+ return (!!value &&
1544
+ typeof value === 'object' &&
1545
+ !Array.isArray(value) &&
1546
+ !(value instanceof Date) &&
1547
+ typeof value.id === 'string' &&
1548
+ !('shape' in value) &&
1549
+ !('fields' in value));
1550
+ }
1551
+ /**
1552
+ * The values a `contains` replace keeps: the bare `{id}`s of the new single value or of
1553
+ * the overwrite array. A nested create that carries an id is not kept — it rewrites that
1554
+ * node completely, as it always has.
1555
+ */
1556
+ function keptReferences(value) {
1557
+ const items = Array.isArray(value) ? value : [value];
1558
+ return items.filter(isBareNodeReference).map((ref) => iriTerm(ref.id));
1559
+ }
1489
1560
  /**
1490
1561
  * Processes IRNodeData fields into DELETE/INSERT/WHERE triples.
1491
1562
  * Shared between updateToAlgebra (IRI subject) and updateWhereToAlgebra (variable subject).
1492
1563
  */
1493
1564
  function processUpdateFields(data, subjectTerm, options) {
1565
+ var _a, _b;
1494
1566
  const deletePatterns = [];
1495
1567
  const insertPatterns = [];
1496
1568
  const oldValueTriples = [];
@@ -1505,6 +1577,10 @@ function processUpdateFields(data, subjectTerm, options) {
1505
1577
  const propertyTerm = resolvePropertyPredicateTerm(field.property);
1506
1578
  const suffix = propertySuffix(field.property);
1507
1579
  const isContainsField = propertyTerm.kind === 'iri' && updContainsPreds.includes(propertyTerm.value);
1580
+ // Only a `contains` property that declares a value shape deletes the old value's own
1581
+ // triples. Without one (a polymorphic slot like `sh:path`) the value may be a shared
1582
+ // IRI, so it is left to the cascade, which deletes it only when it is dependent-typed.
1583
+ const hasValueShape = isContainsField && !!((_a = findPropertyShapeById(field.property)) === null || _a === void 0 ? void 0 : _a.valueShape);
1508
1584
  // Check for set modification ({add, remove})
1509
1585
  if (field.value &&
1510
1586
  typeof field.value === 'object' &&
@@ -1515,17 +1591,26 @@ function processUpdateFields(data, subjectTerm, options) {
1515
1591
  ('add' in field.value || 'remove' in field.value)) {
1516
1592
  const setMod = field.value;
1517
1593
  if (setMod.remove) {
1594
+ // A value that is removed and added back in the same modification stays: its edge
1595
+ // is deleted and re-inserted (a no-op), and the node itself is not touched.
1596
+ const readdedIds = new Set(((_b = setMod.add) !== null && _b !== void 0 ? _b : []).filter(isBareNodeReference).map((ref) => ref.id));
1518
1597
  setMod.remove.forEach((removeItem, ri) => {
1519
- const removeTerm = iriTerm(removeItem.id);
1520
- deletePatterns.push(tripleOf(subjectTerm, propertyTerm, removeTerm));
1521
- oldValueTriples.push(tripleOf(subjectTerm, propertyTerm, removeTerm));
1598
+ const removeId = removeItem.id;
1599
+ const removeTerm = iriTerm(removeId);
1600
+ const edge = tripleOf(subjectTerm, propertyTerm, removeTerm);
1601
+ deletePatterns.push(edge);
1602
+ oldValueTriples.push(edge);
1522
1603
  // For a `contains` set, removing a value deletes the removed node itself
1523
1604
  // (its own triples) and its owned subtree — exclusive ownership via the edge.
1524
- if (isContainsField) {
1525
- const self = buildOwnedSelfDelete(subjectTerm, propertyTerm, removeTerm, `ucr_${suffix}_${ri}_`);
1526
- deletePatterns.push(...self.deletePatterns);
1527
- cascadeOptionals.push(...self.whereOptionals);
1528
- const cascade = buildOwnedCascade(removeTerm, `ucr_${suffix}_${ri}_`);
1605
+ if (isContainsField && !readdedIds.has(removeId)) {
1606
+ if (hasValueShape) {
1607
+ const self = buildOwnedSelfDelete(subjectTerm, propertyTerm, removeTerm, `ucr_${suffix}_${ri}_`);
1608
+ deletePatterns.push(...self.deletePatterns);
1609
+ cascadeOptionals.push(...self.whereOptionals);
1610
+ }
1611
+ // The edge leads the cascade too: with a zero-or-more path the removed node is
1612
+ // itself a match, and it may only be deleted while it is still owned here.
1613
+ const cascade = buildOwnedCascade(removeTerm, `ucr_${suffix}_${ri}_`, edge);
1529
1614
  deletePatterns.push(...cascade.deletePatterns);
1530
1615
  cascadeOptionals.push(...cascade.whereOptionals);
1531
1616
  }
@@ -1535,7 +1620,7 @@ function processUpdateFields(data, subjectTerm, options) {
1535
1620
  for (const addItem of setMod.add) {
1536
1621
  if (addItem && typeof addItem === 'object' && 'shape' in addItem && 'fields' in addItem) {
1537
1622
  const nested = generateNodeDataTriples(addItem, options);
1538
- insertPatterns.push(tripleOf(subjectTerm, propertyTerm, iriTerm(nested.uri)));
1623
+ insertPatterns.push(tripleOf(subjectTerm, propertyTerm, nested.term));
1539
1624
  insertPatterns.push(...nested.triples);
1540
1625
  }
1541
1626
  else {
@@ -1551,7 +1636,12 @@ function processUpdateFields(data, subjectTerm, options) {
1551
1636
  // Non-set-modification replace of a `contains` property: cascade the old value's
1552
1637
  // subtree (every branch below binds `old_${suffix}` in the WHERE).
1553
1638
  if (isContainsField) {
1554
- containsOldVars.push({ oldVar: `old_${suffix}`, propertyTerm });
1639
+ containsOldVars.push({
1640
+ oldVar: `old_${suffix}`,
1641
+ propertyTerm,
1642
+ keep: keptReferences(field.value),
1643
+ hasValueShape,
1644
+ });
1555
1645
  }
1556
1646
  // Unset (undefined/null) — delete only
1557
1647
  if (field.value === undefined || field.value === null) {
@@ -1568,7 +1658,7 @@ function processUpdateFields(data, subjectTerm, options) {
1568
1658
  for (const item of field.value) {
1569
1659
  if (item && typeof item === 'object' && 'shape' in item && 'fields' in item) {
1570
1660
  const nested = generateNodeDataTriples(item, options);
1571
- insertPatterns.push(tripleOf(subjectTerm, propertyTerm, iriTerm(nested.uri)));
1661
+ insertPatterns.push(tripleOf(subjectTerm, propertyTerm, nested.term));
1572
1662
  insertPatterns.push(...nested.triples);
1573
1663
  }
1574
1664
  else {
@@ -1586,7 +1676,7 @@ function processUpdateFields(data, subjectTerm, options) {
1586
1676
  deletePatterns.push(tripleOf(subjectTerm, propertyTerm, oldVar));
1587
1677
  oldValueTriples.push(tripleOf(subjectTerm, propertyTerm, oldVar));
1588
1678
  const nested = generateNodeDataTriples(field.value, options);
1589
- insertPatterns.push(tripleOf(subjectTerm, propertyTerm, iriTerm(nested.uri)));
1679
+ insertPatterns.push(tripleOf(subjectTerm, propertyTerm, nested.term));
1590
1680
  insertPatterns.push(...nested.triples);
1591
1681
  continue;
1592
1682
  }
@@ -1636,15 +1726,19 @@ function processUpdateFields(data, subjectTerm, options) {
1636
1726
  }
1637
1727
  // For each replaced `contains` property: delete the old value's own triples
1638
1728
  // (exclusive ownership via the edge — backlog 032) then cascade any owned descendants.
1639
- containsOldVars.forEach(({ oldVar, propertyTerm }, n) => {
1729
+ // Values re-sent as a bare `{id}` are kept: their blocks are filtered, so re-sending the
1730
+ // current value is a no-op (the edge is deleted and re-inserted).
1731
+ containsOldVars.forEach(({ oldVar, propertyTerm, keep, hasValueShape }, n) => {
1640
1732
  const oldTerm = varTerm(oldVar);
1641
- const self = buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, `uc${n}_`);
1642
- deletePatterns.push(...self.deletePatterns);
1643
- cascadeOptionals.push(...self.whereOptionals);
1733
+ if (hasValueShape) {
1734
+ const self = buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, `uc${n}_`, keep);
1735
+ deletePatterns.push(...self.deletePatterns);
1736
+ cascadeOptionals.push(...self.whereOptionals);
1737
+ }
1644
1738
  // Every cascade block binds `?old` itself through the owning edge, like the
1645
1739
  // self-delete block does. Without it an absent old value leaves `?old` unbound and
1646
1740
  // the block matches the owned subtree of EVERY node in the graph.
1647
- const cascade = buildOwnedCascade(oldTerm, `uc${n}_`, tripleOf(subjectTerm, propertyTerm, oldTerm));
1741
+ const cascade = buildOwnedCascade(oldTerm, `uc${n}_`, tripleOf(subjectTerm, propertyTerm, oldTerm), keep);
1648
1742
  deletePatterns.push(...cascade.deletePatterns);
1649
1743
  cascadeOptionals.push(...cascade.whereOptionals);
1650
1744
  });
@@ -1846,11 +1940,37 @@ function buildUpdateWhere(base, result, traversalPatterns, subjectTerm) {
1846
1940
  // - a property marked `contains` → the cascade FOLLOWS that edge,
1847
1941
  // - a shape marked `dependent` → its instances may be DELETED when reached.
1848
1942
  // Both are read from the live registry, so no vocabulary is hardcoded. From a root
1849
- // node we follow `(c1|c2|…)+` over all contains predicates and, for each dependent
1850
- // targetClass, wildcard-delete the reached typed nodes. Requiring an asserted
1851
- // `?owned a <dependentType>` naturally excludes shared predicate IRIs (e.g. a simple
1852
- // `sh:path`) and `rdf:nil` (never typed), so only owned structural nodes are removed.
1943
+ // node we follow `(c1|c2|…)*` over all contains predicates (zero hops included, so a
1944
+ // dependent-typed root is itself a match) and, for each dependent targetClass,
1945
+ // wildcard-delete the reached typed nodes. Requiring an asserted `?owned a <dependentType>`
1946
+ // naturally excludes shared predicate IRIs (e.g. a simple `sh:path`) and `rdf:nil` (never
1947
+ // typed), so only owned structural nodes are removed.
1948
+ //
1949
+ // Whether a field owns its value is decided per predicate across the registry
1950
+ // (`collectContainment`): the replace and set-remove deletes, and the class check in
1951
+ // `ownedLinkCheck.ts`, apply that same rule. The subject deletes (by id and bulk) use the
1952
+ // deleted shape's own `contains` properties with a value shape for the immediate children.
1853
1953
  // ---------------------------------------------------------------------------
1954
+ /**
1955
+ * The simple-path predicates of the shape's own and inherited `contains` properties that
1956
+ * declare a value shape, deduplicated in declaration order. These are the properties
1957
+ * whose immediate values a subject delete removes.
1958
+ */
1959
+ function ownedValueShapePredicates(shapeId) {
1960
+ var _a, _b;
1961
+ const nodeShape = (_b = (_a = getShapeClass(shapeId)) === null || _a === void 0 ? void 0 : _a.shape) !== null && _b !== void 0 ? _b : getNodeShape(shapeId);
1962
+ if (!nodeShape)
1963
+ return [];
1964
+ const predicates = new Set();
1965
+ for (const ps of getPropertyShapes(nodeShape, true)) {
1966
+ if (!ps.contains || !ps.valueShape)
1967
+ continue;
1968
+ const predicate = getSimplePathId(ps.path);
1969
+ if (predicate)
1970
+ predicates.add(predicate);
1971
+ }
1972
+ return [...predicates];
1973
+ }
1854
1974
  /** True when the shape (or an ancestor) declares at least one `contains` property. */
1855
1975
  function shapeHasContainsProperty(shapeId) {
1856
1976
  var _a, _b;
@@ -1866,7 +1986,7 @@ function shapeHasContainsProperty(shapeId) {
1866
1986
  let containmentCache = null;
1867
1987
  let containmentCacheSize = -1;
1868
1988
  /** Gather contains-predicate IRIs and dependent targetClass IRIs from the registry. */
1869
- function collectContainment() {
1989
+ export function collectContainment() {
1870
1990
  var _a;
1871
1991
  const version = getRegistryVersion();
1872
1992
  if (containmentCache && version === containmentCacheSize) {
@@ -1893,6 +2013,78 @@ function collectContainment() {
1893
2013
  containmentCacheSize = version;
1894
2014
  return containmentCache;
1895
2015
  }
2016
+ let reachableCache = new Map();
2017
+ let reachableCacheVersion = -1;
2018
+ /**
2019
+ * The dependent targetClasses a subject delete of `shapeId` can reach through `contains`
2020
+ * edges, in `collectContainment` order. A subject delete emits one cascade block per type it
2021
+ * returns, and each block is a property-path closure per subject, so a shape that owns only
2022
+ * non-dependent values (every `Thing.image` owner) pays for none.
2023
+ *
2024
+ * The walk starts at the shape and its registered sub-shapes and follows every flattened
2025
+ * property on a `contains` predicate (the registry-wide predicate rule the cascade path uses)
2026
+ * to its value shape and that shape's sub-shapes. A property on a `contains` predicate with no
2027
+ * value shape is polymorphic (`sh:path`) and may reach anything, so it yields every dependent
2028
+ * type; that keeps the shape-sync delete of a NodeShape exact.
2029
+ */
2030
+ function reachableDependentTypes(shapeId) {
2031
+ var _a, _b;
2032
+ const version = getRegistryVersion();
2033
+ if (version !== reachableCacheVersion) {
2034
+ reachableCache = new Map();
2035
+ reachableCacheVersion = version;
2036
+ }
2037
+ const cached = reachableCache.get(shapeId);
2038
+ if (cached)
2039
+ return cached;
2040
+ const { containsPreds, dependentTypes } = collectContainment();
2041
+ const shapeOf = (id) => { var _a, _b; return (_b = (_a = getShapeClass(id)) === null || _a === void 0 ? void 0 : _a.shape) !== null && _b !== void 0 ? _b : getNodeShape(id); };
2042
+ const reached = new Set();
2043
+ // Shapes whose instances an owned edge can lead to. The start shapes join only when an
2044
+ // edge leads back to them (a dependent shape that owns its own kind, like a list cell).
2045
+ const owned = new Set();
2046
+ const seen = new Set();
2047
+ const queue = [];
2048
+ const visit = (id, viaEdge) => {
2049
+ for (const sid of [id, ...getSubShapes(id).map((sub) => sub.id)]) {
2050
+ if (viaEdge)
2051
+ owned.add(sid);
2052
+ if (seen.has(sid))
2053
+ continue;
2054
+ seen.add(sid);
2055
+ queue.push(sid);
2056
+ }
2057
+ };
2058
+ visit(shapeId, false);
2059
+ let everything = false;
2060
+ while (queue.length > 0 && !everything) {
2061
+ const nodeShape = shapeOf(queue.shift());
2062
+ if (!nodeShape)
2063
+ continue;
2064
+ for (const ps of getPropertyShapes(nodeShape, true)) {
2065
+ const predicate = getSimplePathId(ps.path);
2066
+ if (!predicate || !containsPreds.includes(predicate))
2067
+ continue;
2068
+ if (!((_a = ps.valueShape) === null || _a === void 0 ? void 0 : _a.id)) {
2069
+ everything = true;
2070
+ break;
2071
+ }
2072
+ visit(ps.valueShape.id, true);
2073
+ }
2074
+ }
2075
+ if (!everything) {
2076
+ for (const id of owned) {
2077
+ const nodeShape = shapeOf(id);
2078
+ const dependent = nodeShape === null || nodeShape === void 0 ? void 0 : nodeShape.dependent;
2079
+ if (dependent && ((_b = nodeShape === null || nodeShape === void 0 ? void 0 : nodeShape.targetClass) === null || _b === void 0 ? void 0 : _b.id)) {
2080
+ reached.add(nodeShape.targetClass.id);
2081
+ }
2082
+ }
2083
+ }
2084
+ const result = everything ? dependentTypes : dependentTypes.filter((t) => reached.has(t));
2085
+ reachableCache.set(shapeId, result);
2086
+ return result;
2087
+ }
1896
2088
  /**
1897
2089
  * Build DELETE patterns + WHERE OPTIONAL blocks that cascade-delete the owned subtree
1898
2090
  * reachable from `rootTerm` via `contains` edges. `varPrefix` keeps generated variables
@@ -1900,16 +2092,24 @@ function collectContainment() {
1900
2092
  *
1901
2093
  * `anchor`, when given, leads every block: the triple that binds a variable root, so
1902
2094
  * each block stays correct on its own (as a UNION branch) when no such root exists.
2095
+ * `onlyTypes`, when given, restricts the blocks to those dependent types (a subject delete
2096
+ * passes the types its shape can reach); by default every registered dependent type gets one.
1903
2097
  */
1904
- export function buildOwnedCascade(rootTerm, varPrefix, anchor) {
1905
- const { containsPreds, dependentTypes } = collectContainment();
2098
+ export function buildOwnedCascade(rootTerm, varPrefix, anchor, keep, onlyTypes) {
2099
+ const containment = collectContainment();
2100
+ const { containsPreds } = containment;
2101
+ const dependentTypes = onlyTypes !== null && onlyTypes !== void 0 ? onlyTypes : containment.dependentTypes;
1906
2102
  if (containsPreds.length === 0 || dependentTypes.length === 0) {
1907
2103
  return { deletePatterns: [], whereOptionals: [] };
1908
2104
  }
1909
- // (c1|c2|…)+ — follow one-or-more contains edges from the root.
2105
+ // (c1|c2|…)* — zero or more contains edges, so the root itself is matched when it is
2106
+ // dependent-typed. That is what cleans up the value of a `contains` property that has
2107
+ // no value shape (a polymorphic slot like `sh:path`): it is deleted only when it is a
2108
+ // dependent node, never when it is a shared IRI. On a subject delete `*` also matches
2109
+ // the subject, which is being deleted anyway.
1910
2110
  const pathExpr = containsPreds.length === 1
1911
- ? { oneOrMore: { id: containsPreds[0] } }
1912
- : { oneOrMore: { alt: containsPreds.map((id) => ({ id })) } };
2111
+ ? { zeroOrMore: { id: containsPreds[0] } }
2112
+ : { zeroOrMore: { alt: containsPreds.map((id) => ({ id })) } };
1913
2113
  const pathTerm = {
1914
2114
  kind: 'path',
1915
2115
  value: pathExprToSparql(pathExpr),
@@ -1922,7 +2122,7 @@ export function buildOwnedCascade(rootTerm, varPrefix, anchor) {
1922
2122
  const p = varTerm(`${varPrefix}op${i}`);
1923
2123
  const o = varTerm(`${varPrefix}ov${i}`);
1924
2124
  deletePatterns.push(tripleOf(owned, p, o));
1925
- whereOptionals.push({
2125
+ whereOptionals.push(keepFilter({
1926
2126
  type: 'bgp',
1927
2127
  triples: [
1928
2128
  ...(anchor ? [anchor] : []),
@@ -1930,10 +2130,76 @@ export function buildOwnedCascade(rootTerm, varPrefix, anchor) {
1930
2130
  tripleOf(owned, iriTerm(RDF_TYPE), iriTerm(typeId)),
1931
2131
  tripleOf(owned, p, o),
1932
2132
  ],
1933
- });
2133
+ }, rootTerm, keep));
2134
+ });
2135
+ // Blank nodes carry no type (a SHACL path operator must be the subject of exactly one
2136
+ // triple), so the typed blocks above cannot see them. Every blank node owned through
2137
+ // `contains` edges (`?b0`) is deleted, together with the RDF-list / SHACL-path syntax below it
2138
+ // (`?bn`). That inner walk follows `rdf:first` too, because a sequence member that is itself a
2139
+ // path operator is held by `rdf:first`, which is not a `contains` edge. It is restricted to the
2140
+ // syntax predicates on purpose: SPARQL cannot require the nodes *between* `?b0` and `?bn` to be
2141
+ // blank, so following every `contains` edge there would let a list's `rdf:first` lead into an
2142
+ // entity it merely lists and delete that entity's blank children.
2143
+ const insidePathExpr = { zeroOrMore: { alt: BLANK_SYNTAX_PREDICATES.map((id) => ({ id })) } };
2144
+ const insidePathTerm = {
2145
+ kind: 'path',
2146
+ value: pathExprToSparql(insidePathExpr),
2147
+ uris: collectPathUris(insidePathExpr),
2148
+ };
2149
+ const ownedBlank = varTerm(`${varPrefix}bn0`);
2150
+ const blank = varTerm(`${varPrefix}bn`);
2151
+ const bp = varTerm(`${varPrefix}bnp`);
2152
+ const bo = varTerm(`${varPrefix}bno`);
2153
+ const isBlank = (name) => ({
2154
+ kind: 'function_expr',
2155
+ name: 'isBlank',
2156
+ args: [{ kind: 'variable_expr', name }],
1934
2157
  });
2158
+ deletePatterns.push(tripleOf(blank, bp, bo));
2159
+ whereOptionals.push(keepFilter({
2160
+ type: 'filter',
2161
+ expression: isBlank(`${varPrefix}bn`),
2162
+ inner: {
2163
+ type: 'join',
2164
+ // Filter the owned blank nodes before the inner walk runs from each of them.
2165
+ left: {
2166
+ type: 'filter',
2167
+ expression: isBlank(`${varPrefix}bn0`),
2168
+ inner: {
2169
+ type: 'bgp',
2170
+ triples: [...(anchor ? [anchor] : []), tripleOf(rootTerm, pathTerm, ownedBlank)],
2171
+ },
2172
+ },
2173
+ right: {
2174
+ type: 'bgp',
2175
+ triples: [tripleOf(ownedBlank, insidePathTerm, blank), tripleOf(blank, bp, bo)],
2176
+ },
2177
+ },
2178
+ }, rootTerm, keep));
1935
2179
  return { deletePatterns, whereOptionals };
1936
2180
  }
2181
+ /**
2182
+ * Wrap an owned-cleanup block in `FILTER(?old NOT IN (<k1>, …))` so a value the mutation
2183
+ * keeps (re-sent as a bare `{id}`) is not deleted. The filter sits inside the block, next
2184
+ * to the edge triple that binds `?old`, so it only removes that block's matches; the
2185
+ * edge-only delete of the same value stays and is undone by the INSERT. With an IRI old
2186
+ * term, or nothing to keep, the block is returned unchanged.
2187
+ */
2188
+ function keepFilter(block, oldTerm, keep) {
2189
+ const kept = (keep !== null && keep !== void 0 ? keep : []).flatMap((term) => term.kind === 'iri' ? [{ kind: 'iri_expr', value: term.value }] : []);
2190
+ if (oldTerm.kind !== 'variable' || kept.length === 0)
2191
+ return block;
2192
+ return {
2193
+ type: 'filter',
2194
+ expression: {
2195
+ kind: 'in_expr',
2196
+ negated: true,
2197
+ value: { kind: 'variable_expr', name: oldTerm.name },
2198
+ list: kept,
2199
+ },
2200
+ inner: block,
2201
+ };
2202
+ }
1937
2203
  /**
1938
2204
  * Build DELETE + WHERE OPTIONAL that removes the *old node's own* one-hop triples when a
1939
2205
  * `contains` edge is replaced, unset, or set-removed. A `contains` edge asserts exclusive
@@ -1942,22 +2208,31 @@ export function buildOwnedCascade(rootTerm, varPrefix, anchor) {
1942
2208
  * does NOT require the child shape to be `dependent`: ownership via the edge is sufficient.
1943
2209
  * Owned descendants *below* the old node are cascaded separately by `buildOwnedCascade`.
1944
2210
  *
2211
+ * Callers use it only for a property that declares a value shape. A shape-less `contains`
2212
+ * slot (like `sh:path`) may hold a shared IRI, so its old value is left to
2213
+ * `buildOwnedCascade`, which deletes it only when it is dependent-typed.
2214
+ *
1945
2215
  * The owning edge `<subject> <prop> ?old` is re-asserted INSIDE the WHERE group so `?old`
1946
2216
  * is bound there: when no old value exists the group matches nothing and the DELETE is a
1947
2217
  * no-op, rather than an unbound `?old` matching (and deleting) the whole graph. When
1948
2218
  * `oldTerm` is a concrete IRI (set-remove), the guard just confirms the link still holds.
2219
+ *
2220
+ * `keep` lists the bare `{id}`s the mutation writes again (a re-sent current value, the
2221
+ * retained ids of an array overwrite). With a variable `oldTerm` the block is wrapped in
2222
+ * `FILTER(?old NOT IN (…))`, so a kept value is not deleted; its edge is deleted and
2223
+ * re-inserted, a no-op. A nested create carrying an id is never kept: it rewrites that node.
1949
2224
  */
1950
- export function buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, varPrefix) {
2225
+ export function buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, varPrefix, keep) {
1951
2226
  const p = varTerm(`${varPrefix}sp`);
1952
2227
  const o = varTerm(`${varPrefix}so`);
1953
2228
  const wildcard = tripleOf(oldTerm, p, o);
1954
2229
  return {
1955
2230
  deletePatterns: [wildcard],
1956
2231
  whereOptionals: [
1957
- {
2232
+ keepFilter({
1958
2233
  type: 'bgp',
1959
2234
  triples: [tripleOf(subjectTerm, propertyTerm, oldTerm), wildcard],
1960
- },
2235
+ }, oldTerm, keep),
1961
2236
  ],
1962
2237
  };
1963
2238
  }
@@ -1966,7 +2241,12 @@ export function buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, varPref
1966
2241
  *
1967
2242
  * The WHERE is the type guard of every id, joined with ONE UNION of independent
1968
2243
  * blocks: per id its own triples, its incoming references and, for a shape that
1969
- * owns something, one block per dependent type of its owned subtree.
2244
+ * owns something, one block per immediately-owned child (`<s> <p> ?dN . ?dN ?dN_p ?dN_o`,
2245
+ * for each of the shape's own and inherited `contains` properties with a value shape and
2246
+ * a simple path) and one block per dependent type its owned subtree can reach
2247
+ * (`reachableDependentTypes`; none for a shape that owns only non-dependent values). The
2248
+ * bulk deletes (`deleteAllToAlgebra`, `deleteWhereToAlgebra`) add the same owned blocks,
2249
+ * rooted at the subject variable (see `bulkDeleteUnion`).
1970
2250
  *
1971
2251
  * The blocks must not be chained as OPTIONALs. A chain of OPTIONALs is a join, so
1972
2252
  * the solutions multiply: own triples x incoming references x owned triples of each
@@ -1984,6 +2264,9 @@ export function deleteToAlgebra(query, _options) {
1984
2264
  const deletePatterns = [];
1985
2265
  const guards = [];
1986
2266
  const blocks = [];
2267
+ const hasContains = shapeHasContainsProperty(query.shape);
2268
+ const ownedChildPredicates = hasContains ? ownedValueShapePredicates(query.shape) : [];
2269
+ const cascadeTypes = hasContains ? reachableDependentTypes(query.shape) : [];
1987
2270
  for (let i = 0; i < query.ids.length; i++) {
1988
2271
  const subjectTerm = iriTerm(query.ids[i].id);
1989
2272
  const idx = query.ids.length > 1 ? `_${i}` : '';
@@ -1996,10 +2279,22 @@ export function deleteToAlgebra(query, _options) {
1996
2279
  // incoming references (the entity may have none) are separate UNION blocks.
1997
2280
  guards.push(typeGuard);
1998
2281
  blocks.push({ type: 'bgp', triples: [subjWild] }, { type: 'bgp', triples: [objWild] });
2282
+ // The immediately-owned children: for every `contains` property with a value shape,
2283
+ // the child's own one-hop triples, as a replace or a set-remove deletes them. No type
2284
+ // guard and no reference check: the `contains` edge alone asserts ownership.
2285
+ ownedChildPredicates.forEach((predicate, n) => {
2286
+ const child = varTerm(`d${n}${idx}`);
2287
+ const childOwn = tripleOf(child, varTerm(`d${n}${idx}_p`), varTerm(`d${n}${idx}_o`));
2288
+ deletePatterns.push(childOwn);
2289
+ blocks.push({
2290
+ type: 'bgp',
2291
+ triples: [tripleOf(subjectTerm, iriTerm(predicate), child), childOwn],
2292
+ });
2293
+ });
1999
2294
  // Cascade-delete the owned subtree — only for shapes that actually own something
2000
2295
  // (have a `contains` property); a plain entity delete is left untouched.
2001
- if (shapeHasContainsProperty(query.shape)) {
2002
- const cascade = buildOwnedCascade(subjectTerm, `c${idx}_`);
2296
+ if (hasContains && cascadeTypes.length > 0) {
2297
+ const cascade = buildOwnedCascade(subjectTerm, `c${idx}_`, undefined, undefined, cascadeTypes);
2003
2298
  deletePatterns.push(...cascade.deletePatterns);
2004
2299
  blocks.push(...cascade.whereOptionals);
2005
2300
  }
@@ -2079,13 +2374,39 @@ function blankNodeBlocks(shapeId, parentVar, path, counter, deletePatterns, bloc
2079
2374
  }
2080
2375
  }
2081
2376
  /**
2082
- * The blank-node cleanup blocks of a bulk delete, already joined with the root's own
2083
- * triples as ONE UNION so their matches add up instead of multiplying; `null` when the
2084
- * shape has no blank-node property (the root wildcard then stays in the required BGP).
2377
+ * The cleanup blocks of a bulk delete, already joined with the root's own triples as ONE
2378
+ * UNION so their matches add up instead of multiplying; `null` when there is nothing to
2379
+ * clean up (the root wildcard then stays in the required BGP).
2380
+ *
2381
+ * - The blank-node blocks of the shape's blank-node properties.
2382
+ * - For a shape that owns something (a `contains` property), the same owned cleanup as a
2383
+ * delete by id: the immediately-owned children of every `contains` property with a value
2384
+ * shape, and the dependent cascade, rooted at the subject variable. Each owned block is led
2385
+ * by the subject's type triple. With a linear join it is redundant; it keeps a branch that
2386
+ * is evaluated on its own from running `?a0 (c…)* ?own` with both ends unbound, which
2387
+ * matches every node in the graph.
2085
2388
  */
2086
- function bulkDeleteUnion(shapeId, subjectVar, rootWildcard, deletePatterns) {
2389
+ function bulkDeleteUnion(shapeId, subjectVar, rootWildcard, typeTriple, deletePatterns) {
2087
2390
  const blocks = [];
2088
2391
  blankNodeBlocks(shapeId, subjectVar, { triples: [], bnVars: [] }, { next: 1 }, deletePatterns, blocks);
2392
+ if (shapeHasContainsProperty(shapeId)) {
2393
+ const subject = varTerm(subjectVar);
2394
+ ownedValueShapePredicates(shapeId).forEach((predicate, n) => {
2395
+ const child = varTerm(`d${n}`);
2396
+ const childOwn = tripleOf(child, varTerm(`d${n}_p`), varTerm(`d${n}_o`));
2397
+ deletePatterns.push(childOwn);
2398
+ blocks.push({
2399
+ type: 'bgp',
2400
+ triples: [typeTriple, tripleOf(subject, iriTerm(predicate), child), childOwn],
2401
+ });
2402
+ });
2403
+ const cascadeTypes = reachableDependentTypes(shapeId);
2404
+ if (cascadeTypes.length > 0) {
2405
+ const cascade = buildOwnedCascade(subject, 'c_', typeTriple, undefined, cascadeTypes);
2406
+ deletePatterns.push(...cascade.deletePatterns);
2407
+ blocks.push(...cascade.whereOptionals);
2408
+ }
2409
+ }
2089
2410
  if (blocks.length === 0)
2090
2411
  return null;
2091
2412
  return unionOf([{ type: 'bgp', triples: [rootWildcard] }, ...blocks]);
@@ -2093,8 +2414,10 @@ function bulkDeleteUnion(shapeId, subjectVar, rootWildcard, deletePatterns) {
2093
2414
  /**
2094
2415
  * Converts an IRDeleteAllMutation to a SparqlDeleteInsertPlan.
2095
2416
  *
2096
- * Generates DELETE { ?a0 ?p ?o . [blank node wildcards] }
2097
- * WHERE { ?a0 a <Shape> . ?a0 ?p ?o . OPTIONAL { [blank node traversals] } }
2417
+ * Generates DELETE { ?a0 ?p ?o . [blank-node and owned-cleanup wildcards] }
2418
+ * WHERE { ?a0 a <Shape> . { ?a0 ?p ?o } UNION [one block per blank node,
2419
+ * owned child and dependent type] }
2420
+ * (see `bulkDeleteUnion`). Unlike a delete by id, incoming references are not deleted.
2098
2421
  */
2099
2422
  export function deleteAllToAlgebra(query, _options) {
2100
2423
  const subjectVar = 'a0';
@@ -2105,7 +2428,7 @@ export function deleteAllToAlgebra(query, _options) {
2105
2428
  // WHERE: type triple, then the root wildcard and the blank-node cleanup
2106
2429
  const typeTriple = tripleOf(varTerm(subjectVar), iriTerm(RDF_TYPE), iriTerm(resolveShapeScanIri(query.shape)));
2107
2430
  const rootWildcard = tripleOf(varTerm(subjectVar), varTerm('p'), varTerm('o'));
2108
- const union = bulkDeleteUnion(query.shape, subjectVar, rootWildcard, deletePatterns);
2431
+ const union = bulkDeleteUnion(query.shape, subjectVar, rootWildcard, typeTriple, deletePatterns);
2109
2432
  const whereAlgebra = union
2110
2433
  ? { type: 'join', left: { type: 'bgp', triples: [typeTriple] }, right: union }
2111
2434
  : { type: 'bgp', triples: [typeTriple, rootWildcard] };
@@ -2119,7 +2442,9 @@ export function deleteAllToAlgebra(query, _options) {
2119
2442
  /**
2120
2443
  * Converts an IRDeleteWhereMutation to a SparqlDeleteInsertPlan.
2121
2444
  *
2122
- * Like deleteAllToAlgebra but adds filter conditions from the where clause.
2445
+ * Like deleteAllToAlgebra but adds filter conditions from the where clause. The cleanup
2446
+ * UNION (blank nodes, owned children, dependent cascade) is joined with the filtered match,
2447
+ * so only the matched subjects' children are deleted.
2123
2448
  */
2124
2449
  export function deleteWhereToAlgebra(query, _options) {
2125
2450
  const subjectVar = 'a0';
@@ -2131,7 +2456,7 @@ export function deleteWhereToAlgebra(query, _options) {
2131
2456
  // WHERE: type triple + the where clause; the root wildcard and blank-node cleanup follow
2132
2457
  const typeTriple = tripleOf(varTerm(subjectVar), iriTerm(RDF_TYPE), iriTerm(resolveShapeScanIri(query.shape)));
2133
2458
  const rootWildcard = tripleOf(varTerm(subjectVar), varTerm('p'), varTerm('o'));
2134
- const union = bulkDeleteUnion(query.shape, subjectVar, rootWildcard, deletePatterns);
2459
+ const union = bulkDeleteUnion(query.shape, subjectVar, rootWildcard, typeTriple, deletePatterns);
2135
2460
  let whereAlgebra = {
2136
2461
  type: 'bgp',
2137
2462
  triples: union ? [typeTriple] : [typeTriple, rootWildcard],