@_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,13 @@ 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, getSubShapes, getTargetClassId, } from '../utils/ShapeClass.js';
9
+ import { getAllNodeShapes, getNodeShape, getRegistryVersion, getShapeClass, getSubShapes, getTargetClassId, isSubShapeOf, } from '../utils/ShapeClass.js';
10
10
  import { UnresolvedContextError } from '../queries/QueryContext.js';
11
11
  // ---------------------------------------------------------------------------
12
12
  // Constants
13
13
  // ---------------------------------------------------------------------------
14
14
  const RDF_TYPE = rdf.type.id;
15
+ const RDF_NIL = rdf.nil.id;
15
16
  const XSD_DATETIME = xsd.dateTime.id;
16
17
  const XSD_DATE = xsd.date.id;
17
18
  const XSD_TIME = xsd.time.id;
@@ -1593,7 +1594,7 @@ function keptReferences(value) {
1593
1594
  * Shared between updateToAlgebra (IRI subject) and updateWhereToAlgebra (variable subject).
1594
1595
  */
1595
1596
  function processUpdateFields(data, subjectTerm, options) {
1596
- var _a, _b, _c;
1597
+ var _a, _b;
1597
1598
  const deletePatterns = [];
1598
1599
  const insertPatterns = [];
1599
1600
  const oldValueTriples = [];
@@ -1601,18 +1602,16 @@ function processUpdateFields(data, subjectTerm, options) {
1601
1602
  // Owned cleanup on `contains` replace/remove: dropping a `contains` edge must delete
1602
1603
  // the old object itself (its own triples) AND its owned subtree, not just unlink the
1603
1604
  // one-hop edge — otherwise the old node orphans (backlog 032).
1604
- const { containsPreds: updContainsPreds } = collectContainment();
1605
1605
  const containsOldVars = [];
1606
1606
  const cascadeOptionals = [];
1607
1607
  for (const field of data.fields) {
1608
1608
  const propertyTerm = resolvePropertyPredicateTerm(field.property);
1609
1609
  const suffix = propertySuffix(field.property);
1610
- const isContainsField = propertyTerm.kind === 'iri' && updContainsPreds.includes(propertyTerm.value);
1611
- // Only a `contains` property that declares a value shape deletes the old value's own
1612
- // triples. Without one (a polymorphic slot like `sh:path`) the value may be a shared
1613
- // IRI, so it is left to the cascade, which deletes it only when it is dependent-typed.
1614
- const valueShapeId = isContainsField ? (_b = (_a = findPropertyShapeById(field.property)) === null || _a === void 0 ? void 0 : _a.valueShape) === null || _b === void 0 ? void 0 : _b.id : undefined;
1615
- const hasValueShape = !!valueShapeId;
1610
+ const fieldShape = findPropertyShapeById(field.property);
1611
+ // Ownership is the property's own: a `contains` property with a value shape and a simple
1612
+ // path (an IRI predicate term). A plain property on a predicate another shape declares
1613
+ // `contains` owns nothing.
1614
+ const valueShapeId = propertyTerm.kind === 'iri' && (fieldShape === null || fieldShape === void 0 ? void 0 : fieldShape.contains) ? (_a = fieldShape.valueShape) === null || _a === void 0 ? void 0 : _a.id : undefined;
1616
1615
  // Check for set modification ({add, remove})
1617
1616
  if (field.value &&
1618
1617
  typeof field.value === 'object' &&
@@ -1625,7 +1624,7 @@ function processUpdateFields(data, subjectTerm, options) {
1625
1624
  if (setMod.remove) {
1626
1625
  // A value that is removed and added back in the same modification stays: its edge
1627
1626
  // is deleted and re-inserted (a no-op), and the node itself is not touched.
1628
- const readdedIds = new Set(((_c = setMod.add) !== null && _c !== void 0 ? _c : []).filter(isBareNodeReference).map((ref) => ref.id));
1627
+ const readdedIds = new Set(((_b = setMod.add) !== null && _b !== void 0 ? _b : []).filter(isBareNodeReference).map((ref) => ref.id));
1629
1628
  setMod.remove.forEach((removeItem, ri) => {
1630
1629
  const removeId = removeItem.id;
1631
1630
  const removeTerm = iriTerm(removeId);
@@ -1634,24 +1633,21 @@ function processUpdateFields(data, subjectTerm, options) {
1634
1633
  oldValueTriples.push(edge);
1635
1634
  // For a `contains` set, removing a value deletes the removed node itself
1636
1635
  // (its own triples) and its owned subtree — exclusive ownership via the edge.
1637
- if (isContainsField && !readdedIds.has(removeId)) {
1638
- if (hasValueShape) {
1639
- const self = buildOwnedSelfDelete(subjectTerm, propertyTerm, removeTerm, `ucr_${suffix}_${ri}_`);
1640
- deletePatterns.push(...self.deletePatterns);
1641
- cascadeOptionals.push(...self.whereOptionals);
1642
- const blank = valueShapeId
1643
- ? subjectBlankCascade(valueShapeId, removeTerm, [edge], `ucr_${suffix}_${ri}_vb_`)
1644
- : null;
1645
- if (blank) {
1646
- deletePatterns.push(...blank.deletePatterns);
1647
- cascadeOptionals.push(blank.whereBlock);
1648
- }
1636
+ if (valueShapeId && !readdedIds.has(removeId)) {
1637
+ const prefix = `ucr_${suffix}_${ri}_`;
1638
+ const self = buildOwnedSelfDelete(subjectTerm, propertyTerm, removeTerm, prefix);
1639
+ deletePatterns.push(...self.deletePatterns);
1640
+ cascadeOptionals.push(...self.whereOptionals);
1641
+ const blank = subjectBlankCascade(valueShapeId, removeTerm, [edge], `${prefix}vb_`);
1642
+ if (blank) {
1643
+ deletePatterns.push(...blank.deletePatterns);
1644
+ cascadeOptionals.push(blank.whereBlock);
1649
1645
  }
1650
- // The edge leads the cascade too: with a zero-or-more path the removed node is
1651
- // itself a match, and it may only be deleted while it is still owned here.
1652
- const cascade = buildOwnedCascade(removeTerm, `ucr_${suffix}_${ri}_`, edge);
1653
- deletePatterns.push(...cascade.deletePatterns);
1654
- cascadeOptionals.push(...cascade.whereOptionals);
1646
+ // The edge leads every branch: the removed node's subtree may only be deleted
1647
+ // while the node is still owned here.
1648
+ const owned = ownedSubtreeBlocks(valueShapeId, removeTerm, [edge], prefix);
1649
+ deletePatterns.push(...owned.deletePatterns);
1650
+ cascadeOptionals.push(...owned.whereBlocks);
1655
1651
  }
1656
1652
  });
1657
1653
  }
@@ -1672,7 +1668,6 @@ function processUpdateFields(data, subjectTerm, options) {
1672
1668
  }
1673
1669
  // Replace / unset / overwrite of a blank-capable property: its old blank value and the
1674
1670
  // blank nodes below it go (the branch binds `old_${suffix}` through the edge itself).
1675
- const fieldShape = findPropertyShapeById(field.property);
1676
1671
  if (isBlankCapable(fieldShape)) {
1677
1672
  const blank = buildBlankCascade([tripleOf(subjectTerm, propertyTerm, varTerm(`old_${suffix}`))], varTerm(`old_${suffix}`), null, blankWalkPredicates([fieldShape]), `ub_${suffix}_`);
1678
1673
  if (blank) {
@@ -1682,12 +1677,11 @@ function processUpdateFields(data, subjectTerm, options) {
1682
1677
  }
1683
1678
  // Non-set-modification replace of a `contains` property: cascade the old value's
1684
1679
  // subtree (every branch below binds `old_${suffix}` in the WHERE).
1685
- if (isContainsField) {
1680
+ if (valueShapeId) {
1686
1681
  containsOldVars.push({
1687
1682
  oldVar: `old_${suffix}`,
1688
1683
  propertyTerm,
1689
1684
  keep: keptReferences(field.value),
1690
- hasValueShape,
1691
1685
  valueShapeId,
1692
1686
  });
1693
1687
  }
@@ -1772,27 +1766,24 @@ function processUpdateFields(data, subjectTerm, options) {
1772
1766
  // (exclusive ownership via the edge — backlog 032) then cascade any owned descendants.
1773
1767
  // Values re-sent as a bare `{id}` are kept: their blocks are filtered, so re-sending the
1774
1768
  // current value is a no-op (the edge is deleted and re-inserted).
1775
- containsOldVars.forEach(({ oldVar, propertyTerm, keep, hasValueShape, valueShapeId }, n) => {
1769
+ containsOldVars.forEach(({ oldVar, propertyTerm, keep, valueShapeId }, n) => {
1776
1770
  const oldTerm = varTerm(oldVar);
1777
- if (hasValueShape) {
1778
- const self = buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, `uc${n}_`, keep);
1779
- deletePatterns.push(...self.deletePatterns);
1780
- cascadeOptionals.push(...self.whereOptionals);
1781
- // The old value's own blank values go with it.
1782
- const blank = valueShapeId
1783
- ? subjectBlankCascade(valueShapeId, oldTerm, [tripleOf(subjectTerm, propertyTerm, oldTerm)], `uc${n}_vb_`)
1784
- : null;
1785
- if (blank) {
1786
- deletePatterns.push(...blank.deletePatterns);
1787
- cascadeOptionals.push(keepFilter(blank.whereBlock, oldTerm, keep));
1788
- }
1789
- }
1790
- // Every cascade block binds `?old` itself through the owning edge, like the
1791
- // self-delete block does. Without it an absent old value leaves `?old` unbound and
1792
- // the block matches the owned subtree of EVERY node in the graph.
1793
- const cascade = buildOwnedCascade(oldTerm, `uc${n}_`, tripleOf(subjectTerm, propertyTerm, oldTerm), keep);
1794
- deletePatterns.push(...cascade.deletePatterns);
1795
- cascadeOptionals.push(...cascade.whereOptionals);
1771
+ const edge = tripleOf(subjectTerm, propertyTerm, oldTerm);
1772
+ const self = buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, `uc${n}_`, keep);
1773
+ deletePatterns.push(...self.deletePatterns);
1774
+ cascadeOptionals.push(...self.whereOptionals);
1775
+ // The old value's own blank values go with it.
1776
+ const blank = subjectBlankCascade(valueShapeId, oldTerm, [edge], `uc${n}_vb_`);
1777
+ if (blank) {
1778
+ deletePatterns.push(...blank.deletePatterns);
1779
+ cascadeOptionals.push(keepFilter(blank.whereBlock, oldTerm, keep));
1780
+ }
1781
+ // Every subtree branch binds `?old` itself through the owning edge, like the self-delete
1782
+ // block does. Without it an absent old value leaves `?old` unbound and the branch matches
1783
+ // the owned subtree of EVERY node in the graph.
1784
+ const owned = ownedSubtreeBlocks(valueShapeId, oldTerm, [edge], `uc${n}_`, keep);
1785
+ deletePatterns.push(...owned.deletePatterns);
1786
+ cascadeOptionals.push(...owned.whereBlocks);
1796
1787
  });
1797
1788
  return {
1798
1789
  deletePatterns,
@@ -1986,212 +1977,6 @@ function buildUpdateWhere(base, result, traversalPatterns, subjectTerm) {
1986
1977
  return whereAlgebra;
1987
1978
  }
1988
1979
  // ---------------------------------------------------------------------------
1989
- // Owned-subtree cascade
1990
- //
1991
- // Composition cleanup driven by two declarative flags:
1992
- // - a property marked `contains` → the cascade FOLLOWS that edge,
1993
- // - a shape marked `dependent` → its instances may be DELETED when reached.
1994
- // Both are read from the live registry, so no vocabulary is hardcoded. From a root
1995
- // node we follow `(c1|c2|…)*` over all contains predicates (zero hops included, so a
1996
- // dependent-typed root is itself a match) and, for each dependent targetClass,
1997
- // wildcard-delete the reached typed nodes. Requiring an asserted `?owned a <dependentType>`
1998
- // naturally excludes shared predicate IRIs (e.g. a simple `sh:path`) and `rdf:nil` (never
1999
- // typed), so only owned structural nodes are removed.
2000
- //
2001
- // Whether a field owns its value is decided per predicate across the registry
2002
- // (`collectContainment`): the replace and set-remove deletes, and the class check in
2003
- // `ownedLinkCheck.ts`, apply that same rule. The subject deletes (by id and bulk) use the
2004
- // deleted shape's own `contains` properties with a value shape for the immediate children.
2005
- // ---------------------------------------------------------------------------
2006
- /**
2007
- * The simple-path predicates of the shape's own and inherited `contains` properties that
2008
- * declare a value shape, deduplicated in declaration order. These are the properties
2009
- * whose immediate values a subject delete removes.
2010
- */
2011
- function ownedValueShapePredicates(shapeId) {
2012
- var _a, _b;
2013
- const nodeShape = (_b = (_a = getShapeClass(shapeId)) === null || _a === void 0 ? void 0 : _a.shape) !== null && _b !== void 0 ? _b : getNodeShape(shapeId);
2014
- if (!nodeShape)
2015
- return [];
2016
- const predicates = new Set();
2017
- for (const ps of getPropertyShapes(nodeShape, true)) {
2018
- if (!ps.contains || !ps.valueShape)
2019
- continue;
2020
- const predicate = getSimplePathId(ps.path);
2021
- if (predicate)
2022
- predicates.add(predicate);
2023
- }
2024
- return [...predicates];
2025
- }
2026
- /**
2027
- * The blank-node cascade of the immediately-owned children of `shapeId` on `predicate` (see
2028
- * `ownedValueShapePredicates`): the blank values of every value shape its `contains` properties
2029
- * on that predicate declare. `ownerTriples` bind the child.
2030
- */
2031
- function ownedChildBlankCascade(shapeId, predicate, childTerm, ownerTriples, varPrefix) {
2032
- var _a, _b, _c;
2033
- const nodeShape = (_b = (_a = getShapeClass(shapeId)) === null || _a === void 0 ? void 0 : _a.shape) !== null && _b !== void 0 ? _b : getNodeShape(shapeId);
2034
- if (!nodeShape)
2035
- return [];
2036
- const valueShapes = new Set();
2037
- for (const ps of getPropertyShapes(nodeShape, true)) {
2038
- if (ps.contains && ((_c = ps.valueShape) === null || _c === void 0 ? void 0 : _c.id) && getSimplePathId(ps.path) === predicate) {
2039
- valueShapes.add(ps.valueShape.id);
2040
- }
2041
- }
2042
- return [...valueShapes].flatMap((valueShapeId, i) => {
2043
- const cascade = subjectBlankCascade(valueShapeId, childTerm, ownerTriples, `${varPrefix}${i}_`);
2044
- return cascade ? [cascade] : [];
2045
- });
2046
- }
2047
- /** True when the shape (or an ancestor) declares at least one `contains` property. */
2048
- function shapeHasContainsProperty(shapeId) {
2049
- var _a, _b;
2050
- const nodeShape = (_b = (_a = getShapeClass(shapeId)) === null || _a === void 0 ? void 0 : _a.shape) !== null && _b !== void 0 ? _b : getNodeShape(shapeId);
2051
- if (!nodeShape)
2052
- return false;
2053
- return getPropertyShapes(nodeShape, true)
2054
- .some((ps) => ps.contains);
2055
- }
2056
- let containmentCache = null;
2057
- let containmentCacheSize = -1;
2058
- /** Gather contains-predicate IRIs and dependent targetClass IRIs from the registry. */
2059
- export function collectContainment() {
2060
- var _a, _b, _c;
2061
- const version = getRegistryVersion();
2062
- if (containmentCache && version === containmentCacheSize) {
2063
- return containmentCache;
2064
- }
2065
- const containsPreds = new Set();
2066
- const dependentTypes = new Set();
2067
- const dependentBlankProps = new Map();
2068
- // Per targetClass, the predicates some shape targeting it declares NOT blank-capable.
2069
- const plainPredsByType = new Map();
2070
- for (const nodeShape of getAllNodeShapes().values()) {
2071
- if (!nodeShape)
2072
- continue;
2073
- if ((_a = nodeShape.targetClass) === null || _a === void 0 ? void 0 : _a.id) {
2074
- const typeId = nodeShape.targetClass.id;
2075
- for (const ps of getPropertyShapes(nodeShape, true)) {
2076
- const predicate = getSimplePathId(ps.path);
2077
- if (!predicate || isBlankCapable(ps))
2078
- continue;
2079
- if (!plainPredsByType.has(typeId))
2080
- plainPredsByType.set(typeId, new Set());
2081
- plainPredsByType.get(typeId).add(predicate);
2082
- }
2083
- }
2084
- if (nodeShape.dependent && ((_b = nodeShape.targetClass) === null || _b === void 0 ? void 0 : _b.id)) {
2085
- const typeId = nodeShape.targetClass.id;
2086
- dependentTypes.add(typeId);
2087
- const blank = getPropertyShapes(nodeShape, true).filter(isBlankCapable);
2088
- if (blank.length > 0) {
2089
- dependentBlankProps.set(typeId, [...((_c = dependentBlankProps.get(typeId)) !== null && _c !== void 0 ? _c : []), ...blank]);
2090
- }
2091
- }
2092
- const propertyShapes = getPropertyShapes(nodeShape, false);
2093
- for (const ps of propertyShapes) {
2094
- if (ps.contains) {
2095
- const predId = getSimplePathId(ps.path);
2096
- if (predId)
2097
- containsPreds.add(predId);
2098
- }
2099
- }
2100
- }
2101
- // A typed node only says its class, not which of the shapes targeting it wrote it. When the
2102
- // shapes disagree on a predicate (`PathList.first` is blank-capable, `List.first` is not, both
2103
- // target `rdf:List`), the cascade does not follow it: a user list's blank `rdf:first` member
2104
- // is content the list does not own.
2105
- for (const [typeId, props] of dependentBlankProps) {
2106
- const plain = plainPredsByType.get(typeId);
2107
- if (!plain)
2108
- continue;
2109
- const kept = props.filter((ps) => {
2110
- const predicate = getSimplePathId(ps.path);
2111
- return !!predicate && !plain.has(predicate);
2112
- });
2113
- if (kept.length > 0)
2114
- dependentBlankProps.set(typeId, kept);
2115
- else
2116
- dependentBlankProps.delete(typeId);
2117
- }
2118
- containmentCache = { containsPreds: [...containsPreds], dependentTypes: [...dependentTypes], dependentBlankProps };
2119
- containmentCacheSize = version;
2120
- return containmentCache;
2121
- }
2122
- let reachableCache = new Map();
2123
- let reachableCacheVersion = -1;
2124
- /**
2125
- * The dependent targetClasses a subject delete of `shapeId` can reach through `contains`
2126
- * edges, in `collectContainment` order. A subject delete emits one cascade block per type it
2127
- * returns, and each block is a property-path closure per subject, so a shape that owns only
2128
- * non-dependent values (every `Thing.image` owner) pays for none.
2129
- *
2130
- * The walk starts at the shape and its registered sub-shapes and follows every flattened
2131
- * property on a `contains` predicate (the registry-wide predicate rule the cascade path uses)
2132
- * to its value shape and that shape's sub-shapes. A property on a `contains` predicate with no
2133
- * value shape is polymorphic (`sh:path`) and may reach anything, so it yields every dependent
2134
- * type; that keeps the shape-sync delete of a NodeShape exact.
2135
- */
2136
- function reachableDependentTypes(shapeId) {
2137
- var _a, _b;
2138
- const version = getRegistryVersion();
2139
- if (version !== reachableCacheVersion) {
2140
- reachableCache = new Map();
2141
- reachableCacheVersion = version;
2142
- }
2143
- const cached = reachableCache.get(shapeId);
2144
- if (cached)
2145
- return cached;
2146
- const { containsPreds, dependentTypes } = collectContainment();
2147
- 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); };
2148
- const reached = new Set();
2149
- // Shapes whose instances an owned edge can lead to. The start shapes join only when an
2150
- // edge leads back to them (a dependent shape that owns its own kind, like a list cell).
2151
- const owned = new Set();
2152
- const seen = new Set();
2153
- const queue = [];
2154
- const visit = (id, viaEdge) => {
2155
- for (const sid of [id, ...getSubShapes(id).map((sub) => sub.id)]) {
2156
- if (viaEdge)
2157
- owned.add(sid);
2158
- if (seen.has(sid))
2159
- continue;
2160
- seen.add(sid);
2161
- queue.push(sid);
2162
- }
2163
- };
2164
- visit(shapeId, false);
2165
- let everything = false;
2166
- while (queue.length > 0 && !everything) {
2167
- const nodeShape = shapeOf(queue.shift());
2168
- if (!nodeShape)
2169
- continue;
2170
- for (const ps of getPropertyShapes(nodeShape, true)) {
2171
- const predicate = getSimplePathId(ps.path);
2172
- if (!predicate || !containsPreds.includes(predicate))
2173
- continue;
2174
- if (!((_a = ps.valueShape) === null || _a === void 0 ? void 0 : _a.id)) {
2175
- everything = true;
2176
- break;
2177
- }
2178
- visit(ps.valueShape.id, true);
2179
- }
2180
- }
2181
- if (!everything) {
2182
- for (const id of owned) {
2183
- const nodeShape = shapeOf(id);
2184
- const dependent = nodeShape === null || nodeShape === void 0 ? void 0 : nodeShape.dependent;
2185
- if (dependent && ((_b = nodeShape === null || nodeShape === void 0 ? void 0 : nodeShape.targetClass) === null || _b === void 0 ? void 0 : _b.id)) {
2186
- reached.add(nodeShape.targetClass.id);
2187
- }
2188
- }
2189
- }
2190
- const result = everything ? dependentTypes : dependentTypes.filter((t) => reached.has(t));
2191
- reachableCache.set(shapeId, result);
2192
- return result;
2193
- }
2194
- // ---------------------------------------------------------------------------
2195
1980
  // Blank-node cascade: blank values are deleted through the blank-capable properties
2196
1981
  // (`isBlankCapable`) that hold them, never through `contains`.
2197
1982
  // ---------------------------------------------------------------------------
@@ -2227,8 +2012,8 @@ let blankWalkCacheVersion = -1;
2227
2012
  * the blank-capable properties of every shape the value (and, transitively, its blank values)
2228
2013
  * can have — a property's value shape and its sub-shapes. A blank-capable property without a
2229
2014
  * value shape is polymorphic (`sh:path`, the `PathNode` slots, `PathList.first`) and may hold
2230
- * any shape, so it yields every blank-capable predicate in the registry, the same rule
2231
- * `reachableDependentTypes` applies to `contains`. Precision is not what keeps the walk safe:
2015
+ * any shape, so it yields every blank-capable predicate in the registry. Precision is not what
2016
+ * keeps the walk safe:
2232
2017
  * `buildBlankCascade` refuses any node reached through an IRI.
2233
2018
  */
2234
2019
  function blankWalkPredicates(start) {
@@ -2387,72 +2172,354 @@ function subjectBlankCascade(shapeId, subjectTerm, ownerTriples, varPrefix) {
2387
2172
  return null;
2388
2173
  return buildBlankCascade(ownerTriples, subjectTerm, simplePredicates(props), blankWalkPredicates(props), varPrefix);
2389
2174
  }
2175
+ // ---------------------------------------------------------------------------
2176
+ // Owned-subtree walk
2177
+ //
2178
+ // A `contains` property (with a value shape and a simple path) owns its value: replacing,
2179
+ // unsetting or set-removing the value, or deleting the subject, deletes the value node, then
2180
+ // the values that node's shape owns, and so on down. Ownership is decided per property: the
2181
+ // walk starts at the root's shape and follows only the `contains` properties the reached
2182
+ // shapes declare (own and inherited), so a plain property on a predicate that another shape
2183
+ // declares `contains` is never followed. A registered sub-shape that adds `contains`
2184
+ // properties contributes hops guarded by its concrete class (`?n a <SubClass>`). Every node
2185
+ // the walk reaches loses its own triples and its blank values; `dependent` plays no part.
2186
+ //
2187
+ // Recursion: a shape that owns itself through an unguarded edge (`List.rest`) is followed with
2188
+ // a closure (`rdf:rest*`), since every such edge from one of its nodes is its own. Any other
2189
+ // recursion (a cycle through several shapes, a sub-shape's edge back into the walk) is unrolled
2190
+ // into typed hops, up to `MAX_OWNED_RECURSION` repeated shapes per path; deeper values are left.
2191
+ // ---------------------------------------------------------------------------
2192
+ /**
2193
+ * How many times one walk path may re-enter a shape it already passed through (a cycle through
2194
+ * several shapes, or a sub-shape's edge back into its own kind). Values below are not deleted.
2195
+ */
2196
+ export const MAX_OWNED_RECURSION = 4;
2197
+ /** Above this many branches for one root shape, lowering warns once (see backlog 055). */
2198
+ export const OWNED_BRANCH_WARNING = 64;
2199
+ /** The simple-path predicate through which a property owns its value, or null when it does not. */
2200
+ function ownedPredicate(ps) {
2201
+ var _a, _b;
2202
+ if (!ps.contains || !((_a = ps.valueShape) === null || _a === void 0 ? void 0 : _a.id))
2203
+ return null;
2204
+ return (_b = getSimplePathId(ps.path)) !== null && _b !== void 0 ? _b : null;
2205
+ }
2206
+ let classHoldersCache = new Map();
2207
+ let classHoldersVersion = -1;
2208
+ /** Every registered shape id per `targetClass` (memoized per registry version). */
2209
+ function classHolders(classId) {
2210
+ var _a, _b, _c;
2211
+ const version = getRegistryVersion();
2212
+ if (version !== classHoldersVersion) {
2213
+ classHoldersCache = new Map();
2214
+ for (const nodeShape of getAllNodeShapes().values()) {
2215
+ const cls = (_a = nodeShape === null || nodeShape === void 0 ? void 0 : nodeShape.targetClass) === null || _a === void 0 ? void 0 : _a.id;
2216
+ if (!cls)
2217
+ continue;
2218
+ classHoldersCache.set(cls, [...((_b = classHoldersCache.get(cls)) !== null && _b !== void 0 ? _b : []), nodeShape.id]);
2219
+ }
2220
+ classHoldersVersion = version;
2221
+ }
2222
+ return (_c = classHoldersCache.get(classId)) !== null && _c !== void 0 ? _c : [];
2223
+ }
2390
2224
  /**
2391
- * Build DELETE patterns + WHERE OPTIONAL blocks that cascade-delete the owned subtree
2392
- * reachable from `rootTerm` via `contains` edges. `varPrefix` keeps generated variables
2393
- * unique across multiple roots in one query. Returns empty when nothing is owned.
2225
+ * Why the added `contains` property `ps` of the sub-shape `sub` cannot be followed from nodes of
2226
+ * the sub-shape's class, or null when it can. The guard `?n a <C>` also matches the nodes of
2227
+ * every other shape targeting `C`, so it is refused when one of them is a super-shape of `sub`
2228
+ * (its own nodes would match) or declares the same predicate as a plain property (following it
2229
+ * would delete that shape's references). A holder that does not declare the predicate, or owns
2230
+ * it too, does not block it.
2231
+ */
2232
+ function blockedGuard(sub, ps) {
2233
+ var _a;
2234
+ const cls = (_a = sub.targetClass) === null || _a === void 0 ? void 0 : _a.id;
2235
+ if (!cls)
2236
+ return 'has no targetClass';
2237
+ const predicate = getSimplePathId(ps.path);
2238
+ for (const id of classHolders(cls)) {
2239
+ if (id === sub.id || isSubShapeOf(id, sub.id))
2240
+ continue;
2241
+ if (isSubShapeOf(sub.id, id))
2242
+ return `shares its targetClass with its super-shape '${id}'`;
2243
+ const holder = shapeById(id);
2244
+ const plain = holder && getPropertyShapes(holder, true).some((q) => !q.contains && getSimplePathId(q.path) === predicate);
2245
+ if (plain)
2246
+ return `shares its targetClass with '${id}', which holds <${predicate}> as a plain property`;
2247
+ }
2248
+ return null;
2249
+ }
2250
+ const warnedUnguardable = new Set();
2251
+ /**
2252
+ * The owned edges of a shape: its own and inherited `contains` properties, then, per registered
2253
+ * sub-shape, the `contains` properties it adds, guarded by its `targetClass` (a node carries only
2254
+ * its concrete class's `rdf:type`, so each sub-shape lists its full extra set). An added property
2255
+ * whose guard cannot tell the sub-shape's nodes apart (`blockedGuard`) is not followed and is
2256
+ * warned once, unless the sub-shape is a profile or data-only copy of its direct super-shape (same
2257
+ * class or none, the property inherited from that super-shape), which is skipped silently: that
2258
+ * super-shape's entry covers it.
2259
+ */
2260
+ function ownedEdges(shapeId) {
2261
+ var _a, _b, _c;
2262
+ const nodeShape = shapeById(shapeId);
2263
+ if (!nodeShape)
2264
+ return [];
2265
+ const edges = new Map();
2266
+ const add = (ps, guard) => {
2267
+ const predicate = ownedPredicate(ps);
2268
+ if (!predicate)
2269
+ return;
2270
+ const target = ps.valueShape.id;
2271
+ edges.set(`${guard !== null && guard !== void 0 ? guard : ''} ${predicate} ${target}`, { guard, predicate, target });
2272
+ };
2273
+ const own = getPropertyShapes(nodeShape, true);
2274
+ own.forEach((ps) => add(ps));
2275
+ const ownIds = new Set(own.map((ps) => ps.id));
2276
+ for (const sub of getSubShapes(shapeId)) {
2277
+ const extra = getPropertyShapes(sub, true).filter((ps) => !ownIds.has(ps.id) && ownedPredicate(ps));
2278
+ if (extra.length === 0)
2279
+ continue;
2280
+ const parent = ((_a = sub.extends) === null || _a === void 0 ? void 0 : _a.id) ? shapeById(sub.extends.id) : undefined;
2281
+ const inherited = new Set(parent ? getPropertyShapes(parent, true).map((ps) => ps.id) : []);
2282
+ for (const ps of extra) {
2283
+ const reason = blockedGuard(sub, ps);
2284
+ if (!reason) {
2285
+ add(ps, sub.targetClass.id);
2286
+ continue;
2287
+ }
2288
+ // A profile or data-only copy of its direct super-shape (same class, or none) is that
2289
+ // super-shape's node by type: the super-shape's own entry covers the property, unless
2290
+ // that super-shape is this walk's shape itself (no other entry covers it, so warn).
2291
+ const sameKind = !((_b = sub.targetClass) === null || _b === void 0 ? void 0 : _b.id) || sub.targetClass.id === ((_c = parent === null || parent === void 0 ? void 0 : parent.targetClass) === null || _c === void 0 ? void 0 : _c.id);
2292
+ if (sameKind && inherited.has(ps.id) && (parent === null || parent === void 0 ? void 0 : parent.id) !== shapeId)
2293
+ continue;
2294
+ const key = `${sub.id} ${ps.id}`;
2295
+ if (warnedUnguardable.has(key))
2296
+ continue;
2297
+ warnedUnguardable.add(key);
2298
+ console.warn(`[linked] Shape '${sub.id}' adds the contains property "${ps.label}" to '${shapeId}', but ` +
2299
+ `${reason}, so its nodes cannot be told apart by rdf:type: deleting through ` +
2300
+ `'${shapeId}' does not follow it. Give the shape a targetClass of its own.`);
2301
+ }
2302
+ }
2303
+ return [...edges.values()];
2304
+ }
2305
+ /** The classes a node of `shapeId` may carry: its targetClass and every registered sub-shape's. */
2306
+ function shapeClasses(shapeId) {
2307
+ var _a;
2308
+ const nodeShape = shapeById(shapeId);
2309
+ const classes = new Set();
2310
+ for (const shape of nodeShape ? [nodeShape, ...getSubShapes(shapeId)] : []) {
2311
+ if ((_a = shape.targetClass) === null || _a === void 0 ? void 0 : _a.id)
2312
+ classes.add(shape.targetClass.id);
2313
+ }
2314
+ return [...classes];
2315
+ }
2316
+ let ownedWalkCache = new Map();
2317
+ let ownedWalkCacheVersion = -1;
2318
+ const warnedBranches = new Set();
2319
+ /**
2320
+ * The owned subtree of a root shape, as hops (memoized per registry version; read-only).
2394
2321
  *
2395
- * `anchor`, when given, leads every block: the triple that binds a variable root, so
2396
- * each block stays correct on its own (as a UNION branch) when no such root exists.
2397
- * `onlyTypes`, when given, restricts the blocks to those dependent types (a subject delete
2398
- * passes the types its shape can reach); by default every registered dependent type gets one.
2322
+ * One target per path from the root. A self-loop (`S` owns `S` through an unguarded edge) is a
2323
+ * closure, exact because every such edge from an `S` node is `S`'s own. Every other edge is an
2324
+ * explicit hop. A hop is typed when it is a sub-shape's (its class) or when it stays inside a
2325
+ * cycle of several shapes (the declaring shape's classes), and a path stops after
2326
+ * `MAX_OWNED_RECURSION` hops into a shape already on it.
2399
2327
  */
2400
- export function buildOwnedCascade(rootTerm, varPrefix, anchor, keep, onlyTypes) {
2401
- const containment = collectContainment();
2402
- const { containsPreds } = containment;
2403
- const dependentTypes = onlyTypes !== null && onlyTypes !== void 0 ? onlyTypes : containment.dependentTypes;
2404
- if (containsPreds.length === 0 || dependentTypes.length === 0) {
2405
- return { deletePatterns: [], whereOptionals: [] };
2406
- }
2407
- // (c1|c2|…)* — zero or more contains edges, so the root itself is matched when it is
2408
- // dependent-typed. That is what cleans up the value of a `contains` property that has
2409
- // no value shape (a polymorphic slot like `sh:path`): it is deleted only when it is a
2410
- // dependent node, never when it is a shared IRI. On a subject delete `*` also matches
2411
- // the subject, which is being deleted anyway.
2412
- const pathExpr = containsPreds.length === 1
2413
- ? { zeroOrMore: { id: containsPreds[0] } }
2414
- : { zeroOrMore: { alt: containsPreds.map((id) => ({ id })) } };
2415
- const pathTerm = {
2416
- kind: 'path',
2417
- value: pathExprToSparql(pathExpr),
2418
- uris: collectPathUris(pathExpr),
2328
+ function ownedWalk(rootShapeId) {
2329
+ const version = getRegistryVersion();
2330
+ if (version !== ownedWalkCacheVersion) {
2331
+ ownedWalkCache = new Map();
2332
+ ownedWalkCacheVersion = version;
2333
+ }
2334
+ const cached = ownedWalkCache.get(rootShapeId);
2335
+ if (cached)
2336
+ return cached;
2337
+ const edgesOf = new Map();
2338
+ const pending = [rootShapeId];
2339
+ while (pending.length > 0) {
2340
+ const id = pending.pop();
2341
+ if (edgesOf.has(id))
2342
+ continue;
2343
+ const edges = ownedEdges(id);
2344
+ edgesOf.set(id, edges);
2345
+ for (const edge of edges)
2346
+ if (!edgesOf.has(edge.target))
2347
+ pending.push(edge.target);
2348
+ }
2349
+ const isSelfLoop = (from, e) => !e.guard && e.target === from;
2350
+ const closureOf = (id) => [
2351
+ ...new Set(edgesOf.get(id).filter((e) => isSelfLoop(id, e)).map((e) => e.predicate)),
2352
+ ];
2353
+ // Tarjan's strongly connected components, over the edges that are not closures.
2354
+ const index = new Map();
2355
+ const low = new Map();
2356
+ const onStack = new Set();
2357
+ const stack = [];
2358
+ const groupOf = new Map();
2359
+ const groupSize = [];
2360
+ const strong = (v) => {
2361
+ index.set(v, index.size);
2362
+ low.set(v, index.get(v));
2363
+ stack.push(v);
2364
+ onStack.add(v);
2365
+ for (const e of edgesOf.get(v)) {
2366
+ if (isSelfLoop(v, e))
2367
+ continue;
2368
+ const w = e.target;
2369
+ if (!index.has(w)) {
2370
+ strong(w);
2371
+ low.set(v, Math.min(low.get(v), low.get(w)));
2372
+ }
2373
+ else if (onStack.has(w)) {
2374
+ low.set(v, Math.min(low.get(v), index.get(w)));
2375
+ }
2376
+ }
2377
+ if (low.get(v) !== index.get(v))
2378
+ return;
2379
+ let w;
2380
+ let size = 0;
2381
+ do {
2382
+ w = stack.pop();
2383
+ onStack.delete(w);
2384
+ groupOf.set(w, groupSize.length);
2385
+ size++;
2386
+ } while (w !== v);
2387
+ groupSize.push(size);
2388
+ };
2389
+ strong(rootShapeId);
2390
+ // A non-closure edge back to its own shape (a sub-shape's guarded edge) counts as a cycle too.
2391
+ const inCycle = (from, e) => e.target === from || (groupOf.get(from) === groupOf.get(e.target) && groupSize[groupOf.get(from)] > 1);
2392
+ const targets = [];
2393
+ const visit = (shape, hops, path, repeats) => {
2394
+ for (const edge of edgesOf.get(shape)) {
2395
+ if (isSelfLoop(shape, edge))
2396
+ continue;
2397
+ const repeat = path.includes(edge.target) ? 1 : 0;
2398
+ if (repeats + repeat > MAX_OWNED_RECURSION)
2399
+ continue;
2400
+ const typeClasses = edge.guard
2401
+ ? [edge.guard]
2402
+ : inCycle(shape, edge)
2403
+ ? shapeClasses(shape)
2404
+ : undefined;
2405
+ const hop = Object.assign(Object.assign({}, ((typeClasses === null || typeClasses === void 0 ? void 0 : typeClasses.length) ? { typeClasses } : {})), { predicate: edge.predicate, closure: closureOf(edge.target) });
2406
+ const next = [...hops, hop];
2407
+ targets.push({ hops: next, blankProps: blankCapableProperties(edge.target) });
2408
+ visit(edge.target, next, [...path, edge.target], repeats + repeat);
2409
+ }
2410
+ };
2411
+ visit(rootShapeId, [], [rootShapeId], 0);
2412
+ if (targets.length > OWNED_BRANCH_WARNING && !warnedBranches.has(rootShapeId)) {
2413
+ warnedBranches.add(rootShapeId);
2414
+ console.warn(`[linked] Deleting a '${rootShapeId}' walks ${targets.length} owned paths (more than ` +
2415
+ `${OWNED_BRANCH_WARNING}); each is one UNION branch of the delete.`);
2416
+ }
2417
+ const walk = {
2418
+ rootClosure: closureOf(rootShapeId),
2419
+ rootBlankProps: blankCapableProperties(rootShapeId),
2420
+ targets,
2421
+ };
2422
+ ownedWalkCache.set(rootShapeId, walk);
2423
+ return walk;
2424
+ }
2425
+ /** `FILTER(!sameTerm(?n, rdf:nil))`: the walk never deletes `rdf:nil`, which every list ends in. */
2426
+ function notNil(term) {
2427
+ if (term.kind !== 'variable')
2428
+ return null;
2429
+ return {
2430
+ kind: 'not_expr',
2431
+ inner: {
2432
+ kind: 'function_expr',
2433
+ name: 'sameTerm',
2434
+ args: [{ kind: 'variable_expr', name: term.name }, { kind: 'iri_expr', value: RDF_NIL }],
2435
+ },
2436
+ };
2437
+ }
2438
+ /** `inner` filtered by every expression in `filters` (unchanged when there are none). */
2439
+ function filtered(inner, filters) {
2440
+ if (filters.length === 0)
2441
+ return inner;
2442
+ return {
2443
+ type: 'filter',
2444
+ expression: filters.length === 1 ? filters[0] : { kind: 'logical_expr', op: 'and', exprs: filters },
2445
+ inner,
2419
2446
  };
2447
+ }
2448
+ /**
2449
+ * DELETE patterns and UNION branches that delete the owned subtree below `rootTerm`, a node of
2450
+ * `rootShapeId` (see `ownedWalk`). The root itself is the caller's: the subject wildcard of a
2451
+ * delete, or `buildOwnedSelfDelete` for a replaced value. Each branch deletes one node per
2452
+ * solution, its own triples and its blank values. Schematically (variable names simplified):
2453
+ *
2454
+ * ```sparql
2455
+ * …anchor… <root> (r…)* ?w . # the root shape's self-loop, if any
2456
+ * ?w a <SubClass> . # a typed hop only
2457
+ * ?w <c1> ?x0e . ?x0e (s…)* ?x0 . # closure only for a self-owning target shape
2458
+ * ?x0 ?x0p ?x0o . FILTER(!sameTerm(?x0, rdf:nil))
2459
+ * ```
2460
+ *
2461
+ * `anchor` leads every branch: the owning edge that binds a variable root (`<s> <p> ?old`), or a
2462
+ * bulk delete's type triple, so a branch evaluated on its own never runs with an unbound root.
2463
+ * `keep` filters every branch on the root (`FILTER(?old NOT IN (…))`).
2464
+ */
2465
+ function ownedSubtreeBlocks(rootShapeId, rootTerm, anchor, varPrefix, keep) {
2466
+ const walk = ownedWalk(rootShapeId);
2420
2467
  const deletePatterns = [];
2421
- const whereOptionals = [];
2422
- dependentTypes.forEach((typeId, i) => {
2423
- const owned = varTerm(`${varPrefix}own${i}`);
2424
- const p = varTerm(`${varPrefix}op${i}`);
2425
- const o = varTerm(`${varPrefix}ov${i}`);
2426
- deletePatterns.push(tripleOf(owned, p, o));
2427
- whereOptionals.push(keepFilter({
2428
- type: 'bgp',
2429
- triples: [
2430
- ...(anchor ? [anchor] : []),
2431
- tripleOf(rootTerm, pathTerm, owned),
2432
- tripleOf(owned, iriTerm(RDF_TYPE), iriTerm(typeId)),
2433
- tripleOf(owned, p, o),
2434
- ],
2435
- }, rootTerm, keep));
2436
- });
2437
- // The blank values of every dependent node deleted above, through the blank-capable
2438
- // properties of its type's shapes (a property shape's `sh:path`). Blank nodes carry no type,
2439
- // so the typed blocks cannot see them; see `buildBlankCascade`.
2440
- dependentTypes.forEach((typeId, i) => {
2441
- const props = containment.dependentBlankProps.get(typeId);
2442
- if (!props)
2468
+ const whereBlocks = [];
2469
+ const emit = (reach, filters, node, blankProps, prefix) => {
2470
+ const nil = notNil(node);
2471
+ const all = nil ? [...filters, nil] : filters;
2472
+ const own = tripleOf(node, varTerm(`${prefix}p`), varTerm(`${prefix}o`));
2473
+ deletePatterns.push(own);
2474
+ whereBlocks.push(keepFilter(filtered({ type: 'bgp', triples: [...anchor, ...reach, own] }, all), rootTerm, keep));
2475
+ if (blankProps.length === 0)
2443
2476
  return;
2444
- const owner = varTerm(`${varPrefix}bo${i}`);
2445
- const cascade = buildBlankCascade([
2446
- ...(anchor ? [anchor] : []),
2447
- tripleOf(rootTerm, pathTerm, owner),
2448
- tripleOf(owner, iriTerm(RDF_TYPE), iriTerm(typeId)),
2449
- ], owner, simplePredicates(props), blankWalkPredicates(props), `${varPrefix}b${i}_`);
2450
- if (!cascade)
2477
+ const blank = buildBlankCascade([...anchor, ...reach], node, simplePredicates(blankProps), blankWalkPredicates(blankProps), `${prefix}b_`);
2478
+ if (!blank)
2451
2479
  return;
2452
- deletePatterns.push(...cascade.deletePatterns);
2453
- whereOptionals.push(keepFilter(cascade.whereBlock, rootTerm, keep));
2480
+ deletePatterns.push(...blank.deletePatterns);
2481
+ whereBlocks.push(keepFilter(filtered(blank.whereBlock, all), rootTerm, keep));
2482
+ };
2483
+ let base = rootTerm;
2484
+ const baseReach = [];
2485
+ if (walk.rootClosure.length > 0) {
2486
+ // The other nodes of the root's own kind (the tail cells of a list).
2487
+ const others = varTerm(`${varPrefix}r`);
2488
+ emit([tripleOf(rootTerm, predicatePathTerm(walk.rootClosure, 'oneOrMore'), others)], [], others, walk.rootBlankProps, `${varPrefix}r`);
2489
+ if (walk.targets.length > 0) {
2490
+ base = varTerm(`${varPrefix}w`);
2491
+ baseReach.push(tripleOf(rootTerm, predicatePathTerm(walk.rootClosure, 'zeroOrMore'), base));
2492
+ }
2493
+ }
2494
+ walk.targets.forEach((target, t) => {
2495
+ const reach = [...baseReach];
2496
+ const filters = [];
2497
+ let at = base;
2498
+ target.hops.forEach((hop, h) => {
2499
+ var _a;
2500
+ const name = `${varPrefix}${t}_${h}`;
2501
+ if (((_a = hop.typeClasses) === null || _a === void 0 ? void 0 : _a.length) === 1) {
2502
+ reach.push(tripleOf(at, iriTerm(RDF_TYPE), iriTerm(hop.typeClasses[0])));
2503
+ }
2504
+ else if (hop.typeClasses) {
2505
+ reach.push(tripleOf(at, iriTerm(RDF_TYPE), varTerm(`${name}t`)));
2506
+ filters.push({
2507
+ kind: 'in_expr',
2508
+ negated: false,
2509
+ value: { kind: 'variable_expr', name: `${name}t` },
2510
+ list: hop.typeClasses.map((value) => ({ kind: 'iri_expr', value })),
2511
+ });
2512
+ }
2513
+ const end = varTerm(hop.closure.length > 0 ? `${name}e` : name);
2514
+ reach.push(tripleOf(at, iriTerm(hop.predicate), end));
2515
+ if (hop.closure.length > 0) {
2516
+ reach.push(tripleOf(end, predicatePathTerm(hop.closure, 'zeroOrMore'), varTerm(name)));
2517
+ }
2518
+ at = varTerm(name);
2519
+ });
2520
+ emit(reach, filters, at, target.blankProps, `${varPrefix}${t}_`);
2454
2521
  });
2455
- return { deletePatterns, whereOptionals };
2522
+ return { deletePatterns, whereBlocks };
2456
2523
  }
2457
2524
  /**
2458
2525
  * Wrap an owned-cleanup block in `FILTER(?old NOT IN (<k1>, …))` so a value the mutation
@@ -2480,13 +2547,8 @@ function keepFilter(block, oldTerm, keep) {
2480
2547
  * Build DELETE + WHERE OPTIONAL that removes the *old node's own* one-hop triples when a
2481
2548
  * `contains` edge is replaced, unset, or set-removed. A `contains` edge asserts exclusive
2482
2549
  * ownership, so overwriting it must delete the previously owned child fully — not merely
2483
- * unlink it — or the child orphans (backlog 032). This is `contains`-driven and deliberately
2484
- * does NOT require the child shape to be `dependent`: ownership via the edge is sufficient.
2485
- * Owned descendants *below* the old node are cascaded separately by `buildOwnedCascade`.
2486
- *
2487
- * Callers use it only for a property that declares a value shape. A shape-less `contains`
2488
- * slot (like `sh:path`) may hold a shared IRI, so its old value is left to
2489
- * `buildOwnedCascade`, which deletes it only when it is dependent-typed.
2550
+ * unlink it — or the child orphans (backlog 032). Ownership via the edge is sufficient.
2551
+ * Owned descendants *below* the old node are deleted separately (`ownedSubtreeBlocks`).
2490
2552
  *
2491
2553
  * The owning edge `<subject> <prop> ?old` is re-asserted INSIDE the WHERE group so `?old`
2492
2554
  * is bound there: when no old value exists the group matches nothing and the DELETE is a
@@ -2502,13 +2564,11 @@ export function buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, varPref
2502
2564
  const p = varTerm(`${varPrefix}sp`);
2503
2565
  const o = varTerm(`${varPrefix}so`);
2504
2566
  const wildcard = tripleOf(oldTerm, p, o);
2567
+ const nil = notNil(oldTerm);
2505
2568
  return {
2506
2569
  deletePatterns: [wildcard],
2507
2570
  whereOptionals: [
2508
- keepFilter({
2509
- type: 'bgp',
2510
- triples: [tripleOf(subjectTerm, propertyTerm, oldTerm), wildcard],
2511
- }, oldTerm, keep),
2571
+ keepFilter(filtered({ type: 'bgp', triples: [tripleOf(subjectTerm, propertyTerm, oldTerm), wildcard] }, nil ? [nil] : []), oldTerm, keep),
2512
2572
  ],
2513
2573
  };
2514
2574
  }
@@ -2516,19 +2576,18 @@ export function buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, varPref
2516
2576
  * Converts an IRDeleteMutation to a SparqlDeleteInsertPlan (DELETE + WHERE).
2517
2577
  *
2518
2578
  * The WHERE is the type guard of every id, joined with ONE UNION of independent
2519
- * blocks: per id its own triples, its incoming references and, for a shape that
2520
- * owns something, one block per immediately-owned child (`<s> <p> ?dN . ?dN ?dN_p ?dN_o`,
2521
- * for each of the shape's own and inherited `contains` properties with a value shape and
2522
- * a simple path) and one block per dependent type its owned subtree can reach
2523
- * (`reachableDependentTypes`; none for a shape that owns only non-dependent values). The
2579
+ * blocks: per id its own triples, its incoming references, its blank values and one branch
2580
+ * per owned path, plus that node's blank values (`ownedSubtreeBlocks`: the shape's own and
2581
+ * inherited `contains` properties, their value shapes' in turn, and the extra ones of
2582
+ * registered sub-shapes behind a type guard; none for a shape that owns nothing). The
2524
2583
  * bulk deletes (`deleteAllToAlgebra`, `deleteWhereToAlgebra`) add the same owned blocks,
2525
2584
  * rooted at the subject variable (see `bulkDeleteUnion`).
2526
2585
  *
2527
2586
  * The blocks must not be chained as OPTIONALs. A chain of OPTIONALs is a join, so
2528
2587
  * the solutions multiply: own triples x incoming references x owned triples of each
2529
- * dependent type. Deleting one shape description with the shape sync's cascade
2588
+ * owned path. Deleting one shape description with the shape sync's cascade
2530
2589
  * produced ~27,000 solutions for ~130 triples and took over 30 seconds on Fuseki,
2531
- * which re-evaluates the cascade property path for every left-hand row. A UNION
2590
+ * which re-evaluated the former registry-wide cascade path for every left-hand row. A UNION
2532
2591
  * yields each block's matches once, so the solutions add up instead, and it deletes
2533
2592
  * exactly the same triples: a DELETE template triple whose variables a solution
2534
2593
  * leaves unbound is skipped, as it was for an OPTIONAL that did not match.
@@ -2540,9 +2599,6 @@ export function deleteToAlgebra(query, _options) {
2540
2599
  const deletePatterns = [];
2541
2600
  const guards = [];
2542
2601
  const blocks = [];
2543
- const hasContains = shapeHasContainsProperty(query.shape);
2544
- const ownedChildPredicates = hasContains ? ownedValueShapePredicates(query.shape) : [];
2545
- const cascadeTypes = hasContains ? reachableDependentTypes(query.shape) : [];
2546
2602
  for (let i = 0; i < query.ids.length; i++) {
2547
2603
  const subjectTerm = iriTerm(query.ids[i].id);
2548
2604
  const idx = query.ids.length > 1 ? `_${i}` : '';
@@ -2555,23 +2611,6 @@ export function deleteToAlgebra(query, _options) {
2555
2611
  // incoming references (the entity may have none) are separate UNION blocks.
2556
2612
  guards.push(typeGuard);
2557
2613
  blocks.push({ type: 'bgp', triples: [subjWild] }, { type: 'bgp', triples: [objWild] });
2558
- // The immediately-owned children: for every `contains` property with a value shape,
2559
- // the child's own one-hop triples, as a replace or a set-remove deletes them. No type
2560
- // guard and no reference check: the `contains` edge alone asserts ownership.
2561
- ownedChildPredicates.forEach((predicate, n) => {
2562
- const child = varTerm(`d${n}${idx}`);
2563
- const childOwn = tripleOf(child, varTerm(`d${n}${idx}_p`), varTerm(`d${n}${idx}_o`));
2564
- deletePatterns.push(childOwn);
2565
- blocks.push({
2566
- type: 'bgp',
2567
- triples: [tripleOf(subjectTerm, iriTerm(predicate), child), childOwn],
2568
- });
2569
- // …and the child's own blank values.
2570
- for (const blank of ownedChildBlankCascade(query.shape, predicate, child, [tripleOf(subjectTerm, iriTerm(predicate), child)], `d${n}${idx}_b`)) {
2571
- deletePatterns.push(...blank.deletePatterns);
2572
- blocks.push(blank.whereBlock);
2573
- }
2574
- });
2575
2614
  // The subject's own blank values (through its blank-capable properties) and the blank
2576
2615
  // nodes below them.
2577
2616
  const blank = subjectBlankCascade(query.shape, subjectTerm, [], `b${idx}_`);
@@ -2579,13 +2618,10 @@ export function deleteToAlgebra(query, _options) {
2579
2618
  deletePatterns.push(...blank.deletePatterns);
2580
2619
  blocks.push(blank.whereBlock);
2581
2620
  }
2582
- // Cascade-delete the owned subtree — only for shapes that actually own something
2583
- // (have a `contains` property); a plain entity delete is left untouched.
2584
- if (hasContains && cascadeTypes.length > 0) {
2585
- const cascade = buildOwnedCascade(subjectTerm, `c${idx}_`, undefined, undefined, cascadeTypes);
2586
- deletePatterns.push(...cascade.deletePatterns);
2587
- blocks.push(...cascade.whereOptionals);
2588
- }
2621
+ // The owned subtree: every node the subject's `contains` properties reach (`ownedWalk`).
2622
+ const owned = ownedSubtreeBlocks(query.shape, subjectTerm, [], `c${idx}_`);
2623
+ deletePatterns.push(...owned.deletePatterns);
2624
+ blocks.push(...owned.whereBlocks);
2589
2625
  }
2590
2626
  const whereAlgebra = {
2591
2627
  type: 'join',
@@ -2605,12 +2641,10 @@ export function deleteToAlgebra(query, _options) {
2605
2641
  * clean up (the root wildcard then stays in the required BGP).
2606
2642
  *
2607
2643
  * - The blank-node cascade of the shape's blank-capable properties (`subjectBlankCascade`).
2608
- * - For a shape that owns something (a `contains` property), the same owned cleanup as a
2609
- * delete by id: the immediately-owned children of every `contains` property with a value
2610
- * shape, and the dependent cascade, rooted at the subject variable. Each owned block is led
2611
- * by the subject's type triple. With a linear join it is redundant; it keeps a branch that
2612
- * is evaluated on its own from running `?a0 (c…)* ?own` with both ends unbound, which
2613
- * matches every node in the graph.
2644
+ * - The same owned subtree as a delete by id (`ownedSubtreeBlocks`), rooted at the subject
2645
+ * variable. Each owned block is led by the subject's type triple. With a linear join it is
2646
+ * redundant; it keeps a branch that is evaluated on its own from running `?a0 <c> ?x` with
2647
+ * both ends unbound, which matches every edge in the graph.
2614
2648
  */
2615
2649
  function bulkDeleteUnion(shapeId, subjectVar, rootWildcard, typeTriple, deletePatterns) {
2616
2650
  const blocks = [];
@@ -2619,28 +2653,9 @@ function bulkDeleteUnion(shapeId, subjectVar, rootWildcard, typeTriple, deletePa
2619
2653
  deletePatterns.push(...blank.deletePatterns);
2620
2654
  blocks.push(blank.whereBlock);
2621
2655
  }
2622
- if (shapeHasContainsProperty(shapeId)) {
2623
- const subject = varTerm(subjectVar);
2624
- ownedValueShapePredicates(shapeId).forEach((predicate, n) => {
2625
- const child = varTerm(`d${n}`);
2626
- const childOwn = tripleOf(child, varTerm(`d${n}_p`), varTerm(`d${n}_o`));
2627
- deletePatterns.push(childOwn);
2628
- blocks.push({
2629
- type: 'bgp',
2630
- triples: [typeTriple, tripleOf(subject, iriTerm(predicate), child), childOwn],
2631
- });
2632
- for (const blank of ownedChildBlankCascade(shapeId, predicate, child, [typeTriple, tripleOf(subject, iriTerm(predicate), child)], `d${n}_b`)) {
2633
- deletePatterns.push(...blank.deletePatterns);
2634
- blocks.push(blank.whereBlock);
2635
- }
2636
- });
2637
- const cascadeTypes = reachableDependentTypes(shapeId);
2638
- if (cascadeTypes.length > 0) {
2639
- const cascade = buildOwnedCascade(subject, 'c_', typeTriple, undefined, cascadeTypes);
2640
- deletePatterns.push(...cascade.deletePatterns);
2641
- blocks.push(...cascade.whereOptionals);
2642
- }
2643
- }
2656
+ const owned = ownedSubtreeBlocks(shapeId, varTerm(subjectVar), [typeTriple], 'c_');
2657
+ deletePatterns.push(...owned.deletePatterns);
2658
+ blocks.push(...owned.whereBlocks);
2644
2659
  if (blocks.length === 0)
2645
2660
  return null;
2646
2661
  return unionOf([{ type: 'bgp', triples: [rootWildcard] }, ...blocks]);
@@ -2650,7 +2665,7 @@ function bulkDeleteUnion(shapeId, subjectVar, rootWildcard, typeTriple, deletePa
2650
2665
  *
2651
2666
  * Generates DELETE { ?a0 ?p ?o . [blank-node and owned-cleanup wildcards] }
2652
2667
  * WHERE { ?a0 a <Shape> . { ?a0 ?p ?o } UNION [one block per blank node,
2653
- * owned child and dependent type] }
2668
+ * owned group] }
2654
2669
  * (see `bulkDeleteUnion`). Unlike a delete by id, incoming references are not deleted.
2655
2670
  */
2656
2671
  export function deleteAllToAlgebra(query, _options) {
@@ -2677,7 +2692,7 @@ export function deleteAllToAlgebra(query, _options) {
2677
2692
  * Converts an IRDeleteWhereMutation to a SparqlDeleteInsertPlan.
2678
2693
  *
2679
2694
  * Like deleteAllToAlgebra but adds filter conditions from the where clause. The cleanup
2680
- * UNION (blank nodes, owned children, dependent cascade) is joined with the filtered match,
2695
+ * UNION (blank nodes, owned subtree) is joined with the filtered match,
2681
2696
  * so only the matched subjects' children are deleted.
2682
2697
  */
2683
2698
  export function deleteWhereToAlgebra(query, _options) {