@weftlet/toolkit 0.6.1 → 0.7.1
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.
- package/README.md +191 -18
- package/THIRD_PARTY_NOTICES.md +3 -1
- package/dist/application/application.d.ts +10 -0
- package/dist/application/application.js +20 -0
- package/dist/application/document-code-bindings.d.ts +11 -0
- package/dist/application/document-code-bindings.js +25 -0
- package/dist/application/document-obligations.d.ts +8 -0
- package/dist/application/document-obligations.js +11 -0
- package/dist/application/draft-facts.d.ts +3 -0
- package/dist/application/draft-facts.js +37 -0
- package/dist/application/extraction-identity.d.ts +2 -2
- package/dist/application/extraction-identity.js +7 -1
- package/dist/application/extractor-results.d.ts +6 -0
- package/dist/application/extractor-results.js +6 -0
- package/dist/application/hydrator.js +38 -18
- package/dist/application/mapping-proposals.d.ts +108 -0
- package/dist/application/mapping-proposals.js +142 -0
- package/dist/application/mapping-service.d.ts +2 -0
- package/dist/application/mapping-service.js +5 -0
- package/dist/application/overlay.js +43 -29
- package/dist/application/saved-code-coverage.d.ts +40 -0
- package/dist/application/saved-code-coverage.js +20 -0
- package/dist/application/saved-compatibility.d.ts +38 -0
- package/dist/application/saved-compatibility.js +20 -0
- package/dist/application/saved-evidence.d.ts +17 -11
- package/dist/application/saved-evidence.js +44 -40
- package/dist/application/saved-exploration.d.ts +57 -0
- package/dist/application/saved-exploration.js +21 -0
- package/dist/application/saved-paths.d.ts +58 -0
- package/dist/application/saved-paths.js +20 -0
- package/dist/application/semantic-run.d.ts +1 -1
- package/dist/extraction/call-syntax.d.ts +18 -0
- package/dist/extraction/call-syntax.js +61 -0
- package/dist/extraction/canonical-document-links.d.ts +5 -1
- package/dist/extraction/canonical-document-links.js +4 -4
- package/dist/extraction/code-inventory.d.ts +6 -0
- package/dist/extraction/code-inventory.js +26 -0
- package/dist/extraction/document-code-bindings.d.ts +14 -0
- package/dist/extraction/document-code-bindings.js +121 -0
- package/dist/extraction/document-obligations.d.ts +12 -0
- package/dist/extraction/document-obligations.js +112 -0
- package/dist/extraction/mapping-candidates.d.ts +13 -0
- package/dist/extraction/mapping-candidates.js +46 -0
- package/dist/extraction/mapping-output.d.ts +75 -0
- package/dist/extraction/mapping-output.js +53 -0
- package/dist/extraction/mapping-plan.d.ts +7 -0
- package/dist/extraction/mapping-plan.js +117 -0
- package/dist/extraction/mapping-prompt.d.ts +2 -0
- package/dist/extraction/mapping-prompt.js +17 -0
- package/dist/extraction/mapping-provider.d.ts +15 -0
- package/dist/extraction/mapping-provider.js +60 -0
- package/dist/extraction/mapping-request.d.ts +3 -0
- package/dist/extraction/mapping-request.js +32 -0
- package/dist/extraction/ontology.d.ts +2 -0
- package/dist/extraction/ontology.js +28 -13
- package/dist/extraction/routing.js +5 -20
- package/dist/extraction/scip-occurrences.d.ts +3 -0
- package/dist/extraction/scip-occurrences.js +102 -0
- package/dist/extraction/scip-ranges.d.ts +6 -0
- package/dist/extraction/scip-ranges.js +31 -0
- package/dist/extraction/scip-runtime.d.ts +39 -0
- package/dist/extraction/scip-runtime.js +8 -0
- package/dist/extraction/scip-symbols.d.ts +21 -0
- package/dist/extraction/scip-symbols.js +91 -0
- package/dist/extraction/semantic.d.ts +1 -1
- package/dist/extraction/semantic.js +5 -17
- package/dist/extraction/static-calls.d.ts +10 -0
- package/dist/extraction/static-calls.js +133 -0
- package/dist/extraction/static.d.ts +3 -20
- package/dist/extraction/static.js +8 -84
- package/dist/extraction/symbol-resolution.d.ts +12 -0
- package/dist/extraction/symbol-resolution.js +51 -0
- package/dist/extraction/symbol-selectors.d.ts +11 -0
- package/dist/extraction/symbol-selectors.js +51 -0
- package/dist/graph/binding-rdf.d.ts +7 -0
- package/dist/graph/binding-rdf.js +109 -0
- package/dist/graph/call-rdf.d.ts +4 -0
- package/dist/graph/call-rdf.js +94 -0
- package/dist/graph/call-shapes.d.ts +2 -0
- package/dist/graph/call-shapes.js +28 -0
- package/dist/graph/code-coverage-projection.d.ts +9 -0
- package/dist/graph/code-coverage-projection.js +117 -0
- package/dist/graph/code-coverage-renderer.d.ts +2 -0
- package/dist/graph/code-coverage-renderer.js +15 -0
- package/dist/graph/code-coverage-request.d.ts +6 -0
- package/dist/graph/code-coverage-request.js +19 -0
- package/dist/graph/code-coverage.d.ts +6 -0
- package/dist/graph/code-coverage.js +54 -0
- package/dist/graph/code-description.d.ts +4 -0
- package/dist/graph/code-description.js +24 -0
- package/dist/graph/code-rdf.d.ts +6 -0
- package/dist/graph/code-rdf.js +79 -0
- package/dist/graph/compatibility-context.d.ts +2 -0
- package/dist/graph/compatibility-context.js +18 -0
- package/dist/graph/compatibility-renderer.d.ts +2 -0
- package/dist/graph/compatibility-renderer.js +45 -0
- package/dist/graph/compatibility-request.d.ts +3 -0
- package/dist/graph/compatibility-request.js +18 -0
- package/dist/graph/compatibility.d.ts +7 -0
- package/dist/graph/compatibility.js +67 -0
- package/dist/graph/constraint-comparison.d.ts +7 -0
- package/dist/graph/constraint-comparison.js +49 -0
- package/dist/graph/coverage.d.ts +2 -2
- package/dist/graph/coverage.js +8 -3
- package/dist/graph/diagnostics.d.ts +1 -1
- package/dist/graph/diagnostics.js +10 -6
- package/dist/graph/evidence-description.d.ts +3 -0
- package/dist/graph/evidence-description.js +13 -0
- package/dist/graph/identity-preflight.js +1 -1
- package/dist/graph/neighborhood-renderer.d.ts +2 -0
- package/dist/graph/neighborhood-renderer.js +38 -0
- package/dist/graph/neighborhood-request.d.ts +9 -0
- package/dist/graph/neighborhood-request.js +37 -0
- package/dist/graph/neighborhood.d.ts +8 -0
- package/dist/graph/neighborhood.js +100 -0
- package/dist/graph/obligation-rdf.d.ts +5 -0
- package/dist/graph/obligation-rdf.js +67 -0
- package/dist/graph/obligation-shapes.d.ts +2 -0
- package/dist/graph/obligation-shapes.js +32 -0
- package/dist/graph/partitioned-shacl.d.ts +14 -0
- package/dist/graph/partitioned-shacl.js +66 -0
- package/dist/graph/path-request.d.ts +11 -0
- package/dist/graph/path-request.js +24 -0
- package/dist/graph/path-search.d.ts +45 -0
- package/dist/graph/path-search.js +101 -0
- package/dist/graph/paths-renderer.d.ts +2 -0
- package/dist/graph/paths-renderer.js +36 -0
- package/dist/graph/paths.d.ts +7 -0
- package/dist/graph/paths.js +74 -0
- package/dist/graph/query.js +23 -6
- package/dist/graph/rdf-partitions.d.ts +24 -0
- package/dist/graph/rdf-partitions.js +170 -0
- package/dist/graph/rdf-projection.d.ts +7 -0
- package/dist/graph/rdf-projection.js +27 -0
- package/dist/graph/rdf-records.d.ts +12 -0
- package/dist/graph/rdf-records.js +90 -0
- package/dist/graph/rdf.d.ts +10 -10
- package/dist/graph/rdf.js +84 -89
- package/dist/graph/shacl-shapes.js +70 -0
- package/dist/graph/snapshot-context.d.ts +3 -0
- package/dist/graph/snapshot-context.js +8 -0
- package/dist/graph/validation-progress.d.ts +4 -1
- package/dist/index.d.ts +23 -0
- package/dist/index.js +22 -0
- package/dist/infrastructure/json-digest.d.ts +3 -0
- package/dist/infrastructure/json-digest.js +3 -0
- package/dist/infrastructure/unique-json.d.ts +2 -0
- package/dist/infrastructure/unique-json.js +20 -0
- package/dist/interfaces/cli.js +33 -1
- package/dist/interfaces/code-coverage.d.ts +46 -0
- package/dist/interfaces/code-coverage.js +18 -0
- package/dist/interfaces/graph-compatibility.d.ts +35 -0
- package/dist/interfaces/graph-compatibility.js +21 -0
- package/dist/interfaces/graph-exploration.d.ts +59 -0
- package/dist/interfaces/graph-exploration.js +27 -0
- package/dist/interfaces/graph-paths.d.ts +77 -0
- package/dist/interfaces/graph-paths.js +31 -0
- package/dist/interfaces/graph-query-args.d.ts +2 -0
- package/dist/interfaces/graph-query-args.js +25 -0
- package/dist/interfaces/mapping-proposals.d.ts +204 -0
- package/dist/interfaces/mapping-proposals.js +74 -0
- package/dist/interfaces/mcp.js +31 -3
- package/dist/storage/mapping-proposals.d.ts +9 -0
- package/dist/storage/mapping-proposals.js +42 -0
- package/dist/storage/storage.d.ts +3 -0
- package/dist/storage/storage.js +37 -13
- package/dist/storage/streaming-json.d.ts +5 -0
- package/dist/storage/streaming-json.js +45 -0
- package/dist/visualization/evidence-sources.d.ts +6 -0
- package/dist/visualization/evidence-sources.js +19 -0
- package/dist/visualization/viewer-interaction.d.ts +5 -8
- package/dist/visualization/viewer-interaction.js +46 -44
- package/dist/visualization/visualization.js +14 -5
- package/docs/1.0-WORK-QUEUE.md +48 -0
- package/docs/ADR-029-exact-graph-neighborhoods.md +70 -0
- package/docs/ADR-030-bounded-evidence-paths.md +97 -0
- package/docs/ADR-031-code-extractor-selection.md +91 -0
- package/docs/ADR-032-scip-code-evidence.md +73 -0
- package/docs/ADR-033-document-symbol-bindings.md +83 -0
- package/docs/ADR-034-mapping-gaps.md +80 -0
- package/docs/ADR-035-static-call-evidence.md +78 -0
- package/docs/ADR-036-contract-obligation-declarations.md +124 -0
- package/docs/ADR-037-selected-contract-compatibility.md +93 -0
- package/docs/ADR-038-monorepo-code-coverage.md +105 -0
- package/docs/ADR-039-mapping-proposals.md +119 -0
- package/docs/ADR-040-bounded-graph-io.md +90 -0
- package/docs/GRAPHIFY-EVALUATION.md +21 -4
- package/docs/PRE-1.0-BASELINE.md +49 -2
- package/docs/RELEASE-1.0-PLAN.md +14 -2
- package/docs/RELEASING.md +3 -3
- package/docs/ROADMAP.md +27 -6
- package/docs/VALIDATION.md +521 -14
- package/docs/benchmarks/large-graph-2026-11-02/result.json +156 -0
- package/docs/extraction-manifest.json +266 -23
- package/package.json +10 -4
- package/templates/context-graph-development/SKILL.md +30 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { parseGraphNeighborsRequest } from './neighborhood-request.js';
|
|
2
|
+
const fields = new Set(['snapshotId', 'fromNodeId', 'toNodeId', 'direction', 'predicates', 'maxDepth', 'maxPaths', 'maxSteps', 'continuation']);
|
|
3
|
+
export function parseGraphPathsRequest(value) {
|
|
4
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
5
|
+
throw new Error('graph.paths arguments must be an object');
|
|
6
|
+
const record = value;
|
|
7
|
+
if (Object.keys(record).some(key => !fields.has(key)))
|
|
8
|
+
throw new Error('Unknown graph.paths field');
|
|
9
|
+
const common = { snapshotId: record.snapshotId, direction: record.direction === undefined ? 'outgoing' : record.direction,
|
|
10
|
+
predicates: record.predicates, continuation: record.continuation };
|
|
11
|
+
const from = parseGraphNeighborsRequest({ ...common, nodeId: record.fromNodeId });
|
|
12
|
+
const to = parseGraphNeighborsRequest({ ...common, nodeId: record.toNodeId });
|
|
13
|
+
const bound = (name, fallback, maximum) => {
|
|
14
|
+
const item = record[name] === undefined ? fallback : record[name];
|
|
15
|
+
if (!Number.isInteger(item) || Number(item) < 1 || Number(item) > maximum) {
|
|
16
|
+
throw new Error(`${name} must be an integer between 1 and ${maximum}`);
|
|
17
|
+
}
|
|
18
|
+
return Number(item);
|
|
19
|
+
};
|
|
20
|
+
return { snapshotId: from.snapshotId, fromNodeId: from.nodeId, toNodeId: to.nodeId,
|
|
21
|
+
direction: from.direction, predicates: from.predicates, maxDepth: bound('maxDepth', 8, 32),
|
|
22
|
+
maxPaths: bound('maxPaths', 5, 20), maxSteps: bound('maxSteps', 1000, 10_000),
|
|
23
|
+
...(from.continuation ? { continuation: from.continuation } : {}) };
|
|
24
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { EvidencePath } from '@weftlet/core';
|
|
2
|
+
export interface PathEdge {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly from: string;
|
|
5
|
+
readonly to: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PathSearchOptions {
|
|
8
|
+
readonly maxDepth: number;
|
|
9
|
+
readonly maxPaths: number;
|
|
10
|
+
readonly maxSteps: number;
|
|
11
|
+
readonly offset?: number;
|
|
12
|
+
}
|
|
13
|
+
/** Pure, dependency-free search shared verbatim with the offline viewer. No global visited set:
|
|
14
|
+
* distinct simple paths and parallel assertions survive; cycles do not repeat a node. */
|
|
15
|
+
export declare function searchEvidencePaths(edges: readonly PathEdge[], start: string, target: string, options: PathSearchOptions, knownNodeIds?: ReadonlySet<string>): {
|
|
16
|
+
paths: EvidencePath[];
|
|
17
|
+
frontier: {
|
|
18
|
+
pending: {
|
|
19
|
+
nodeId: string;
|
|
20
|
+
viaFactIds: string[];
|
|
21
|
+
nextFactId: string;
|
|
22
|
+
remainingCandidates: number;
|
|
23
|
+
}[];
|
|
24
|
+
depthLimited: {
|
|
25
|
+
count: number;
|
|
26
|
+
examples: {
|
|
27
|
+
readonly nodeId: string;
|
|
28
|
+
readonly viaFactIds: readonly string[];
|
|
29
|
+
readonly nextFactId: string;
|
|
30
|
+
readonly nextNodeId: string;
|
|
31
|
+
}[];
|
|
32
|
+
omitted: number;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
search: {
|
|
36
|
+
searchLimit: number;
|
|
37
|
+
replayedSteps: number;
|
|
38
|
+
examinedSteps: number;
|
|
39
|
+
totalSteps: number;
|
|
40
|
+
stopReason: "exhausted" | "search-limit" | "path-page-limit" | "work-page-limit";
|
|
41
|
+
exhaustedWithinDepth: boolean;
|
|
42
|
+
depthLimitReached: boolean;
|
|
43
|
+
truncated: boolean;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** Pure, dependency-free search shared verbatim with the offline viewer. No global visited set:
|
|
2
|
+
* distinct simple paths and parallel assertions survive; cycles do not repeat a node. */
|
|
3
|
+
export function searchEvidencePaths(edges, start, target, options, knownNodeIds) {
|
|
4
|
+
const searchLimit = 100_000;
|
|
5
|
+
const offset = options.offset ?? 0;
|
|
6
|
+
const validateBounds = () => {
|
|
7
|
+
for (const [value, maximum] of [[options.maxDepth, 32], [options.maxPaths, 20], [options.maxSteps, 10_000]]) {
|
|
8
|
+
if (!Number.isInteger(value) || value < 1 || value > maximum)
|
|
9
|
+
throw new Error('Invalid path search bound');
|
|
10
|
+
}
|
|
11
|
+
if (!Number.isInteger(offset) || offset < 0 || offset >= searchLimit)
|
|
12
|
+
throw new Error('Invalid path continuation offset');
|
|
13
|
+
};
|
|
14
|
+
validateBounds();
|
|
15
|
+
const adjacency = new Map();
|
|
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)) {
|
|
18
|
+
const entries = adjacency.get(edge.from) ?? [];
|
|
19
|
+
entries.push(edge);
|
|
20
|
+
adjacency.set(edge.from, entries);
|
|
21
|
+
nodeIds.add(edge.from);
|
|
22
|
+
nodeIds.add(edge.to);
|
|
23
|
+
}
|
|
24
|
+
const paths = [];
|
|
25
|
+
const stack = nodeIds.has(start) && nodeIds.has(target) && start !== target ? [{ nodeId: start, next: 0 }] : [];
|
|
26
|
+
const depthExamples = [];
|
|
27
|
+
let depthCount = 0;
|
|
28
|
+
let totalSteps = 0;
|
|
29
|
+
const trim = () => {
|
|
30
|
+
while (stack.length && stack.at(-1).next >= (adjacency.get(stack.at(-1).nodeId)?.length ?? 0))
|
|
31
|
+
stack.pop();
|
|
32
|
+
};
|
|
33
|
+
const step = () => {
|
|
34
|
+
const frame = stack.at(-1);
|
|
35
|
+
const edge = adjacency.get(frame.nodeId)[frame.next++];
|
|
36
|
+
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) {
|
|
41
|
+
depthCount++;
|
|
42
|
+
if (depthExamples.length < 20)
|
|
43
|
+
depthExamples.push({ nodeId: frame.nodeId, viaFactIds,
|
|
44
|
+
nextFactId: edge.id, nextNodeId: edge.to });
|
|
45
|
+
}
|
|
46
|
+
else if (edge.to === target) {
|
|
47
|
+
return { nodeIds: [...stack.map(item => item.nodeId), target], factIds: [...viaFactIds, edge.id] };
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
stack.push({ nodeId: edge.to, next: 0, via: edge.id });
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
trim();
|
|
54
|
+
// Reconstruct only bounded search state. Never accept a caller-supplied stack or facts.
|
|
55
|
+
const replay = () => {
|
|
56
|
+
let replayPageSteps = 0;
|
|
57
|
+
let replayPagePaths = 0;
|
|
58
|
+
while (totalSteps < offset && stack.length) {
|
|
59
|
+
if (step())
|
|
60
|
+
replayPagePaths++;
|
|
61
|
+
replayPageSteps++;
|
|
62
|
+
if (replayPagePaths === options.maxPaths || replayPageSteps === options.maxSteps) {
|
|
63
|
+
replayPagePaths = 0;
|
|
64
|
+
replayPageSteps = 0;
|
|
65
|
+
}
|
|
66
|
+
trim();
|
|
67
|
+
}
|
|
68
|
+
if (totalSteps !== offset || (offset && (!stack.length || replayPagePaths || replayPageSteps))) {
|
|
69
|
+
throw new Error('Invalid path continuation boundary');
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
replay();
|
|
73
|
+
if (!offset && start === target && (nodeIds.has(start) || knownNodeIds?.has(start)))
|
|
74
|
+
paths.push({ nodeIds: [start], factIds: [] });
|
|
75
|
+
while (stack.length && paths.length < options.maxPaths && totalSteps - offset < options.maxSteps && totalSteps < searchLimit) {
|
|
76
|
+
const path = step();
|
|
77
|
+
if (path)
|
|
78
|
+
paths.push(path);
|
|
79
|
+
trim();
|
|
80
|
+
}
|
|
81
|
+
const pending = stack.flatMap((frame, index) => {
|
|
82
|
+
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;
|
|
88
|
+
const stopReason = () => {
|
|
89
|
+
if (exhaustedWithinDepth)
|
|
90
|
+
return 'exhausted';
|
|
91
|
+
if (totalSteps === searchLimit)
|
|
92
|
+
return 'search-limit';
|
|
93
|
+
if (paths.length === options.maxPaths)
|
|
94
|
+
return 'path-page-limit';
|
|
95
|
+
return 'work-page-limit';
|
|
96
|
+
};
|
|
97
|
+
return { paths, frontier: { pending, depthLimited: { count: depthCount, examples: depthExamples,
|
|
98
|
+
omitted: depthCount - depthExamples.length } }, search: { searchLimit, replayedSteps: offset,
|
|
99
|
+
examinedSteps: totalSteps - offset, totalSteps, stopReason: stopReason(), exhaustedWithinDepth,
|
|
100
|
+
depthLimitReached: depthCount > 0, truncated: !exhaustedWithinDepth || depthCount > 0 } };
|
|
101
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { factEvidenceDescription } from './evidence-description.js';
|
|
2
|
+
import { sourceLocation } from '@weftlet/core';
|
|
3
|
+
import { contextPackEvidenceAuthority } from './authority.js';
|
|
4
|
+
import { coverageDescription } from './coverage.js';
|
|
5
|
+
import { diagnosticSummary } from './diagnostics.js';
|
|
6
|
+
import { documentSelectionDescription } from './document-role-description.js';
|
|
7
|
+
const quoted = (value) => JSON.stringify(value).replace(/[\\`*_{}\[\]()<>!|]/g, '\\$&');
|
|
8
|
+
export function renderGraphPaths(result) {
|
|
9
|
+
const { snapshot, endpoints, search, frontier, evidenceState } = result;
|
|
10
|
+
return [
|
|
11
|
+
'# Cited evidence paths',
|
|
12
|
+
`Snapshot: ${quoted(snapshot.snapshotId)} (${snapshot.commitSha}); configuration: ${snapshot.configurationDigest ?? 'unknown'}`,
|
|
13
|
+
`Scope: ${snapshot.scope?.map(quoted).join(', ') ?? 'unknown'}`,
|
|
14
|
+
documentSelectionDescription(snapshot.documentSelection),
|
|
15
|
+
`From ${quoted(endpoints.from.nodeId)} (${endpoints.from.status}) to ${quoted(endpoints.to.nodeId)} (${endpoints.to.status})`,
|
|
16
|
+
`Direction: ${search.direction}; predicates: ${search.predicates.join(', ') || 'all'}; order: ${search.order}`,
|
|
17
|
+
`Depth: ${search.maxDepth}; paths on page: ${result.paths.length}/${search.maxPaths}; stop: ${search.stopReason}; truncated: ${search.truncated}`,
|
|
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).`,
|
|
20
|
+
...(search.continuation ? [`Continuation (same request): ${search.continuation}`] : []),
|
|
21
|
+
...(search.depthLimitReached ? ['Depth limit reached: restart with a larger maxDepth or explore a frontier node. Continuation does not lift the depth bound.'] : []),
|
|
22
|
+
...(search.stopReason === 'search-limit' ? ['Total search limit reached: narrow the relations or explore an intermediate node; no further continuation is available.'] : []),
|
|
23
|
+
...(evidenceState ? [`Stale: ${evidenceState.stale}; configuration matches: ${evidenceState.configurationMatches}; extraction compatible: ${evidenceState.extractionCompatible ?? 'unknown'}; overlay freshness: ${evidenceState.overlayFreshness ?? 'not an overlay'}`,
|
|
24
|
+
`Excluded working changes: ${evidenceState.excludedWorkingChanges.map(quoted).join(', ') || '(none)'}`] : []),
|
|
25
|
+
'Paths are cited associations, not proof of causality, business compatibility or current-diff validation. A completed search covers only the selected saved facts and depth; absence is not proof of no product dependency.',
|
|
26
|
+
coverageDescription(result.coverage), ...result.coverage.gaps.map(quoted),
|
|
27
|
+
'', '## Paths',
|
|
28
|
+
...result.paths.map((path, index) => `${index + 1}. ${path.nodeIds.map(quoted).join(' → ')}; facts: ${path.factIds.map(quoted).join(', ') || '(zero-length identity path)'}`),
|
|
29
|
+
'', '## Supporting assertions',
|
|
30
|
+
...result.facts.map(fact => `- ${quoted(fact.id)} [${contextPackEvidenceAuthority(fact)}] ${quoted(fact.subject.id)} —${fact.predicate}→ ${quoted(fact.object.id)} (${quoted(sourceLocation(fact.provenance))}–${fact.provenance.endLine}${fact.provenance.endColumn === undefined ? '' : `:${fact.provenance.endColumn}`}; confidence ${fact.confidence}; source ${fact.sourceAuthority ?? 'unspecified'}; extraction ${fact.extractionReview ?? 'unspecified'}${factEvidenceDescription(fact) ? `; ${factEvidenceDescription(fact)}` : ''})`),
|
|
31
|
+
'', '## Unexplored frontier',
|
|
32
|
+
...frontier.pending.map(item => `- ${quoted(item.nodeId)} via ${item.viaFactIds.map(quoted).join(', ') || '(start)'}: ${item.remainingCandidates} candidates; next ${quoted(item.nextFactId)}`),
|
|
33
|
+
...frontier.depthLimited.examples.map(item => `- Depth boundary: ${quoted(item.nodeId)} → ${quoted(item.nextNodeId)} via ${quoted(item.nextFactId)}`),
|
|
34
|
+
'', '## Scoped diagnostics', ...diagnosticSummary(result).map(quoted),
|
|
35
|
+
].filter(line => line !== undefined).join('\n');
|
|
36
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GraphPathQueryService, GraphPaths, GraphPathsRequest, SnapshotStore } from '@weftlet/core';
|
|
2
|
+
export declare const GRAPH_PATHS_VERSION = "simple-evidence-paths-v1";
|
|
3
|
+
export declare class SnapshotGraphPathService implements GraphPathQueryService {
|
|
4
|
+
private readonly store;
|
|
5
|
+
constructor(store: Pick<SnapshotStore, 'read'>);
|
|
6
|
+
paths(input: GraphPathsRequest): Promise<GraphPaths>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { snapshotContext } from './snapshot-context.js';
|
|
2
|
+
import { stableJsonDigest } from '../infrastructure/util.js';
|
|
3
|
+
import { RdfEvidenceGraph } from './rdf.js';
|
|
4
|
+
import { coverageForFacts } from './coverage.js';
|
|
5
|
+
import { selectDiagnostics } from './diagnostics.js';
|
|
6
|
+
import { parseGraphPathsRequest } from './path-request.js';
|
|
7
|
+
import { searchEvidencePaths } from './path-search.js';
|
|
8
|
+
import { renderGraphPaths } from './paths-renderer.js';
|
|
9
|
+
export const GRAPH_PATHS_VERSION = 'simple-evidence-paths-v1';
|
|
10
|
+
function continuationOffset(token, binding) {
|
|
11
|
+
if (!token)
|
|
12
|
+
return 0;
|
|
13
|
+
try {
|
|
14
|
+
if (!/^[A-Za-z0-9_-]+$/.test(token))
|
|
15
|
+
throw new Error();
|
|
16
|
+
const value = JSON.parse(Buffer.from(token, 'base64url').toString('utf8'));
|
|
17
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) ||
|
|
18
|
+
Object.keys(value).sort().join(',') !== 'binding,offset,version' ||
|
|
19
|
+
value.version !== GRAPH_PATHS_VERSION || value.binding !== binding ||
|
|
20
|
+
!Number.isInteger(value.offset) || value.offset < 1 || value.offset >= 100_000)
|
|
21
|
+
throw new Error();
|
|
22
|
+
return value.offset;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error('Invalid graph.paths continuation or query/snapshot mismatch; restart graph.paths');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class SnapshotGraphPathService {
|
|
29
|
+
store;
|
|
30
|
+
constructor(store) {
|
|
31
|
+
this.store = store;
|
|
32
|
+
}
|
|
33
|
+
async paths(input) {
|
|
34
|
+
const request = parseGraphPathsRequest(input);
|
|
35
|
+
const saved = await this.store.read(request.snapshotId);
|
|
36
|
+
if (!saved)
|
|
37
|
+
throw new Error('No graph snapshot is available for that identifier');
|
|
38
|
+
if (saved.manifest.snapshotId !== request.snapshotId)
|
|
39
|
+
throw new Error('Saved snapshot identity does not match the requested identifier');
|
|
40
|
+
const all = await new RdfEvidenceGraph(saved).facts();
|
|
41
|
+
if (new Set(all.map(fact => fact.id)).size !== all.length)
|
|
42
|
+
throw new Error('Duplicate fact identities prevent stable graph.paths pagination');
|
|
43
|
+
const { continuation, ...query } = request;
|
|
44
|
+
const binding = stableJsonDigest({ version: GRAPH_PATHS_VERSION, query, manifest: saved.manifest,
|
|
45
|
+
facts: all, diagnostics: saved.diagnostics, unresolved: saved.unresolved });
|
|
46
|
+
const offset = continuationOffset(continuation, binding);
|
|
47
|
+
const selected = all.filter(fact => !request.predicates.length || request.predicates.includes(fact.predicate));
|
|
48
|
+
const edges = selected.flatMap(fact => {
|
|
49
|
+
const forward = { id: fact.id, from: fact.subject.id, to: fact.object.id };
|
|
50
|
+
const reverse = { id: fact.id, from: fact.object.id, to: fact.subject.id };
|
|
51
|
+
return request.direction === 'incoming' ? [reverse] : request.direction === 'outgoing' || forward.from === forward.to
|
|
52
|
+
? [forward] : [forward, reverse];
|
|
53
|
+
});
|
|
54
|
+
const nodes = new Set(all.flatMap(fact => [fact.subject.id, fact.object.id]));
|
|
55
|
+
const result = searchEvidencePaths(edges, request.fromNodeId, request.toNodeId, { ...request, offset }, nodes);
|
|
56
|
+
const factIds = new Set(result.paths.flatMap(path => path.factIds));
|
|
57
|
+
const facts = all.filter(fact => factIds.has(fact.id));
|
|
58
|
+
const endpoint = (nodeId) => ({ nodeId, status: nodes.has(nodeId) ? 'present' : 'absent-from-snapshot' });
|
|
59
|
+
const next = !result.search.exhaustedWithinDepth && result.search.stopReason !== 'search-limit'
|
|
60
|
+
? Buffer.from(JSON.stringify({ version: GRAPH_PATHS_VERSION, binding, offset: result.search.totalSteps })).toString('base64url') : undefined;
|
|
61
|
+
const manifest = saved.manifest;
|
|
62
|
+
const plain = {
|
|
63
|
+
snapshot: snapshotContext(manifest),
|
|
64
|
+
endpoints: { from: endpoint(request.fromNodeId), to: endpoint(request.toNodeId) },
|
|
65
|
+
paths: result.paths, facts, coverage: coverageForFacts(facts, saved),
|
|
66
|
+
diagnostics: selectDiagnostics(saved, {}, facts, [], [], new Set([request.fromNodeId, request.toNodeId,
|
|
67
|
+
...facts.flatMap(fact => [fact.subject.id, fact.object.id])])),
|
|
68
|
+
search: { version: GRAPH_PATHS_VERSION, order: 'fact-id-depth-first', direction: request.direction,
|
|
69
|
+
predicates: request.predicates, maxDepth: request.maxDepth, maxPaths: request.maxPaths, maxSteps: request.maxSteps,
|
|
70
|
+
...result.search, ...(next ? { continuation: next } : {}) }, frontier: result.frontier,
|
|
71
|
+
};
|
|
72
|
+
return { ...plain, markdown: renderGraphPaths(plain) };
|
|
73
|
+
}
|
|
74
|
+
}
|
package/dist/graph/query.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
// Modified for standalone extraction; see docs/extraction-manifest.json.
|
|
2
|
+
import { unexpandedCodeDescription } from './code-description.js';
|
|
3
|
+
import { factEvidenceDescription } from './evidence-description.js';
|
|
4
|
+
import { sourceLocation } from '@weftlet/core';
|
|
2
5
|
import { contextPackEvidenceAuthority } from "./authority.js";
|
|
3
6
|
import { RdfEvidenceGraph } from "./rdf.js";
|
|
4
7
|
import { sha256, stableJson } from "../infrastructure/util.js";
|
|
@@ -61,7 +64,8 @@ function markdownFactDetails(fact) {
|
|
|
61
64
|
const document = sourceDocumentDescription(fact)?.replace(/[\\`*_{}\[\]()<>!|]/g, '\\$&').replace(/[\r\n]/g, ' ');
|
|
62
65
|
return `confidence ${fact.confidence}` +
|
|
63
66
|
(fact.sourceAuthority ? `; source ${fact.sourceAuthority}; extraction ${fact.extractionReview ?? 'unknown'}` : '') +
|
|
64
|
-
(document ? `; ${document}` : '')
|
|
67
|
+
(document ? `; ${document}` : '') +
|
|
68
|
+
(factEvidenceDescription(fact) ? `; ${factEvidenceDescription(fact)}` : '');
|
|
65
69
|
}
|
|
66
70
|
export class MarkdownContextPackRenderer {
|
|
67
71
|
render(pack) {
|
|
@@ -106,7 +110,7 @@ export class MarkdownContextPackRenderer {
|
|
|
106
110
|
...pack.facts
|
|
107
111
|
.filter((fact) => contextPackLaneForFact(fact) === lane)
|
|
108
112
|
.map((fact) => `- [${contextPackEvidenceAuthority(fact)}] ${fact.subject.label} —${fact.predicate}→ ${fact.object.label} ` +
|
|
109
|
-
`(\`${fact.provenance
|
|
113
|
+
`(\`${sourceLocation(fact.provenance)}\`, ${markdownFactDetails(fact)})`),
|
|
110
114
|
]),
|
|
111
115
|
'',
|
|
112
116
|
'## Runtime evidence',
|
|
@@ -122,6 +126,7 @@ export class MarkdownContextPackRenderer {
|
|
|
122
126
|
'## Suggested files',
|
|
123
127
|
...pack.suggestedFiles.map((path) => `- \`${path}\``),
|
|
124
128
|
'',
|
|
129
|
+
...unexpandedCodeDescription(pack.exploration),
|
|
125
130
|
'## Scoped diagnostics',
|
|
126
131
|
...diagnosticSummary(pack).map((item) => `- ${item}`),
|
|
127
132
|
];
|
|
@@ -165,8 +170,16 @@ export class SnapshotImpactQueryService {
|
|
|
165
170
|
const directTerms = anchorTerms.length > 0 ? anchorTerms : focus;
|
|
166
171
|
const direct = snapshot.facts.filter((fact) => this.matches(fact, directTerms));
|
|
167
172
|
const ranked = new Map(direct.map((fact) => [fact.id, fact]));
|
|
173
|
+
const unexpandedCodeTargets = new Set();
|
|
174
|
+
const expand = (node) => {
|
|
175
|
+
if (node.kind === 'CodeSymbol' && node.codeSymbol?.resolution === 'no-indexed-definition') {
|
|
176
|
+
unexpandedCodeTargets.add(node.id);
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
return node.kind !== 'Project';
|
|
180
|
+
};
|
|
168
181
|
let frontier = new Set(direct.flatMap((fact) => [fact.subject, fact.object]
|
|
169
|
-
.filter(
|
|
182
|
+
.filter(expand)
|
|
170
183
|
.map((node) => node.id)));
|
|
171
184
|
const visitedNodes = new Set(frontier);
|
|
172
185
|
for (let depth = 0; depth < maxDepth && frontier.size > 0; depth += 1) {
|
|
@@ -176,7 +189,7 @@ export class SnapshotImpactQueryService {
|
|
|
176
189
|
continue;
|
|
177
190
|
ranked.set(fact.id, fact);
|
|
178
191
|
for (const node of [fact.subject, fact.object]) {
|
|
179
|
-
if (node
|
|
192
|
+
if (expand(node) && !visitedNodes.has(node.id))
|
|
180
193
|
next.add(node.id);
|
|
181
194
|
}
|
|
182
195
|
}
|
|
@@ -196,7 +209,7 @@ export class SnapshotImpactQueryService {
|
|
|
196
209
|
...tier.filter((fact) => contextPackLaneForFact(fact) === undefined),
|
|
197
210
|
]);
|
|
198
211
|
const { continuation: _continuation, snapshotId: _snapshotId, ...query } = request;
|
|
199
|
-
const queryDigest = sha256(stableJson({ ranking: IMPACT_RANKING_VERSION, query }));
|
|
212
|
+
const queryDigest = sha256(stableJson({ ranking: IMPACT_RANKING_VERSION, codeTraversal: 'indexed-symbols-v1', query }));
|
|
200
213
|
let offset = 0;
|
|
201
214
|
if (request.continuation) {
|
|
202
215
|
if (request.continuation.length > 2048)
|
|
@@ -215,7 +228,7 @@ export class SnapshotImpactQueryService {
|
|
|
215
228
|
offset = token.offset;
|
|
216
229
|
}
|
|
217
230
|
const facts = allBalanced.slice(offset, offset + maxFacts);
|
|
218
|
-
const coverage = coverageForFacts(facts);
|
|
231
|
+
const coverage = coverageForFacts(facts, snapshot);
|
|
219
232
|
const nextOffset = offset + facts.length;
|
|
220
233
|
const continuation = nextOffset < allBalanced.length ? Buffer.from(JSON.stringify({
|
|
221
234
|
snapshot: snapshot.manifest.snapshotId, digest: snapshot.manifest.contentDigest,
|
|
@@ -306,6 +319,10 @@ export class SnapshotImpactQueryService {
|
|
|
306
319
|
diagnostics: selectDiagnostics(snapshot, request, facts, focus, queryDiagnostics),
|
|
307
320
|
exploration: {
|
|
308
321
|
rankingVersion: IMPACT_RANKING_VERSION,
|
|
322
|
+
codeTraversalVersion: 'indexed-symbols-v1',
|
|
323
|
+
unexpandedCodeTargets: [...unexpandedCodeTargets].sort().slice(0, 100),
|
|
324
|
+
unexpandedCodeTargetCount: unexpandedCodeTargets.size,
|
|
325
|
+
unexpandedCodeTargetsTruncated: unexpandedCodeTargets.size > 100,
|
|
309
326
|
returned: facts.length, reachableFacts: allBalanced.length, maxDepth,
|
|
310
327
|
depthLimitReached: frontier.size > 0, truncated: Boolean(continuation) || frontier.size > 0,
|
|
311
328
|
continuation,
|
|
@@ -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 {};
|