@origintrail-official/dkg-query 10.0.5 → 10.0.7

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.
@@ -1,5 +1,5 @@
1
- import type { TripleStore, Quad } from '@origintrail-official/dkg-storage';
2
- import type { QueryResult, QueryOptions, QueryEngine } from './query-engine.js';
1
+ import type { TripleStore } from '@origintrail-official/dkg-storage';
2
+ import type { QueryResult, QueryOptions, GraphAwareQueryEngine, ResolvedKnowledgeAsset, ResolvedLegacyKnowledgeAsset } from './query-engine.js';
3
3
  import { type GetView, TrustLevel } from '@origintrail-official/dkg-core';
4
4
  /**
5
5
  * Result of resolving a V10 GET view to concrete graph targets.
@@ -50,7 +50,7 @@ export declare function resolveViewGraphs(view: GetView, contextGraphId: string,
50
50
  * Isolation). All data must arrive via protocol messages (publish, access,
51
51
  * sync) before it can be queried here.
52
52
  */
53
- export declare class DKGQueryEngine implements QueryEngine {
53
+ export declare class DKGQueryEngine implements GraphAwareQueryEngine {
54
54
  private readonly store;
55
55
  private readonly graphManager;
56
56
  private readonly scopedContentGraphAllowListInFlight;
@@ -65,6 +65,8 @@ export declare class DKGQueryEngine implements QueryEngine {
65
65
  * Execute a SPARQL query scoped to a declared V10 state view.
66
66
  */
67
67
  private queryWithView;
68
+ /** Canonical graph rewrite for root + per-KA/per-cgId verifiable-memory reads. */
69
+ private wrapVerifiableMemoryGraphSet;
68
70
  private queryMultipleGraphs;
69
71
  private discoverGraphsByPrefix;
70
72
  /**
@@ -78,6 +80,15 @@ export declare class DKGQueryEngine implements QueryEngine {
78
80
  * private/SWM rows.
79
81
  */
80
82
  private discoverContextGraphPerCgIdDataGraphs;
83
+ /**
84
+ * Resolve only the immutable private graphs referenced by valid current V2
85
+ * metadata in this exact context graph. The broad `/_private/` prefix also
86
+ * contains mutable WM drafts, so prefix-enumerating it would let an
87
+ * `includePrivate` query cross the draft/finalized boundary. Deriving each
88
+ * graph from the canonical UAL + assertion version keeps the allow-list
89
+ * fail-closed and makes GRAPH-variable discovery work for rootless KAs.
90
+ */
91
+ private discoverGraphScopedPrivateGraphs;
81
92
  /**
82
93
  * Resolve a WM assertion's allocated KA number (`dkg:kaId`) off its lifecycle URN
83
94
  * in `_meta`, for single-graph by-name reads under the uniform layout. Returns
@@ -92,12 +103,10 @@ export declare class DKGQueryEngine implements QueryEngine {
92
103
  private discoverRegisteredAssertionGraphs;
93
104
  private discoverKnownChildContextGraphUris;
94
105
  private execAndNormalize;
95
- resolveKA(ual: string): Promise<{
96
- rootEntity: string;
97
- rootEntities: string[];
98
- contextGraphId: string;
99
- quads: Quad[];
100
- }>;
106
+ resolveKA(ual: string): Promise<ResolvedLegacyKnowledgeAsset>;
107
+ resolveKnowledgeAsset(ual: string): Promise<ResolvedKnowledgeAsset>;
108
+ private resolveGraphScopedKA;
109
+ private resolveLegacyRootScopedKA;
101
110
  /**
102
111
  * Execute a query across all locally-stored context graphs.
103
112
  */
@@ -1 +1 @@
1
- {"version":3,"file":"dkg-query-engine.d.ts","sourceRoot":"","sources":["../src/dkg-query-engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,IAAI,EAAmC,MAAM,mCAAmC,CAAC;AAE5G,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChF,OAAO,EAOL,KAAK,OAAO,EAEZ,UAAU,EAEX,MAAM,gCAAgC,CAAC;AAQxC;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB;;;;OAIG;IACH,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,OAAO,EACb,cAAc,EAAE,MAAM,EACtB,IAAI,CAAC,EAAE;IACL,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GACA,cAAc,CA0IhB;AAED;;;;;GAKG;AACH,qBAAa,cAAe,YAAW,WAAW;IAChD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAG5C,OAAO,CAAC,QAAQ,CAAC,mCAAmC,CAAwC;gBAEhF,KAAK,EAAE,WAAW;IAKxB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IAyLzE;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAe5B;;OAEG;YACW,aAAa;YA0Jb,mBAAmB;YAuEnB,sBAAsB;IAOpC;;;;;;;;;OASG;YACW,qCAAqC;IASnD;;;;;OAKG;YACW,4BAA4B;YAsB5B,mCAAmC;YAqBnC,kCAAkC;YAsBlC,mCAAmC;YA8BnC,+BAA+B;YAsB/B,iCAAiC;YAqBjC,kCAAkC;YA2BlC,gBAAgB;IAmBxB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QACpC,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,EAAE,CAAC;QACvB,cAAc,EAAE,MAAM,CAAC;QACvB,KAAK,EAAE,IAAI,EAAE,CAAC;KACf,CAAC;IAgFF;;OAEG;IACG,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;CAYlE;AA05BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAoCtE"}
1
+ {"version":3,"file":"dkg-query-engine.d.ts","sourceRoot":"","sources":["../src/dkg-query-engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAyC,MAAM,mCAAmC,CAAC;AAO5G,OAAO,KAAK,EACV,WAAW,EACX,YAAY,EACZ,qBAAqB,EAErB,sBAAsB,EACtB,4BAA4B,EAC7B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAQL,KAAK,OAAO,EAEZ,UAAU,EAOX,MAAM,gCAAgC,CAAC;AAQxC;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB;;;;OAIG;IACH,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,OAAO,EACb,cAAc,EAAE,MAAM,EACtB,IAAI,CAAC,EAAE;IACL,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GACA,cAAc,CAgKhB;AAED;;;;;GAKG;AACH,qBAAa,cAAe,YAAW,qBAAqB;IAC1D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAG5C,OAAO,CAAC,QAAQ,CAAC,mCAAmC,CAAwC;gBAEhF,KAAK,EAAE,WAAW;IAKxB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IA+LzE;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAe5B;;OAEG;YACW,aAAa;IA+J3B,kFAAkF;IAClF,OAAO,CAAC,4BAA4B;YAQtB,mBAAmB;YAsFnB,sBAAsB;IAOpC;;;;;;;;;OASG;YACW,qCAAqC;IASnD;;;;;;;OAOG;YACW,gCAAgC;IAkE9C;;;;;OAKG;YACW,4BAA4B;YAsB5B,mCAAmC;YAqBnC,kCAAkC;YAsBlC,mCAAmC;YA8BnC,+BAA+B;YAsB/B,iCAAiC;YAqBjC,kCAAkC;YA2BlC,gBAAgB;IAmBxB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,4BAA4B,CAAC;IAU7D,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC;YAe3D,oBAAoB;YAsCpB,yBAAyB;IAkFvC;;OAEG;IACG,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;CAYlE;AAo6BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAoCtE"}
@@ -1,5 +1,5 @@
1
- import { GraphManager } from '@origintrail-official/dkg-storage';
2
- import { contextGraphDataUri, contextGraphSharedMemoryUri, contextGraphVerifiableMemoryUri, contextGraphAssertionUri, contextGraphLayerUri, MemoryLayer, contextGraphSubGraphUri, contextGraphMetaUri, contextGraphSharedMemoryMetaUri, assertionLifecycleUri, contextGraphSubGraphMetaUri, contextGraphPrivateUri, contextGraphSubGraphPrivateUri, assertSafeIri, escapeSparqlLiteral, validateSubGraphName, ASSERTION_NAMED_GRAPH_PREFIX, isAssertionScopedChildGraph, REMOVED_VIEWS, TrustLevel, TRUST_LEVEL_PREDICATE, } from '@origintrail-official/dkg-core';
1
+ import { ExactGraphReadError, GraphManager, readExactGraphPaged, resolveGraphScopedOrLegacyMetadata, } from '@origintrail-official/dkg-storage';
2
+ import { contextGraphDataUri, contextGraphSharedMemoryUri, contextGraphVerifiableMemoryUri, contextGraphAssertionUri, MemoryLayer, contextGraphLayerUriCandidates, contextGraphLayerPrefixCandidates, contextGraphSubGraphUri, contextGraphMetaUri, contextGraphSharedMemoryMetaUri, assertionLifecycleUri, contextGraphSubGraphMetaUri, contextGraphPrivateUri, contextGraphSubGraphPrivateUri, assertSafeIri, escapeSparqlLiteral, validateSubGraphName, ASSERTION_NAMED_GRAPH_PREFIX, isAssertionScopedChildGraph, REMOVED_VIEWS, TrustLevel, TRUST_LEVEL_PREDICATE, GRAPH_KA_CONTENT_SCOPE_VERSION, createGraphKnowledgeAssetScope, knowledgeAssetLayerGraphUri, buildLegacyKnowledgeAssetMetadataQuery, } from '@origintrail-official/dkg-core';
3
3
  import { validateReadOnlySparql, emptyResultForSparql, detectSparqlQueryForm, } from './sparql-guard.js';
4
4
  import { stripLiteralsAndComments } from './sparql-utils.js';
5
5
  export class ScopedQueryViolationError extends Error {
@@ -38,32 +38,41 @@ export function resolveViewGraphs(view, contextGraphId, opts) {
38
38
  // `subGraphName` + `assertionName` read targets the ROOT assertion graph and
39
39
  // misses sub-graph assertions (Codex review on PR #1132).
40
40
  if (opts.assertionName) {
41
+ if (opts.kaNumber !== undefined) {
42
+ return {
43
+ graphs: contextGraphLayerUriCandidates(contextGraphId, MemoryLayer.WorkingMemory, opts.agentAddress, opts.kaNumber, opts.subGraphName),
44
+ graphPrefixes: [],
45
+ };
46
+ }
41
47
  return {
42
- graphs: [opts.kaNumber !== undefined
43
- ? contextGraphLayerUri(contextGraphId, MemoryLayer.WorkingMemory, opts.agentAddress, opts.kaNumber, opts.subGraphName)
44
- : contextGraphAssertionUri(contextGraphId, opts.agentAddress, opts.assertionName, opts.subGraphName)],
48
+ graphs: [contextGraphAssertionUri(contextGraphId, opts.agentAddress, opts.assertionName, opts.subGraphName)],
45
49
  graphPrefixes: [],
46
50
  };
47
51
  }
48
52
  // PR #1107 review (🟡): span the primary address AND every
49
53
  // same-identity alias so a node default agent's legacy peerId-keyed
50
54
  // drafts and rc.17+ wallet-keyed drafts are both visible from one
51
- // unscoped WM read. Dedupe case-insensitively graph URIs embed the
52
- // address verbatim, so the prefix must use the caller's original form.
53
- const seen = new Set([opts.agentAddress.toLowerCase()]);
54
- const addresses = [opts.agentAddress];
55
- for (const alias of opts.agentAddressAliases ?? []) {
56
- if (!alias || seen.has(alias.toLowerCase()))
55
+ // unscoped WM read. New full EVM-address writes use the lowercase core
56
+ // identity, while the caller's original casing is retained as a legacy
57
+ // read prefix for graphs written before canonicalization.
58
+ const graphPrefixes = [];
59
+ const seen = new Set();
60
+ for (const address of [opts.agentAddress, ...(opts.agentAddressAliases ?? [])]) {
61
+ if (!address)
57
62
  continue;
58
- seen.add(alias.toLowerCase());
59
- addresses.push(alias);
63
+ for (const candidate of contextGraphLayerPrefixCandidates(contextGraphId, MemoryLayer.WorkingMemory, address, opts.subGraphName)) {
64
+ if (seen.has(candidate))
65
+ continue;
66
+ seen.add(candidate);
67
+ graphPrefixes.push(candidate);
68
+ }
60
69
  }
61
70
  return {
62
71
  graphs: [],
63
72
  // Combine main's same-identity alias span (#1107 review 🟡) with
64
73
  // #1132's sub-graph scoping (#184/#675): one prefix per alias address,
65
74
  // each carrying the optional sub-graph suffix.
66
- graphPrefixes: addresses.map((addr) => `did:dkg:context-graph:${contextGraphId}${sg}/_working_memory/${addr}/`),
75
+ graphPrefixes,
67
76
  };
68
77
  }
69
78
  case 'shared-working-memory':
@@ -265,6 +274,7 @@ export class DKGQueryEngine {
265
274
  subGraphName
266
275
  ? contextGraphSubGraphPrivateUri(effectiveContextGraphId, subGraphName)
267
276
  : contextGraphPrivateUri(effectiveContextGraphId),
277
+ ...(await this.discoverGraphScopedPrivateGraphs(effectiveContextGraphId, subGraphName)),
268
278
  ]
269
279
  : [];
270
280
  const explicitAllowedGraphs = [...allowedGraphs, ...metaAllowList, ...privateAllowList];
@@ -304,7 +314,8 @@ export class DKGQueryEngine {
304
314
  // Per-KA VM: read-both the published per-KA …/_verifiable_memory/{addr}/{number} + root.
305
315
  const vmGraphsInc = await this.discoverGraphsByPrefix(`${dataGraph}/_verifiable_memory/`);
306
316
  const dataSparql = vmGraphsInc.length > 0
307
- ? (wrapWithGraphUnion(sparql, [dataGraph, ...vmGraphsInc]) ?? wrapWithGraph(sparql, dataGraph))
317
+ ? (this.wrapVerifiableMemoryGraphSet(sparql, [dataGraph, ...vmGraphsInc])
318
+ ?? wrapWithGraph(sparql, dataGraph))
308
319
  : wrapWithGraph(sparql, dataGraph);
309
320
  // Per-KA SWM: union the discovered …/_shared_memory/{addr}/{number} graphs.
310
321
  const swmGraphs = await this.discoverGraphsByPrefix(`${sharedMemoryGraph}/`);
@@ -327,7 +338,8 @@ export class DKGQueryEngine {
327
338
  // Per-KA VM: read-both the published per-KA …/_verifiable_memory/{addr}/{number} + root.
328
339
  const vmGraphs = await this.discoverGraphsByPrefix(`${dataGraph}/_verifiable_memory/`);
329
340
  effectiveSparql = vmGraphs.length > 0
330
- ? (wrapWithGraphUnion(sparql, [dataGraph, ...vmGraphs]) ?? wrapWithGraph(sparql, dataGraph))
341
+ ? (this.wrapVerifiableMemoryGraphSet(sparql, [dataGraph, ...vmGraphs])
342
+ ?? wrapWithGraph(sparql, dataGraph))
331
343
  : wrapWithGraph(sparql, dataGraph);
332
344
  }
333
345
  }
@@ -486,16 +498,46 @@ export class DKGQueryEngine {
486
498
  if (allGraphs.length === 1) {
487
499
  return this.execAndNormalize(wrapWithGraph(effectiveSparql, allGraphs[0]));
488
500
  }
501
+ if (view === 'verifiable-memory') {
502
+ const rewritten = this.wrapVerifiableMemoryGraphSet(effectiveSparql, allGraphs);
503
+ if (rewritten !== null)
504
+ return this.execAndNormalize(rewritten);
505
+ }
489
506
  return this.queryMultipleGraphs(effectiveSparql, allGraphs);
490
507
  }
508
+ /** Canonical graph rewrite for root + per-KA/per-cgId verifiable-memory reads. */
509
+ wrapVerifiableMemoryGraphSet(sparql, graphs) {
510
+ if (graphs.length === 0)
511
+ return sparql;
512
+ if (graphs.length === 1)
513
+ return wrapWithGraph(sparql, graphs[0]);
514
+ return wrapWithDeduplicatedGraphValues(sparql, graphs)
515
+ ?? wrapWithGraphValues(sparql, graphs)
516
+ ?? wrapWithGraphUnion(sparql, graphs);
517
+ }
491
518
  async queryMultipleGraphs(sparql, graphs) {
492
519
  if (graphs.length === 0)
493
520
  return { bindings: [] };
494
521
  if (graphs.length === 1) {
495
522
  return this.execAndNormalize(wrapWithGraph(sparql, graphs[0]));
496
523
  }
497
- // Build a single union query so LIMIT/ORDER BY/DISTINCT/aggregates
498
- // apply over the full dataset rather than per-graph.
524
+ // Prefer a single `VALUES ?g { … } GRAPH ?g { … }` query: it scopes the
525
+ // read to the named-graph set as ONE basic graph pattern iterated over a
526
+ // table, instead of one `{ GRAPH <g> { … } }` UNION branch per graph. A
527
+ // per-graph UNION over the ~1.4k SWM graphs of a large public context
528
+ // graph builds a ~1.4k-branch UnionNode that makes the oxigraph-server
529
+ // planner blow up and drop the socket ("fetch failed", issue #1596); the
530
+ // VALUES form plans in constant depth. LIMIT/ORDER BY/DISTINCT/aggregate
531
+ // semantics are preserved because those modifiers stay in the outer query
532
+ // around the injected block.
533
+ const valuesSparql = wrapWithGraphValues(sparql, graphs);
534
+ if (valuesSparql !== null) {
535
+ return this.execAndNormalize(valuesSparql);
536
+ }
537
+ // Residual shapes `wrapWithGraphValues` declines (an inner top-level
538
+ // UNION, no locatable WHERE block, or a sentinel-variable collision) keep
539
+ // the original union / per-graph fallback so the #789 form-aware
540
+ // cross-graph merge below is preserved unchanged.
499
541
  const unionSparql = wrapWithGraphUnion(sparql, graphs);
500
542
  if (unionSparql !== null) {
501
543
  return this.execAndNormalize(unionSparql);
@@ -577,6 +619,67 @@ export class DKGQueryEngine {
577
619
  return rest.length > 0 && !rest.includes('/');
578
620
  });
579
621
  }
622
+ /**
623
+ * Resolve only the immutable private graphs referenced by valid current V2
624
+ * metadata in this exact context graph. The broad `/_private/` prefix also
625
+ * contains mutable WM drafts, so prefix-enumerating it would let an
626
+ * `includePrivate` query cross the draft/finalized boundary. Deriving each
627
+ * graph from the canonical UAL + assertion version keeps the allow-list
628
+ * fail-closed and makes GRAPH-variable discovery work for rootless KAs.
629
+ */
630
+ async discoverGraphScopedPrivateGraphs(contextGraphId, subGraphName) {
631
+ const metaGraph = contextGraphMetaUri(contextGraphId);
632
+ const subGraphClause = subGraphName
633
+ ? `?ual <http://dkg.io/ontology/subGraphName> "${escapeSparqlLiteral(subGraphName)}" .`
634
+ : `FILTER NOT EXISTS { ?ual <http://dkg.io/ontology/subGraphName> ?_subGraphName . }`;
635
+ const result = await this.store.query(`SELECT DISTINCT ?ual ?scopeVersion ?kaUal ?assertionVersion ?assertionGraph ?privateCount WHERE {
636
+ GRAPH <${assertSafeIri(metaGraph)}> {
637
+ ?ual <http://dkg.io/ontology/contentScopeVersion> ?scopeVersion ;
638
+ <http://dkg.io/ontology/kaUal> ?kaUal ;
639
+ <http://dkg.io/ontology/assertionVersion> ?assertionVersion ;
640
+ <http://dkg.io/ontology/assertionGraph> ?assertionGraph ;
641
+ <http://dkg.io/ontology/privateTripleCount> ?privateCount ;
642
+ <http://dkg.io/ontology/status> ?status .
643
+ FILTER(?status IN ("confirmed", "tentative"))
644
+ ${subGraphClause}
645
+ }
646
+ }`);
647
+ if (result.type !== 'bindings')
648
+ return [];
649
+ const privateRoot = subGraphName
650
+ ? contextGraphSubGraphPrivateUri(contextGraphId, subGraphName)
651
+ : contextGraphPrivateUri(contextGraphId);
652
+ const graphs = new Set();
653
+ for (const row of result.bindings) {
654
+ const ual = row['ual'];
655
+ const metadataUal = row['kaUal'];
656
+ const assertionGraph = row['assertionGraph'];
657
+ const scopeVersion = parseCanonicalIntegerBinding(row['scopeVersion']);
658
+ const assertionVersion = parseCanonicalIntegerBinding(row['assertionVersion']);
659
+ const privateCount = parseCanonicalIntegerBinding(row['privateCount']);
660
+ if (!ual
661
+ || metadataUal !== ual
662
+ || !assertionGraph
663
+ || scopeVersion !== BigInt(GRAPH_KA_CONTENT_SCOPE_VERSION)
664
+ || assertionVersion === undefined
665
+ || assertionVersion < 1n
666
+ || privateCount === undefined
667
+ || privateCount < 1n) {
668
+ continue;
669
+ }
670
+ try {
671
+ const scope = createGraphKnowledgeAssetScope(ual, assertionVersion);
672
+ const expectedPublicGraph = knowledgeAssetLayerGraphUri(contextGraphId, MemoryLayer.VerifiableMemory, scope, subGraphName);
673
+ if (assertionGraph !== expectedPublicGraph)
674
+ continue;
675
+ graphs.add(assertSafeIri(`${privateRoot}/${scope.agentAddress}/${scope.kaNumber}/assertions/${scope.assertionVersion}`));
676
+ }
677
+ catch {
678
+ // Invalid or non-canonical metadata never widens the private allow-list.
679
+ }
680
+ }
681
+ return [...graphs];
682
+ }
580
683
  /**
581
684
  * Resolve a WM assertion's allocated KA number (`dkg:kaId`) off its lifecycle URN
582
685
  * in `_meta`, for single-graph by-name reads under the uniform layout. Returns
@@ -721,28 +824,55 @@ export class DKGQueryEngine {
721
824
  return { bindings: [] };
722
825
  }
723
826
  async resolveKA(ual) {
724
- // Look up KA metadata across all meta graphs, including subGraphName if recorded.
725
- // Design B (OT-RFC-44): one KA can hold MULTIPLE rootEntities, so collect
726
- // every `?ka <rootEntity>` binding (not just bindings[0]) PR #968 salvage.
727
- // Read-both (RFC ka-metadata-trim P3.1): the collapsed shape carries
728
- // `dkg:rootEntity` directly on the UAL subject (no `<ual>/<n>` token rows,
729
- // no `dkg:partOf`); legacy-shape rows synced from older nodes still use
730
- // the token-row + partOf form, so both branches are queried.
731
- const metaResult = await this.store.query(`SELECT ?ka ?rootEntity ?ctxGraph ?sgName WHERE {
732
- GRAPH ?g {
733
- {
734
- ?ka <http://dkg.io/ontology/rootEntity> ?rootEntity .
735
- ?ka <http://dkg.io/ontology/partOf> <${assertSafeIri(ual)}> .
736
- }
737
- UNION
738
- {
739
- <${assertSafeIri(ual)}> <http://dkg.io/ontology/rootEntity> ?rootEntity .
740
- BIND(<${assertSafeIri(ual)}> AS ?ka)
741
- }
742
- <${assertSafeIri(ual)}> <http://dkg.io/ontology/contextGraph> ?ctxGraph .
743
- OPTIONAL { <${assertSafeIri(ual)}> <http://dkg.io/ontology/subGraphName> ?sgName }
827
+ const resolved = await this.resolveKnowledgeAsset(ual);
828
+ if (resolved.contentScopeVersion === GRAPH_KA_CONTENT_SCOPE_VERSION) {
829
+ throw new Error('Graph-scoped Knowledge Assets do not have root entities; use resolveKnowledgeAsset()');
830
+ }
831
+ return resolved;
832
+ }
833
+ async resolveKnowledgeAsset(ual) {
834
+ const safeUal = assertSafeIri(ual);
835
+ const resolved = await resolveGraphScopedOrLegacyMetadata(this.store, safeUal, () => this.resolveLegacyRootScopedKA(safeUal), { source: 'query.resolveKnowledgeAsset.metadata' });
836
+ if (resolved.kind === 'graph') {
837
+ return this.resolveGraphScopedKA(safeUal, resolved.metadata);
838
+ }
839
+ if (resolved.kind === 'legacy')
840
+ return resolved.metadata;
841
+ throw new Error(`KA not found for UAL: ${safeUal}`);
842
+ }
843
+ async resolveGraphScopedKA(ual, metadata) {
844
+ const expectedGraph = metadata.assertionGraph;
845
+ let quads;
846
+ try {
847
+ quads = await readExactGraphPaged(this.store, expectedGraph, {
848
+ expectedQuadCount: metadata.publicTripleCount,
849
+ queryOptions: { source: 'query.resolveKnowledgeAsset' },
850
+ });
851
+ }
852
+ catch (error) {
853
+ if (error instanceof ExactGraphReadError && error.code === 'QUAD_COUNT_MISMATCH') {
854
+ throw new Error(`Graph-scoped KA ${ual} graph integrity mismatch: metadata declares ` +
855
+ `${metadata.publicTripleCount} public triples, found ${error.actual ?? 'an unknown count'}`);
856
+ }
857
+ throw error;
858
+ }
859
+ if (quads.length !== metadata.publicTripleCount) {
860
+ throw new Error(`Graph-scoped KA ${ual} graph integrity mismatch: metadata declares ${metadata.publicTripleCount} public triples, found ${quads.length}`);
744
861
  }
745
- } ORDER BY ?ka`);
862
+ return {
863
+ ual: metadata.scope.ual,
864
+ contentScopeVersion: GRAPH_KA_CONTENT_SCOPE_VERSION,
865
+ assertionVersion: metadata.scope.assertionVersion,
866
+ assertionGraph: expectedGraph,
867
+ rootEntities: [],
868
+ contextGraphId: metadata.contextGraphId,
869
+ quads,
870
+ };
871
+ }
872
+ async resolveLegacyRootScopedKA(ual) {
873
+ // Existing V10 metadata can use either token-row + partOf membership or
874
+ // the collapsed UAL-subject rootEntity form. This path is read-only.
875
+ const metaResult = await this.store.query(buildLegacyKnowledgeAssetMetadataQuery(ual));
746
876
  if (metaResult.type !== 'bindings' || metaResult.bindings.length === 0) {
747
877
  throw new Error(`KA not found for UAL: ${ual}`);
748
878
  }
@@ -755,9 +885,21 @@ export class DKGQueryEngine {
755
885
  throw new Error(`KA not found for UAL: ${ual}`);
756
886
  }
757
887
  const rootEntity = rootEntities[0];
758
- const contextGraphUri = metaResult.bindings[0]['ctxGraph'];
759
- const contextGraphId = contextGraphUri.replace('did:dkg:context-graph:', '');
760
- const sgNameRaw = metaResult.bindings[0]['sgName'];
888
+ const contextGraphUris = [
889
+ ...new Set(metaResult.bindings
890
+ .map((row) => row['ctxGraph'])
891
+ .filter((value) => typeof value === 'string' && value.length > 0)),
892
+ ];
893
+ if (contextGraphUris.length !== 1) {
894
+ throw new Error(`Legacy KA ${ual} has ambiguous contextGraph metadata`);
895
+ }
896
+ const contextGraphPrefix = 'did:dkg:context-graph:';
897
+ const contextGraphUri = contextGraphUris[0];
898
+ if (!contextGraphUri.startsWith(contextGraphPrefix)) {
899
+ throw new Error(`Legacy KA ${ual} has invalid contextGraph metadata`);
900
+ }
901
+ const contextGraphId = contextGraphUri.slice(contextGraphPrefix.length);
902
+ const sgNameRaw = metaResult.bindings[0]?.['sgName'];
761
903
  const subGraphName = sgNameRaw ? sgNameRaw.replace(/^"(.*)".*$/, '$1') : undefined;
762
904
  const dataGraph = subGraphName
763
905
  ? contextGraphSubGraphUri(contextGraphId, subGraphName)
@@ -765,7 +907,6 @@ export class DKGQueryEngine {
765
907
  const rootFilter = rootEntities
766
908
  .map((root) => `(?s = <${assertSafeIri(root)}> || STRSTARTS(STR(?s), "${escapeSparqlLiteral(root)}/.well-known/genid/"))`)
767
909
  .join(' || ');
768
- // Fetch all triples for every KA member root from the correct data graph.
769
910
  const dataResult = await this.store.query(`SELECT ?s ?p ?o WHERE {
770
911
  GRAPH <${assertSafeIri(dataGraph)}> {
771
912
  ?s ?p ?o .
@@ -780,7 +921,14 @@ export class DKGQueryEngine {
780
921
  graph: dataGraph,
781
922
  }))
782
923
  : [];
783
- return { rootEntity, rootEntities, contextGraphId, quads };
924
+ return {
925
+ ual,
926
+ contentScopeVersion: 1,
927
+ rootEntity,
928
+ rootEntities,
929
+ contextGraphId,
930
+ quads,
931
+ };
784
932
  }
785
933
  /**
786
934
  * Execute a query across all locally-stored context graphs.
@@ -877,6 +1025,17 @@ function stripSparqlLiteralValue(value) {
877
1025
  return '';
878
1026
  return value.replace(/^"/, '').replace(/"(?:\^\^<[^>]+>|@[a-zA-Z-]+)?$/, '');
879
1027
  }
1028
+ function parseCanonicalIntegerBinding(value) {
1029
+ const lexical = stripSparqlLiteralValue(value);
1030
+ if (!/^(?:0|[1-9][0-9]*)$/.test(lexical))
1031
+ return undefined;
1032
+ try {
1033
+ return BigInt(lexical);
1034
+ }
1035
+ catch {
1036
+ return undefined;
1037
+ }
1038
+ }
880
1039
  function assertNoCallerDatasetClauses(sparql) {
881
1040
  if (hasCallerDatasetClause(sparql)) {
882
1041
  throw new ScopedQueryViolationError('FROM clauses are not allowed on scoped local queries');
@@ -2172,6 +2331,193 @@ function wrapWithGraphUnion(sparql, graphUris) {
2172
2331
  .join(' UNION ');
2173
2332
  return `${before} ${unionBranches} ${after}`;
2174
2333
  }
2334
+ /** The graph variable `wrapWithGraphValues` injects. Double-underscore + a
2335
+ * view-specific name so a user query is astronomically unlikely to bind it;
2336
+ * a collision is nonetheless detected and declined (never silently clamped). */
2337
+ const VIEW_GRAPH_SENTINEL = '?__dkgViewGraph';
2338
+ const DEDUP_GRAPH_SENTINEL = '?__dkgDedupGraph';
2339
+ const DEDUP_RANK_SENTINEL = '?__dkgDedupRank';
2340
+ const DEDUP_PRIOR_GRAPH_SENTINEL = '?__dkgDedupPriorGraph';
2341
+ const DEDUP_PRIOR_RANK_SENTINEL = '?__dkgDedupPriorRank';
2342
+ function wrapWithProjectedGraphSubselect(sparql, graphUris, helperVariables, buildGraphPattern, acceptsInner = () => true) {
2343
+ if (hasGraphClause(sparql))
2344
+ return sparql;
2345
+ if (graphUris.length === 0)
2346
+ return sparql;
2347
+ const braceStart = findWhereBraceStart(sparql);
2348
+ if (braceStart === -1)
2349
+ return null;
2350
+ const braceEnd = findMatchingCloseBrace(sparql, braceStart);
2351
+ if (braceEnd === -1)
2352
+ return null;
2353
+ const before = sparql.slice(0, braceStart + 1);
2354
+ const inner = sparql.slice(braceStart + 1, braceEnd);
2355
+ const after = sparql.slice(braceEnd);
2356
+ const graphs = [...new Set(graphUris)];
2357
+ if (graphs.length === 1) {
2358
+ return `${before} GRAPH <${assertSafeIri(graphs[0])}> { ${inner} } ${after}`;
2359
+ }
2360
+ if (/\bUNION\b/i.test(inner) || !acceptsInner(inner))
2361
+ return null;
2362
+ const helperNames = new Set(helperVariables.map((variable) => variable.slice(1)));
2363
+ if (collectQueryVariables(sparql).some((variable) => helperNames.has(variable.slice(1)))) {
2364
+ return null;
2365
+ }
2366
+ const innerVars = collectQueryVariables(inner);
2367
+ if (innerVars.length === 0)
2368
+ return null;
2369
+ const graphPattern = buildGraphPattern(inner, graphs);
2370
+ return `${before} { SELECT ${innerVars.join(' ')} WHERE { ${graphPattern} } } ${after}`;
2371
+ }
2372
+ /**
2373
+ * Run one graph pattern across an ordered graph set while suppressing only a
2374
+ * solution mapping already produced by an earlier graph. The comparison uses
2375
+ * every variable bound by the caller's inner pattern, before the caller's
2376
+ * projection runs. Thus an identical mirrored triple is emitted once, while
2377
+ * distinct triples that both project to the same `?s` still produce two rows.
2378
+ *
2379
+ * Helper graph/rank variables are hidden inside a sub-SELECT, preserving
2380
+ * `SELECT *` and caller DISTINCT semantics. Unsupported/colliding query shapes
2381
+ * return null so the existing generic multi-graph fallback remains available.
2382
+ */
2383
+ function wrapWithDeduplicatedGraphValues(sparql, graphUris) {
2384
+ return wrapWithProjectedGraphSubselect(sparql, graphUris, [
2385
+ DEDUP_GRAPH_SENTINEL,
2386
+ DEDUP_RANK_SENTINEL,
2387
+ DEDUP_PRIOR_GRAPH_SENTINEL,
2388
+ DEDUP_PRIOR_RANK_SENTINEL,
2389
+ ], (inner, graphs) => {
2390
+ const rows = graphs
2391
+ .map((graph, rank) => `(<${assertSafeIri(graph)}> ${rank})`)
2392
+ .join(' ');
2393
+ return [
2394
+ `VALUES (${DEDUP_GRAPH_SENTINEL} ${DEDUP_RANK_SENTINEL}) { ${rows} }`,
2395
+ `GRAPH ${DEDUP_GRAPH_SENTINEL} { ${inner} }`,
2396
+ 'FILTER NOT EXISTS {',
2397
+ ` VALUES (${DEDUP_PRIOR_GRAPH_SENTINEL} ${DEDUP_PRIOR_RANK_SENTINEL}) { ${rows} }`,
2398
+ ` FILTER (${DEDUP_PRIOR_RANK_SENTINEL} < ${DEDUP_RANK_SENTINEL})`,
2399
+ ` GRAPH ${DEDUP_PRIOR_GRAPH_SENTINEL} { ${inner} }`,
2400
+ '}',
2401
+ ].join(' ');
2402
+ }, isDedupSafeBasicGraphPattern);
2403
+ }
2404
+ /**
2405
+ * Wrap a query so it runs over a set of named graphs in ONE execution using a
2406
+ * single `VALUES ?g { <g1> … } GRAPH ?g { inner }` block, instead of one
2407
+ * `{ GRAPH <g> { inner } } UNION …` branch per graph.
2408
+ *
2409
+ * Why this exists (issue #1596): the per-graph UNION form (`wrapWithGraphUnion`)
2410
+ * emits an N-branch UnionNode. At the ~1,424 SWM graphs of a large public
2411
+ * context graph that is a ~257 KB query whose union tree makes the
2412
+ * oxigraph-server query planner blow up and reset the connection ("fetch
2413
+ * failed"). A single VALUES table is one basic graph pattern iterated over the
2414
+ * graph list — constant plan depth — so it scales.
2415
+ *
2416
+ * Semantics are preserved. LIMIT/ORDER BY/DISTINCT/GROUP BY/aggregate all live
2417
+ * in the outer query (the `before`/`after` slices) and reduce over the merged
2418
+ * solution set exactly as the UNION form did. The one hazard is the injected
2419
+ * graph variable leaking into the caller's projection: a bare `SELECT *` would
2420
+ * expose `?__dkgViewGraph` as a mystery column, and cross-graph DISTINCT of an
2421
+ * identical triple would split on the graph. Both are neutralised by scoping
2422
+ * the VALUES+GRAPH block inside a sub-SELECT that projects ONLY the user's own
2423
+ * variables (collected from `inner`), so the sentinel never escapes.
2424
+ *
2425
+ * Returns `null` (caller falls back to `wrapWithGraphUnion` / per-graph
2426
+ * execution) when the WHERE block cannot be located, when `inner` carries a
2427
+ * top-level UNION (kept on the #789 form-aware fallback so its behaviour and
2428
+ * tests are untouched), when the user query already binds the sentinel, or
2429
+ * when the WHERE body binds no user variable at all (a var-less body offers
2430
+ * nowhere to hide the sentinel from a `SELECT *` projection).
2431
+ */
2432
+ function wrapWithGraphValues(sparql, graphUris) {
2433
+ return wrapWithProjectedGraphSubselect(sparql, graphUris, [VIEW_GRAPH_SENTINEL], (inner, graphs) => {
2434
+ const values = graphs.map((graph) => `<${assertSafeIri(graph)}>`).join(' ');
2435
+ return `VALUES ${VIEW_GRAPH_SENTINEL} { ${values} } GRAPH ${VIEW_GRAPH_SENTINEL} { ${inner} }`;
2436
+ });
2437
+ }
2438
+ /**
2439
+ * The mirror anti-join is valid only for a flat basic graph pattern (plus
2440
+ * FILTER expressions). Nested graph patterns can differ by boundness, where a
2441
+ * correlated NOT EXISTS compatibility check is not exact mapping equality.
2442
+ */
2443
+ function isDedupSafeBasicGraphPattern(inner) {
2444
+ const forbidden = ['OPTIONAL', 'MINUS', 'SERVICE', 'VALUES', 'BIND', 'SELECT', 'GRAPH', 'EXISTS'];
2445
+ let i = 0;
2446
+ while (i < inner.length) {
2447
+ const ch = inner[i];
2448
+ if (ch === '#') {
2449
+ while (i < inner.length && inner[i] !== '\n')
2450
+ i++;
2451
+ continue;
2452
+ }
2453
+ if (ch === '"' || ch === "'") {
2454
+ i = skipSparqlStringLiteral(inner, i);
2455
+ continue;
2456
+ }
2457
+ if (ch === '<') {
2458
+ const end = skipSparqlIriRef(inner, i);
2459
+ i = end ?? i + 1;
2460
+ continue;
2461
+ }
2462
+ if (ch === '{' || ch === '}')
2463
+ return false;
2464
+ if (isKeywordStart(inner, i)) {
2465
+ let end = i + 1;
2466
+ while (end < inner.length && isWordContinuation(inner[end]))
2467
+ end++;
2468
+ if (forbidden.some((keyword) => isSparqlKeyword(inner, i, end, keyword))) {
2469
+ return false;
2470
+ }
2471
+ i = end;
2472
+ continue;
2473
+ }
2474
+ i++;
2475
+ }
2476
+ return true;
2477
+ }
2478
+ /**
2479
+ * Collect every distinct SPARQL variable (`?x` / `$x`, in first-seen order,
2480
+ * sigil included) in a query fragment, skipping tokens inside line comments,
2481
+ * string literals, and IRI refs. Reuses the same literal/comment/IRI-aware
2482
+ * primitives as `collectGraphVariables` so a `?`/`$` inside a literal or IRI is
2483
+ * never mistaken for a variable.
2484
+ */
2485
+ function collectQueryVariables(sparql) {
2486
+ const variables = [];
2487
+ const seen = new Set();
2488
+ const n = sparql.length;
2489
+ let i = 0;
2490
+ while (i < n) {
2491
+ const ch = sparql[i];
2492
+ if (ch === '#') {
2493
+ while (i < n && sparql[i] !== '\n')
2494
+ i++;
2495
+ continue;
2496
+ }
2497
+ if (ch === '"' || ch === "'") {
2498
+ i = skipSparqlStringLiteral(sparql, i);
2499
+ continue;
2500
+ }
2501
+ if (ch === '<') {
2502
+ const end = skipSparqlIriRef(sparql, i);
2503
+ i = end ?? i + 1;
2504
+ continue;
2505
+ }
2506
+ if (ch === '?' || ch === '$') {
2507
+ const variable = readSparqlVariable(sparql, i);
2508
+ if (variable) {
2509
+ if (!seen.has(variable)) {
2510
+ seen.add(variable);
2511
+ variables.push(variable);
2512
+ }
2513
+ i += variable.length;
2514
+ continue;
2515
+ }
2516
+ }
2517
+ i++;
2518
+ }
2519
+ return variables;
2520
+ }
2175
2521
  /**
2176
2522
  * Rewrites a SPARQL query so EVERY subject variable used in its WHERE
2177
2523
  * block also matches `<http://dkg.io/ontology/trustLevel> ?__trustN`