@weftlet/toolkit 0.7.0 → 0.7.2

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 (50) hide show
  1. package/README.md +40 -29
  2. package/THIRD_PARTY_NOTICES.md +2 -0
  3. package/dist/application/hydrator.js +18 -15
  4. package/dist/application/overlay.js +9 -9
  5. package/dist/application/saved-paths.d.ts +1 -1
  6. package/dist/application/semantic-run.d.ts +1 -1
  7. package/dist/extraction/ontology.d.ts +2 -0
  8. package/dist/extraction/ontology.js +28 -13
  9. package/dist/graph/code-coverage.js +2 -2
  10. package/dist/graph/code-rdf.d.ts +3 -2
  11. package/dist/graph/code-rdf.js +21 -14
  12. package/dist/graph/impact-signals.d.ts +7 -0
  13. package/dist/graph/impact-signals.js +54 -0
  14. package/dist/graph/neighborhood.js +3 -3
  15. package/dist/graph/partitioned-shacl.d.ts +14 -0
  16. package/dist/graph/partitioned-shacl.js +66 -0
  17. package/dist/graph/path-search.d.ts +6 -4
  18. package/dist/graph/path-search.js +50 -24
  19. package/dist/graph/paths-renderer.js +1 -1
  20. package/dist/graph/paths.d.ts +1 -1
  21. package/dist/graph/paths.js +5 -5
  22. package/dist/graph/ranking.d.ts +1 -1
  23. package/dist/graph/ranking.js +17 -9
  24. package/dist/graph/rdf-partitions.d.ts +24 -0
  25. package/dist/graph/rdf-partitions.js +170 -0
  26. package/dist/graph/rdf-projection.d.ts +7 -0
  27. package/dist/graph/rdf-projection.js +27 -0
  28. package/dist/graph/rdf-records.d.ts +12 -0
  29. package/dist/graph/rdf-records.js +90 -0
  30. package/dist/graph/rdf.d.ts +10 -10
  31. package/dist/graph/rdf.js +66 -97
  32. package/dist/graph/validation-progress.d.ts +4 -1
  33. package/dist/infrastructure/json-digest.d.ts +2 -1
  34. package/dist/infrastructure/json-digest.js +2 -2
  35. package/dist/interfaces/cli.js +1 -1
  36. package/dist/storage/storage.d.ts +3 -0
  37. package/dist/storage/storage.js +37 -13
  38. package/dist/storage/streaming-json.d.ts +5 -0
  39. package/dist/storage/streaming-json.js +45 -0
  40. package/dist/visualization/viewer-interaction.js +1 -1
  41. package/docs/1.0-WORK-QUEUE.md +25 -8
  42. package/docs/ADR-012-impact-ranking.md +2 -1
  43. package/docs/ADR-030-bounded-evidence-paths.md +21 -18
  44. package/docs/ADR-040-bounded-graph-io.md +90 -0
  45. package/docs/ADR-041-bounded-relationship-retrieval.md +77 -0
  46. package/docs/RELEASING.md +3 -3
  47. package/docs/VALIDATION.md +121 -0
  48. package/docs/benchmarks/large-graph-2026-11-02/result.json +156 -0
  49. package/docs/extraction-manifest.json +37 -11
  50. package/package.json +5 -2
@@ -0,0 +1,66 @@
1
+ import { DataFactory, Store } from 'n3';
2
+ import SHACLValidator from 'rdf-validate-shacl';
3
+ import { RDF, evidenceShapes } from './shacl-shapes.js';
4
+ const SH = 'http://www.w3.org/ns/shacl#', { namedNode: iri } = DataFactory;
5
+ const targets = new Set([SH + 'targetClass', SH + 'targetSubjectsOf']);
6
+ // All admitted constraints inspect the focus subject's outgoing triples. sh:class
7
+ // additionally reads rdf:type on referenced objects, supplied from the entire graph.
8
+ const allowed = new Set(['targetClass', 'targetSubjectsOf', 'property', 'path', 'minCount', 'maxCount',
9
+ 'datatype', 'minLength', 'minInclusive', 'maxInclusive', 'hasValue', 'in', 'or', 'nodeKind', 'class']);
10
+ export function assertPartitionLocalShapes(shapes) {
11
+ const propertyShapes = new Set(shapes.filter(quad => quad.predicate.value === SH + 'path')
12
+ .map(quad => quad.subject.value));
13
+ for (const quad of shapes) {
14
+ if (quad.predicate.value.startsWith(SH) && !allowed.has(quad.predicate.value.slice(SH.length))) {
15
+ throw Error(`SHACL partition locality must be reviewed for ${quad.predicate.value}`);
16
+ }
17
+ if (quad.predicate.value === SH + 'path' && quad.object.termType !== 'NamedNode') {
18
+ throw Error('SHACL partition locality requires direct predicate paths');
19
+ }
20
+ if (propertyShapes.has(quad.subject.value) && [SH + 'property', SH + 'or'].includes(quad.predicate.value)) {
21
+ throw Error('SHACL partition locality forbids nested value-shape traversal');
22
+ }
23
+ if (quad.predicate.value === RDF + 'type' && ![SH + 'NodeShape', SH + 'PropertyShape'].includes(quad.object.value)) {
24
+ throw Error('SHACL partition locality requires explicit reviewed targets');
25
+ }
26
+ if (!quad.predicate.value.startsWith(SH) && ![RDF + 'type', RDF + 'first', RDF + 'rest'].includes(quad.predicate.value)) {
27
+ throw Error(`Unsupported SHACL partition vocabulary: ${quad.predicate.value}`);
28
+ }
29
+ }
30
+ }
31
+ /** Run the same strict validator/shapes on complete subjects with global class membership. */
32
+ export async function validatePartitions(partitions, types, progress) {
33
+ assertPartitionLocalShapes(evidenceShapes);
34
+ const targetRules = evidenceShapes.filter(quad => targets.has(quad.predicate.value));
35
+ const constraints = evidenceShapes.filter(quad => !targets.has(quad.predicate.value));
36
+ const issues = [];
37
+ let conforms = true, completed = 0;
38
+ for (const batch of partitions.validation()) {
39
+ // Select targets before adding support-only class triples; those objects are
40
+ // validated with their complete outgoing records in their own partition.
41
+ const shapes = new Store(constraints);
42
+ for (const target of targetRules) {
43
+ const selected = target.predicate.value === SH + 'targetClass'
44
+ ? batch.getQuads(null, iri(RDF + 'type'), target.object, null)
45
+ : batch.getQuads(null, target.object, null, null);
46
+ for (const quad of selected)
47
+ shapes.addQuad(target.subject, iri(SH + 'targetNode'), quad.subject);
48
+ }
49
+ const referenced = new Set();
50
+ for (const quad of batch)
51
+ if (quad.object.termType === 'NamedNode')
52
+ referenced.add(quad.object.value);
53
+ for (const node of referenced)
54
+ for (const type of types.get(node) ?? [])
55
+ batch.addQuad(iri(node), iri(RDF + 'type'), iri(type));
56
+ const report = await new SHACLValidator(shapes).validate(batch);
57
+ conforms &&= report.conforms;
58
+ for (const result of report.results)
59
+ issues.push({ category: 'extraction-gap', focus: result.focusNode?.value,
60
+ path: result.path?.value, message: result.message.map(message => message.value).join('; ') });
61
+ progress?.(++completed);
62
+ // Allow progress, cancellation observers and GC between bounded batches.
63
+ await new Promise(resolve => setImmediate(resolve));
64
+ }
65
+ return { conforms, issues };
66
+ }
@@ -16,11 +16,13 @@ export declare function searchEvidencePaths(edges: readonly PathEdge[], start: s
16
16
  paths: EvidencePath[];
17
17
  frontier: {
18
18
  pending: {
19
- nodeId: string;
20
- viaFactIds: string[];
21
- nextFactId: string;
22
- remainingCandidates: number;
19
+ readonly nodeId: string;
20
+ readonly viaFactIds: readonly string[];
21
+ readonly nextFactId: string;
22
+ readonly remainingCandidates: number;
23
23
  }[];
24
+ pendingCount: number;
25
+ omittedPending: number;
24
26
  depthLimited: {
25
27
  count: number;
26
28
  examples: {
@@ -14,7 +14,10 @@ export function searchEvidencePaths(edges, start, target, options, knownNodeIds)
14
14
  validateBounds();
15
15
  const adjacency = new Map();
16
16
  const nodeIds = new Set();
17
- for (const edge of [...edges].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) {
17
+ // Building adjacency already visits every input edge. Prefer closing edges in
18
+ // that index; candidate examination below still counts every selected edge.
19
+ for (const edge of [...edges].sort((a, b) => Number(b.to === target) - Number(a.to === target) ||
20
+ (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
18
21
  const entries = adjacency.get(edge.from) ?? [];
19
22
  entries.push(edge);
20
23
  adjacency.set(edge.from, entries);
@@ -22,40 +25,55 @@ export function searchEvidencePaths(edges, start, target, options, knownNodeIds)
22
25
  nodeIds.add(edge.to);
23
26
  }
24
27
  const paths = [];
25
- const stack = nodeIds.has(start) && nodeIds.has(target) && start !== target ? [{ nodeId: start, next: 0 }] : [];
28
+ // At most one prefix is allocated per examined edge, bounded by searchLimit.
29
+ // Parent links retain distinct simple paths without copying whole path arrays.
30
+ const queue = nodeIds.has(start) && nodeIds.has(target) && start !== target
31
+ ? [{ nodeId: start, next: 0, depth: 0 }] : [];
32
+ let head = 0;
26
33
  const depthExamples = [];
27
34
  let depthCount = 0;
28
35
  let totalSteps = 0;
29
36
  const trim = () => {
30
- while (stack.length && stack.at(-1).next >= (adjacency.get(stack.at(-1).nodeId)?.length ?? 0))
31
- stack.pop();
37
+ while (head < queue.length && queue[head].next >= (adjacency.get(queue[head].nodeId)?.length ?? 0))
38
+ head++;
39
+ };
40
+ const prefixPath = (frame) => {
41
+ const nodeIds = [], factIds = [];
42
+ for (let cursor = frame; cursor; cursor = cursor.parent) {
43
+ nodeIds.push(cursor.nodeId);
44
+ if (cursor.via !== undefined)
45
+ factIds.push(cursor.via);
46
+ }
47
+ return { nodeIds: nodeIds.reverse(), factIds: factIds.reverse() };
32
48
  };
33
49
  const step = () => {
34
- const frame = stack.at(-1);
50
+ const frame = queue[head];
35
51
  const edge = adjacency.get(frame.nodeId)[frame.next++];
36
52
  totalSteps++;
37
- if (stack.some(item => item.nodeId === edge.to))
38
- return;
39
- const viaFactIds = stack.slice(1).map(item => item.via);
40
- if (viaFactIds.length === options.maxDepth) {
53
+ for (let cursor = frame; cursor; cursor = cursor.parent) {
54
+ if (cursor.nodeId === edge.to)
55
+ return;
56
+ }
57
+ if (frame.depth === options.maxDepth) {
41
58
  depthCount++;
42
59
  if (depthExamples.length < 20)
43
- depthExamples.push({ nodeId: frame.nodeId, viaFactIds,
60
+ depthExamples.push({ nodeId: frame.nodeId, viaFactIds: prefixPath(frame).factIds,
44
61
  nextFactId: edge.id, nextNodeId: edge.to });
45
62
  }
46
63
  else if (edge.to === target) {
47
- return { nodeIds: [...stack.map(item => item.nodeId), target], factIds: [...viaFactIds, edge.id] };
64
+ const path = prefixPath(frame);
65
+ return { nodeIds: [...path.nodeIds, target], factIds: [...path.factIds, edge.id] };
48
66
  }
49
- else {
50
- stack.push({ nodeId: edge.to, next: 0, via: edge.id });
67
+ else if (adjacency.has(edge.to)) {
68
+ queue.push({ nodeId: edge.to, next: 0, depth: frame.depth + 1, via: edge.id, parent: frame });
51
69
  }
52
70
  };
53
71
  trim();
54
- // Reconstruct only bounded search state. Never accept a caller-supplied stack or facts.
72
+ // Reconstruct bounded search state; never accept a caller-supplied queue or facts.
55
73
  const replay = () => {
56
74
  let replayPageSteps = 0;
57
75
  let replayPagePaths = 0;
58
- while (totalSteps < offset && stack.length) {
76
+ while (totalSteps < offset && head < queue.length) {
59
77
  if (step())
60
78
  replayPagePaths++;
61
79
  replayPageSteps++;
@@ -65,26 +83,33 @@ export function searchEvidencePaths(edges, start, target, options, knownNodeIds)
65
83
  }
66
84
  trim();
67
85
  }
68
- if (totalSteps !== offset || (offset && (!stack.length || replayPagePaths || replayPageSteps))) {
86
+ if (totalSteps !== offset || (offset && (head === queue.length || replayPagePaths || replayPageSteps))) {
69
87
  throw new Error('Invalid path continuation boundary');
70
88
  }
71
89
  };
72
90
  replay();
73
91
  if (!offset && start === target && (nodeIds.has(start) || knownNodeIds?.has(start)))
74
92
  paths.push({ nodeIds: [start], factIds: [] });
75
- while (stack.length && paths.length < options.maxPaths && totalSteps - offset < options.maxSteps && totalSteps < searchLimit) {
93
+ while (head < queue.length && paths.length < options.maxPaths && totalSteps - offset < options.maxSteps && totalSteps < searchLimit) {
76
94
  const path = step();
77
95
  if (path)
78
96
  paths.push(path);
79
97
  trim();
80
98
  }
81
- const pending = stack.flatMap((frame, index) => {
99
+ const pending = [];
100
+ let pendingCount = 0;
101
+ for (let index = head; index < queue.length; index++) {
102
+ const frame = queue[index];
82
103
  const entries = adjacency.get(frame.nodeId) ?? [];
83
- return frame.next < entries.length ? [{ nodeId: frame.nodeId,
84
- viaFactIds: stack.slice(1, index + 1).map(item => item.via), nextFactId: entries[frame.next].id,
85
- remainingCandidates: entries.length - frame.next }] : [];
86
- });
87
- const exhaustedWithinDepth = stack.length === 0;
104
+ if (frame.next >= entries.length)
105
+ continue;
106
+ pendingCount++;
107
+ if (pending.length < 32)
108
+ pending.push({ nodeId: frame.nodeId,
109
+ viaFactIds: prefixPath(frame).factIds, nextFactId: entries[frame.next].id,
110
+ remainingCandidates: entries.length - frame.next });
111
+ }
112
+ const exhaustedWithinDepth = head === queue.length;
88
113
  const stopReason = () => {
89
114
  if (exhaustedWithinDepth)
90
115
  return 'exhausted';
@@ -94,7 +119,8 @@ export function searchEvidencePaths(edges, start, target, options, knownNodeIds)
94
119
  return 'path-page-limit';
95
120
  return 'work-page-limit';
96
121
  };
97
- return { paths, frontier: { pending, depthLimited: { count: depthCount, examples: depthExamples,
122
+ return { paths, frontier: { pending, pendingCount, omittedPending: pendingCount - pending.length,
123
+ depthLimited: { count: depthCount, examples: depthExamples,
98
124
  omitted: depthCount - depthExamples.length } }, search: { searchLimit, replayedSteps: offset,
99
125
  examinedSteps: totalSteps - offset, totalSteps, stopReason: stopReason(), exhaustedWithinDepth,
100
126
  depthLimitReached: depthCount > 0, truncated: !exhaustedWithinDepth || depthCount > 0 } };
@@ -16,7 +16,7 @@ export function renderGraphPaths(result) {
16
16
  `Direction: ${search.direction}; predicates: ${search.predicates.join(', ') || 'all'}; order: ${search.order}`,
17
17
  `Depth: ${search.maxDepth}; paths on page: ${result.paths.length}/${search.maxPaths}; stop: ${search.stopReason}; truncated: ${search.truncated}`,
18
18
  `Candidate steps: ${search.examinedSteps} new + ${search.replayedSteps} replay; ${search.totalSteps}/${search.searchLimit} total ceiling.`,
19
- `Unexamined prefixes: ${frontier.pending.length}; depth-limited extensions: ${frontier.depthLimited.count} (${frontier.depthLimited.omitted} examples omitted).`,
19
+ `Unexamined prefixes: ${frontier.pendingCount ?? frontier.pending.length} (${frontier.omittedPending ?? 0} examples omitted); depth-limited extensions: ${frontier.depthLimited.count} (${frontier.depthLimited.omitted} examples omitted).`,
20
20
  ...(search.continuation ? [`Continuation (same request): ${search.continuation}`] : []),
21
21
  ...(search.depthLimitReached ? ['Depth limit reached: restart with a larger maxDepth or explore a frontier node. Continuation does not lift the depth bound.'] : []),
22
22
  ...(search.stopReason === 'search-limit' ? ['Total search limit reached: narrow the relations or explore an intermediate node; no further continuation is available.'] : []),
@@ -1,5 +1,5 @@
1
1
  import type { GraphPathQueryService, GraphPaths, GraphPathsRequest, SnapshotStore } from '@weftlet/core';
2
- export declare const GRAPH_PATHS_VERSION = "simple-evidence-paths-v1";
2
+ export declare const GRAPH_PATHS_VERSION = "simple-evidence-paths-v2";
3
3
  export declare class SnapshotGraphPathService implements GraphPathQueryService {
4
4
  private readonly store;
5
5
  constructor(store: Pick<SnapshotStore, 'read'>);
@@ -1,12 +1,12 @@
1
1
  import { snapshotContext } from './snapshot-context.js';
2
- import { sha256, stableJson } from '../infrastructure/util.js';
2
+ import { stableJsonDigest } from '../infrastructure/util.js';
3
3
  import { RdfEvidenceGraph } from './rdf.js';
4
4
  import { coverageForFacts } from './coverage.js';
5
5
  import { selectDiagnostics } from './diagnostics.js';
6
6
  import { parseGraphPathsRequest } from './path-request.js';
7
7
  import { searchEvidencePaths } from './path-search.js';
8
8
  import { renderGraphPaths } from './paths-renderer.js';
9
- export const GRAPH_PATHS_VERSION = 'simple-evidence-paths-v1';
9
+ export const GRAPH_PATHS_VERSION = 'simple-evidence-paths-v2';
10
10
  function continuationOffset(token, binding) {
11
11
  if (!token)
12
12
  return 0;
@@ -41,8 +41,8 @@ export class SnapshotGraphPathService {
41
41
  if (new Set(all.map(fact => fact.id)).size !== all.length)
42
42
  throw new Error('Duplicate fact identities prevent stable graph.paths pagination');
43
43
  const { continuation, ...query } = request;
44
- const binding = sha256(stableJson({ version: GRAPH_PATHS_VERSION, query, manifest: saved.manifest,
45
- facts: all, diagnostics: saved.diagnostics, unresolved: saved.unresolved }));
44
+ const binding = stableJsonDigest({ version: GRAPH_PATHS_VERSION, query, manifest: saved.manifest,
45
+ facts: all, diagnostics: saved.diagnostics, unresolved: saved.unresolved });
46
46
  const offset = continuationOffset(continuation, binding);
47
47
  const selected = all.filter(fact => !request.predicates.length || request.predicates.includes(fact.predicate));
48
48
  const edges = selected.flatMap(fact => {
@@ -65,7 +65,7 @@ export class SnapshotGraphPathService {
65
65
  paths: result.paths, facts, coverage: coverageForFacts(facts, saved),
66
66
  diagnostics: selectDiagnostics(saved, {}, facts, [], [], new Set([request.fromNodeId, request.toNodeId,
67
67
  ...facts.flatMap(fact => [fact.subject.id, fact.object.id])])),
68
- search: { version: GRAPH_PATHS_VERSION, order: 'fact-id-depth-first', direction: request.direction,
68
+ search: { version: GRAPH_PATHS_VERSION, order: 'shortest-first-target-first', direction: request.direction,
69
69
  predicates: request.predicates, maxDepth: request.maxDepth, maxPaths: request.maxPaths, maxSteps: request.maxSteps,
70
70
  ...result.search, ...(next ? { continuation: next } : {}) }, frontier: result.frontier,
71
71
  };
@@ -1,6 +1,6 @@
1
1
  import type { GraphFact } from '@weftlet/core';
2
2
  /** Binds continuation offsets to the ordering policy; changing this requires restarting a scan. */
3
- export declare const IMPACT_RANKING_VERSION = "proximity-diversity-v1";
3
+ export declare const IMPACT_RANKING_VERSION = "local-relationships-diversity-v2";
4
4
  export declare function factSearchText(fact: GraphFact): string;
5
5
  export interface RankedImpactFact {
6
6
  readonly fact: GraphFact;
@@ -1,6 +1,7 @@
1
1
  import { stableJson } from '../infrastructure/util.js';
2
+ import { impactRankingSignals } from './impact-signals.js';
2
3
  /** Binds continuation offsets to the ordering policy; changing this requires restarting a scan. */
3
- export const IMPACT_RANKING_VERSION = 'proximity-diversity-v1';
4
+ export const IMPACT_RANKING_VERSION = 'local-relationships-diversity-v2';
4
5
  export function factSearchText(fact) {
5
6
  return [fact.subject.id, fact.subject.label, fact.predicate, fact.object.id,
6
7
  fact.object.label, fact.provenance.path].join(' ').toLowerCase();
@@ -24,6 +25,9 @@ function stableTie(fact) {
24
25
  }
25
26
  /** Order an already bounded reachable set. No facts are pruned and no authority is changed. */
26
27
  export function rankImpactFacts(facts, direct, anchorTerms, focus) {
28
+ const signals = impactRankingSignals(facts, anchorTerms);
29
+ const costs = new Map(facts.map(fact => [fact, signals.penalty(fact)]));
30
+ const cost = (fact, forward) => Math.max(edgeCost(fact, forward), costs.get(fact));
27
31
  const adjacent = new Map();
28
32
  const connect = (from, to, cost) => {
29
33
  if (from.kind === 'Project' || to.kind === 'Project')
@@ -33,8 +37,8 @@ export function rankImpactFacts(facts, direct, anchorTerms, focus) {
33
37
  adjacent.set(from.id, edges);
34
38
  };
35
39
  for (const fact of facts) {
36
- connect(fact.subject, fact.object, edgeCost(fact, true));
37
- connect(fact.object, fact.subject, edgeCost(fact, false));
40
+ connect(fact.subject, fact.object, cost(fact, true));
41
+ connect(fact.object, fact.subject, cost(fact, false));
38
42
  }
39
43
  const distance = new Map();
40
44
  // Integer edge costs permit a bucket queue: O(edges + maximum path cost), without a heap dependency.
@@ -46,10 +50,8 @@ export function rankImpactFacts(facts, direct, anchorTerms, focus) {
46
50
  (pending[cost] ??= []).push(id);
47
51
  };
48
52
  for (const fact of direct) {
49
- const eligible = [fact.subject, fact.object].filter((node) => node.kind !== 'Project');
50
- const matching = eligible.filter((node) => anchorTerms.some((term) => `${node.id} ${node.label}`.toLowerCase().includes(term)));
51
53
  // Some adapters only identify an anchor through its citation. Keep those queries supported.
52
- for (const node of matching.length ? matching : eligible)
54
+ for (const node of signals.seedNodes(fact))
53
55
  enqueue(node.id, 0);
54
56
  }
55
57
  for (let cost = 0; cost < pending.length; cost++) {
@@ -65,14 +67,20 @@ export function rankImpactFacts(facts, direct, anchorTerms, focus) {
65
67
  const text = factSearchText(fact);
66
68
  return {
67
69
  fact,
68
- proximity: directIds.has(fact.id) ? 0 : Math.min((distance.get(fact.subject.id) ?? Infinity) + edgeCost(fact, true), (distance.get(fact.object.id) ?? Infinity) + edgeCost(fact, false)),
70
+ proximity: costs.get(fact) + (directIds.has(fact.id) ? 0 : Math.min((distance.get(fact.subject.id) ?? Infinity) + cost(fact, true), (distance.get(fact.object.id) ?? Infinity) + cost(fact, false))),
71
+ relationshipOrder: signals.relationshipOrder(fact),
69
72
  focus: focus.filter((term) => text.includes(term)).length,
70
73
  tie: stableTie(fact),
71
74
  };
72
75
  });
73
76
  const lexical = (a, b) => a < b ? -1 : a > b ? 1 : 0;
74
- const compare = (a, b) => b.focus - a.focus || lexical(a.tie, b.tie) || lexical(a.fact.id, b.fact.id);
75
- rows.sort((a, b) => a.proximity - b.proximity || compare(a, b));
77
+ const compare = (a, b) => a.relationshipOrder - b.relationshipOrder || b.focus - a.focus || lexical(a.tie, b.tie) || lexical(a.fact.id, b.fact.id);
78
+ // Allocate each source's early slots to relationships before local occurrence
79
+ // detail. Otherwise hundreds of nearer variable references consume a file's
80
+ // diversity allowance before its file-to-symbol consumer can be considered.
81
+ // Final ordering below still charges distance and shared-library penalties.
82
+ rows.sort((a, b) => costs.get(a.fact) - costs.get(b.fact) ||
83
+ a.relationshipOrder - b.relationshipOrder || a.proximity - b.proximity || compare(a, b));
76
84
  const counts = new Map();
77
85
  const diverse = rows.map((row) => {
78
86
  const seen = counts.get(row.fact.provenance.path) ?? 0;
@@ -0,0 +1,24 @@
1
+ import { Store, type Term } from 'n3';
2
+ import type { GraphSnapshot } from '@weftlet/core';
3
+ type Quad = Parameters<Store['add']>[0];
4
+ /** Complete subject groups: repeated identities are joined before validation, never split. */
5
+ export declare class RdfPartitions {
6
+ private readonly snapshot;
7
+ private readonly batchSize;
8
+ private readonly records;
9
+ private readonly nodes;
10
+ constructor(snapshot: GraphSnapshot, batchSize?: number);
11
+ /** RDF/JS matching over the same records, without a second whole-graph quad store. */
12
+ match(subject?: Term | null, predicate?: Term | null, object?: Term | null, graph?: Term | null): IterableIterator<Quad>;
13
+ /** Exact cardinalities for the record projection; other patterns count the same iterator. */
14
+ countMatches(subject?: Term | null, predicate?: Term | null, object?: Term | null, graph?: Term | null): number;
15
+ /** Each union subject occurs in exactly one partition, with all its outgoing triples. */
16
+ validation(): IterableIterator<Store>;
17
+ /** Original named dataset, in bounded groups. Triple order is not a content identity. */
18
+ named(): IterableIterator<Store>;
19
+ private recordPartitions;
20
+ /** Only cross-subject information used by the admitted SHACL fragment is rdf:type. */
21
+ get count(): number;
22
+ types(progress?: (completed: number) => void): Map<string, Set<string>>;
23
+ }
24
+ export {};
@@ -0,0 +1,170 @@
1
+ import { DataFactory, Store } from 'n3';
2
+ import { stableJson } from '../infrastructure/util.js';
3
+ import { addAttributes, addFactEdges, addFactRecord, addManifest, addNodeRdf, nodeIri } from './rdf-records.js';
4
+ import { CG, RDF } from './shacl-shapes.js';
5
+ const { namedNode: iri } = DataFactory;
6
+ /** Complete subject groups: repeated identities are joined before validation, never split. */
7
+ export class RdfPartitions {
8
+ snapshot;
9
+ batchSize;
10
+ records = new Map();
11
+ nodes = new Map();
12
+ constructor(snapshot, batchSize = 256) {
13
+ this.snapshot = snapshot;
14
+ this.batchSize = batchSize;
15
+ if (!Number.isSafeInteger(batchSize) || batchSize < 1)
16
+ throw Error('Invalid RDF partition size');
17
+ for (const [ordinal, fact] of snapshot.facts.entries()) {
18
+ const records = this.records.get(fact.id) ?? [];
19
+ records.push({ fact, ordinal });
20
+ this.records.set(fact.id, records);
21
+ for (const node of [fact.subject, fact.object]) {
22
+ const group = this.nodes.get(node.id) ?? { variants: new Map(), outgoing: [], attributes: [] };
23
+ group.variants.set(stableJson(node), node);
24
+ this.nodes.set(node.id, group);
25
+ if (Object.keys(node.attributes ?? {}).length)
26
+ group.attributes.push({ node, factId: fact.id });
27
+ }
28
+ this.nodes.get(fact.subject.id).outgoing.push(fact);
29
+ }
30
+ }
31
+ /** RDF/JS matching over the same records, without a second whole-graph quad store. */
32
+ *match(subject, predicate, object, graph) {
33
+ const decode = (term, prefix, suffix = false) => {
34
+ if (term?.termType !== 'NamedNode' || !term.value.startsWith(prefix))
35
+ return undefined;
36
+ try {
37
+ return decodeURIComponent(suffix ? term.value.slice(prefix.length).split(':')[0] : term.value.slice(prefix.length));
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ };
43
+ const recordId = decode(subject, 'urn:context-graph:fact:', true);
44
+ const graphId = decode(graph, 'urn:context-graph:fact:');
45
+ const nodeId = decode(subject, 'urn:context-graph:node:');
46
+ let groups;
47
+ if (recordId !== undefined || graphId !== undefined) {
48
+ const batch = new Store();
49
+ for (const { fact, ordinal } of this.records.get(recordId ?? graphId) ?? []) {
50
+ addFactRecord(batch, fact, ordinal);
51
+ addFactEdges(batch, fact);
52
+ }
53
+ groups = [batch];
54
+ }
55
+ else if (nodeId !== undefined) {
56
+ const batch = new Store(), group = this.nodes.get(nodeId);
57
+ if (group) {
58
+ for (const node of group.variants.values())
59
+ addNodeRdf(batch, node);
60
+ for (const fact of group.outgoing)
61
+ addFactEdges(batch, fact);
62
+ for (const { node, factId } of group.attributes)
63
+ addAttributes(batch, node, iri(`urn:context-graph:fact:${encodeURIComponent(factId)}`));
64
+ }
65
+ groups = [batch];
66
+ }
67
+ else if (subject?.value === 'urn:context-graph:snapshot') {
68
+ const batch = new Store();
69
+ addManifest(batch, this.snapshot);
70
+ groups = [batch];
71
+ }
72
+ else if (subject)
73
+ groups = [];
74
+ else
75
+ groups = this.named();
76
+ for (const batch of groups)
77
+ for (const quad of batch)
78
+ if ((!subject || subject.equals(quad.subject)) && (!predicate || predicate.equals(quad.predicate)) &&
79
+ (!object || object.equals(quad.object)) && (!graph || graph.equals(quad.graph)))
80
+ yield quad;
81
+ }
82
+ /** Exact cardinalities for the record projection; other patterns count the same iterator. */
83
+ countMatches(subject, predicate, object, graph) {
84
+ if (!subject && !object && [CG + 'record', CG + 'ordinal'].includes(predicate?.value ?? '')) {
85
+ if (graph && graph.termType !== 'DefaultGraph')
86
+ return 0;
87
+ if (predicate.value === CG + 'ordinal')
88
+ return this.snapshot.facts.length;
89
+ let count = 0;
90
+ for (const group of this.records.values())
91
+ count += group.length === 1 ? 1 : new Set(group.map(({ fact }) => stableJson(fact))).size;
92
+ return count;
93
+ }
94
+ let count = 0;
95
+ for (const _quad of this.match(subject, predicate, object, graph))
96
+ count++;
97
+ return count;
98
+ }
99
+ /** Each union subject occurs in exactly one partition, with all its outgoing triples. */
100
+ *validation() {
101
+ yield* this.recordPartitions(false);
102
+ let batch = new Store(), count = 0;
103
+ for (const [id, group] of this.nodes) {
104
+ for (const node of group.variants.values()) {
105
+ addNodeRdf(batch, node);
106
+ addAttributes(batch, node);
107
+ }
108
+ for (const fact of group.outgoing)
109
+ batch.addQuad(iri(nodeIri(id)), iri(CG + fact.predicate), iri(nodeIri(fact.object.id)));
110
+ if (++count % this.batchSize === 0) {
111
+ yield batch;
112
+ batch = new Store();
113
+ }
114
+ }
115
+ if (batch.size)
116
+ yield batch;
117
+ const manifest = new Store();
118
+ addManifest(manifest, this.snapshot);
119
+ yield manifest;
120
+ }
121
+ /** Original named dataset, in bounded groups. Triple order is not a content identity. */
122
+ *named() {
123
+ yield* this.recordPartitions(true);
124
+ let batch = new Store(), count = 0;
125
+ for (const group of this.nodes.values()) {
126
+ for (const node of group.variants.values())
127
+ addNodeRdf(batch, node);
128
+ if (++count % this.batchSize === 0) {
129
+ yield batch;
130
+ batch = new Store();
131
+ }
132
+ }
133
+ if (batch.size)
134
+ yield batch;
135
+ const manifest = new Store();
136
+ addManifest(manifest, this.snapshot);
137
+ yield manifest;
138
+ }
139
+ *recordPartitions(edges) {
140
+ let batch = new Store(), count = 0;
141
+ for (const records of this.records.values()) {
142
+ for (const { fact, ordinal } of records) {
143
+ addFactRecord(batch, fact, ordinal);
144
+ if (edges)
145
+ addFactEdges(batch, fact);
146
+ }
147
+ if (++count % this.batchSize === 0) {
148
+ yield batch;
149
+ batch = new Store();
150
+ }
151
+ }
152
+ if (batch.size)
153
+ yield batch;
154
+ }
155
+ /** Only cross-subject information used by the admitted SHACL fragment is rdf:type. */
156
+ get count() { return Math.ceil(this.records.size / this.batchSize) + Math.ceil(this.nodes.size / this.batchSize) + 1; }
157
+ types(progress) {
158
+ const result = new Map();
159
+ let completed = 0;
160
+ for (const batch of this.validation()) {
161
+ for (const quad of batch.getQuads(null, iri(RDF + 'type'), null, null)) {
162
+ const values = result.get(quad.subject.value) ?? new Set();
163
+ values.add(quad.object.value);
164
+ result.set(quad.subject.value, values);
165
+ }
166
+ progress?.(++completed);
167
+ }
168
+ return result;
169
+ }
170
+ }
@@ -0,0 +1,7 @@
1
+ import type { GraphFact } from '@weftlet/core';
2
+ /**
3
+ * Equivalent to SELECT cg:record JOIN cg:ordinal ORDER BY ordinal. The record
4
+ * literal uses the canonical RDF encoder, not a separate interpretation of facts.
5
+ * Preserve duplicate-ID join multiplicity too; callers can reject corrupt IDs.
6
+ */
7
+ export declare function projectRdfFacts(facts: readonly GraphFact[]): readonly GraphFact[];
@@ -0,0 +1,27 @@
1
+ import { stableJson } from '../infrastructure/util.js';
2
+ /**
3
+ * Equivalent to SELECT cg:record JOIN cg:ordinal ORDER BY ordinal. The record
4
+ * literal uses the canonical RDF encoder, not a separate interpretation of facts.
5
+ * Preserve duplicate-ID join multiplicity too; callers can reject corrupt IDs.
6
+ */
7
+ export function projectRdfFacts(facts) {
8
+ const counts = new Map();
9
+ for (const fact of facts)
10
+ counts.set(fact.id, (counts.get(fact.id) ?? 0) + 1);
11
+ const duplicates = new Map();
12
+ for (const fact of facts)
13
+ if (counts.get(fact.id) > 1) {
14
+ const records = duplicates.get(fact.id) ?? new Set();
15
+ records.add(stableJson(fact));
16
+ duplicates.set(fact.id, records);
17
+ }
18
+ const result = [];
19
+ for (const fact of facts) {
20
+ if (duplicates.has(fact.id))
21
+ for (const record of duplicates.get(fact.id))
22
+ result.push(JSON.parse(record));
23
+ else
24
+ result.push(JSON.parse(stableJson(fact)));
25
+ }
26
+ return result;
27
+ }
@@ -0,0 +1,12 @@
1
+ import { DataFactory, Store } from 'n3';
2
+ import type { GraphFact, GraphSnapshot, GraphNode } from '@weftlet/core';
3
+ declare const iri: <Iri extends string = string>(value: Iri) => import("n3").NamedNode<Iri>;
4
+ export declare const nodeIri: (id: string) => string;
5
+ export declare const factIri: (id: string) => string;
6
+ export declare function addFactRecord(dataset: Store, fact: GraphFact, ordinal: number): void;
7
+ export declare function addNodeRdf(dataset: Store, node: GraphNode): void;
8
+ export declare function addAttributes(dataset: Store, node: GraphNode, graph?: ReturnType<typeof iri> | ReturnType<typeof DataFactory.defaultGraph>): void;
9
+ export declare function addFactEdges(dataset: Store, fact: GraphFact): void;
10
+ export declare function addManifest(dataset: Store, snapshot: GraphSnapshot): void;
11
+ export declare function evidenceDataset(snapshot: GraphSnapshot): Store;
12
+ export {};