@weftlet/toolkit 0.7.0 → 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.
Files changed (37) hide show
  1. package/README.md +32 -26
  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/semantic-run.d.ts +1 -1
  6. package/dist/extraction/ontology.d.ts +2 -0
  7. package/dist/extraction/ontology.js +28 -13
  8. package/dist/graph/code-coverage.js +2 -2
  9. package/dist/graph/code-rdf.d.ts +3 -2
  10. package/dist/graph/code-rdf.js +21 -14
  11. package/dist/graph/neighborhood.js +3 -3
  12. package/dist/graph/partitioned-shacl.d.ts +14 -0
  13. package/dist/graph/partitioned-shacl.js +66 -0
  14. package/dist/graph/paths.js +3 -3
  15. package/dist/graph/rdf-partitions.d.ts +24 -0
  16. package/dist/graph/rdf-partitions.js +170 -0
  17. package/dist/graph/rdf-projection.d.ts +7 -0
  18. package/dist/graph/rdf-projection.js +27 -0
  19. package/dist/graph/rdf-records.d.ts +12 -0
  20. package/dist/graph/rdf-records.js +90 -0
  21. package/dist/graph/rdf.d.ts +10 -10
  22. package/dist/graph/rdf.js +66 -97
  23. package/dist/graph/validation-progress.d.ts +4 -1
  24. package/dist/infrastructure/json-digest.d.ts +2 -1
  25. package/dist/infrastructure/json-digest.js +2 -2
  26. package/dist/interfaces/cli.js +1 -1
  27. package/dist/storage/storage.d.ts +3 -0
  28. package/dist/storage/storage.js +37 -13
  29. package/dist/storage/streaming-json.d.ts +5 -0
  30. package/dist/storage/streaming-json.js +45 -0
  31. package/docs/1.0-WORK-QUEUE.md +18 -8
  32. package/docs/ADR-040-bounded-graph-io.md +90 -0
  33. package/docs/RELEASING.md +3 -3
  34. package/docs/VALIDATION.md +84 -0
  35. package/docs/benchmarks/large-graph-2026-11-02/result.json +156 -0
  36. package/docs/extraction-manifest.json +26 -10
  37. package/package.json +4 -2
@@ -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 {};
@@ -0,0 +1,90 @@
1
+ import { DataFactory, Store } from 'n3';
2
+ import { contextPackEvidenceAuthority } from './authority.js';
3
+ import { stableJson } from '../infrastructure/util.js';
4
+ import { CG, RDF, XSD } from './shacl-shapes.js';
5
+ import { addBindingEvidence } from './binding-rdf.js';
6
+ import { addCallEvidence } from './call-rdf.js';
7
+ import { addObligationEvidence } from './obligation-rdf.js';
8
+ import { addCodeEvidence, addCodeNodeEvidence } from './code-rdf.js';
9
+ const { namedNode: iri, literal } = DataFactory;
10
+ export const nodeIri = (id) => `urn:context-graph:node:${encodeURIComponent(id)}`;
11
+ export const factIri = (id) => `urn:context-graph:fact:${encodeURIComponent(id)}`;
12
+ export function addFactRecord(dataset, fact, ordinal) {
13
+ const record = iri(factIri(fact.id));
14
+ const subject = iri(nodeIri(fact.subject.id));
15
+ const object = iri(nodeIri(fact.object.id));
16
+ const predicate = iri(CG + fact.predicate);
17
+ dataset.addQuad(record, iri(RDF + 'type'), iri(RDF + 'Statement'));
18
+ dataset.addQuad(record, iri(CG + 'ordinal'), literal(String(ordinal), iri(XSD + 'integer')));
19
+ for (const [p, value] of [['subject', subject], ['predicate', predicate], ['object', object]]) {
20
+ dataset.addQuad(record, iri(RDF + p), value);
21
+ }
22
+ for (const [p, value] of Object.entries({
23
+ authority: contextPackEvidenceAuthority(fact), path: fact.provenance.path,
24
+ commit: fact.provenance.commitSha, sourceDigest: fact.provenance.digest,
25
+ extractor: `${fact.extractor.id}@${fact.extractor.version}`,
26
+ snapshotId: fact.snapshotId, record: stableJson(fact),
27
+ ...(fact.sourceAuthority ? { sourceAuthority: fact.sourceAuthority } : {}),
28
+ ...(fact.extractionReview ? { extractionReview: fact.extractionReview } : {}),
29
+ ...(fact.sourceRole !== undefined ? { sourceRole: fact.sourceRole } : {}),
30
+ ...(fact.sourcePolicyDigest !== undefined ? { sourcePolicyDigest: fact.sourcePolicyDigest } : {}),
31
+ }))
32
+ dataset.addQuad(record, iri(CG + p), literal(value));
33
+ if (fact.sourceOwnership !== undefined) {
34
+ dataset.addQuad(record, iri(CG + 'ownershipStatus'), literal(fact.sourceOwnership.status));
35
+ for (const ownerDocument of fact.sourceOwnership.ownerDocuments) {
36
+ dataset.addQuad(record, iri(CG + 'ownerDocument'), literal(ownerDocument));
37
+ }
38
+ for (const mappingId of fact.sourceOwnership.mappingIds) {
39
+ dataset.addQuad(record, iri(CG + 'ownershipMapping'), literal(mappingId));
40
+ }
41
+ }
42
+ for (const [p, value] of [['startLine', fact.provenance.startLine], ['endLine', fact.provenance.endLine]]) {
43
+ dataset.addQuad(record, iri(CG + p), literal(String(value), iri(XSD + 'integer')));
44
+ }
45
+ dataset.addQuad(record, iri(CG + 'confidence'), literal(String(fact.confidence), iri(XSD + 'decimal')));
46
+ addCodeEvidence(dataset, record, fact, nodeIri, false);
47
+ addBindingEvidence(dataset, record, fact, factIri);
48
+ addCallEvidence(dataset, record, fact, factIri);
49
+ addObligationEvidence(dataset, record, fact);
50
+ }
51
+ export function addNodeRdf(dataset, node) {
52
+ const term = iri(nodeIri(node.id));
53
+ dataset.addQuad(term, iri(RDF + 'type'), iri(CG + 'Node'));
54
+ dataset.addQuad(term, iri(CG + 'identifier'), literal(node.id));
55
+ dataset.addQuad(term, iri(CG + 'kind'), literal(node.kind));
56
+ dataset.addQuad(term, iri(CG + 'label'), literal(node.label));
57
+ addCodeNodeEvidence(dataset, node, nodeIri);
58
+ }
59
+ export function addAttributes(dataset, node, graph = DataFactory.defaultGraph()) {
60
+ for (const [key, value] of Object.entries(node.attributes ?? {})) {
61
+ const datatype = typeof value === 'boolean' ? 'boolean' : typeof value === 'number' ? 'decimal' : 'string';
62
+ dataset.addQuad(iri(nodeIri(node.id)), iri(`${CG}attribute/${encodeURIComponent(key)}`), literal(String(value), iri(XSD + datatype)), graph);
63
+ }
64
+ }
65
+ export function addFactEdges(dataset, fact) {
66
+ const record = iri(factIri(fact.id));
67
+ dataset.addQuad(iri(nodeIri(fact.subject.id)), iri(CG + fact.predicate), iri(nodeIri(fact.object.id)), record);
68
+ for (const node of [fact.subject, fact.object])
69
+ addAttributes(dataset, node, record);
70
+ }
71
+ export function addManifest(dataset, snapshot) {
72
+ dataset.addQuad(iri('urn:context-graph:snapshot'), iri(CG + 'manifest'), literal(stableJson(snapshot.manifest)));
73
+ for (const scope of snapshot.manifest.scope ?? []) {
74
+ dataset.addQuad(iri('urn:context-graph:snapshot'), iri(CG + 'scope'), literal(scope));
75
+ }
76
+ if (snapshot.manifest.configurationDigest) {
77
+ dataset.addQuad(iri('urn:context-graph:snapshot'), iri(CG + 'configurationDigest'), literal(snapshot.manifest.configurationDigest));
78
+ }
79
+ }
80
+ export function evidenceDataset(snapshot) {
81
+ const dataset = new Store();
82
+ for (const [ordinal, fact] of snapshot.facts.entries()) {
83
+ addFactRecord(dataset, fact, ordinal);
84
+ addFactEdges(dataset, fact);
85
+ for (const node of [fact.subject, fact.object])
86
+ addNodeRdf(dataset, node);
87
+ }
88
+ addManifest(dataset, snapshot);
89
+ return dataset;
90
+ }
@@ -1,29 +1,29 @@
1
+ import type { Writable } from 'node:stream';
1
2
  import type { GraphFact, GraphSnapshot } from "@weftlet/core";
2
3
  import { type GraphValidationProgress } from './validation-progress.js';
3
4
  export { CG } from './shacl-shapes.js';
4
- export declare const nodeIri: (id: string) => string;
5
- export declare const factIri: (id: string) => string;
5
+ export { factIri, nodeIri } from './rdf-records.js';
6
6
  /** Lossless RDF dataset: one named graph per cited assertion, with provenance in the default graph. */
7
7
  export declare class RdfEvidenceGraph {
8
8
  readonly snapshot: GraphSnapshot;
9
- private readonly dataset;
9
+ private indexed?;
10
10
  constructor(snapshot: GraphSnapshot);
11
11
  validate(options?: {
12
12
  readonly onProgress?: (event: GraphValidationProgress) => void;
13
13
  }): Promise<{
14
14
  conforms: boolean;
15
- issues: {
16
- category: "extraction-gap";
17
- focus: string;
18
- path: string;
19
- message: string;
20
- }[];
15
+ issues: import("./partitioned-shacl.js").ShaclIssue[];
21
16
  }>;
22
17
  select(sparql: string, maxRows?: number): Promise<{
23
18
  rows: Record<string, string>[];
24
19
  truncated: boolean;
25
20
  }>;
26
- /** The traversal projection is read through SPARQL, not a separate fact source. */
21
+ /** Lossless optimized projection, checked for equivalence with the public SPARQL path. */
27
22
  facts(): Promise<readonly GraphFact[]>;
23
+ /** Complete N-Quads in bounded groups; RDF triple order is not a snapshot identity. */
24
+ serializeChunks(): IterableIterator<string>;
25
+ /** Export large datasets with backpressure, without a whole-dataset string/quad array. */
26
+ serializeTo(destination: Writable): Promise<void>;
27
+ /** Compatibility convenience for small exports; use serializeTo for large graphs. */
28
28
  serialize(): Promise<string>;
29
29
  }
package/dist/graph/rdf.js CHANGED
@@ -1,105 +1,48 @@
1
1
  import { QueryEngine } from '@comunica/query-sparql';
2
- import { DataFactory, Store, Writer } from 'n3';
3
- import SHACLValidator from 'rdf-validate-shacl';
4
- import { contextPackEvidenceAuthority } from "./authority.js";
5
- import { stableJson } from "../infrastructure/util.js";
2
+ import { constants } from 'node:buffer';
3
+ import { pipeline } from 'node:stream/promises';
4
+ import { Readable } from 'node:stream';
5
+ import { Writer } from 'n3';
6
6
  import { createRequire } from 'node:module';
7
- import { CG, RDF, XSD, evidenceShapes } from './shacl-shapes.js';
7
+ import { CG } from './shacl-shapes.js';
8
8
  import { measureValidationPhase } from './validation-progress.js';
9
- import { addBindingEvidence, bindingEvidenceIssues } from './binding-rdf.js';
10
- import { addCallEvidence, callEvidenceIssues } from './call-rdf.js';
11
- import { addObligationEvidence, obligationEvidenceIssues } from './obligation-rdf.js';
12
- import { addCodeEvidence, codeEvidenceIssues, invalidCodeRange } from './code-rdf.js';
13
- const { namedNode: iri, literal } = DataFactory;
9
+ import { bindingEvidenceIssues } from './binding-rdf.js';
10
+ import { callEvidenceIssues } from './call-rdf.js';
11
+ import { obligationEvidenceIssues } from './obligation-rdf.js';
12
+ import { codeEvidenceIssues, invalidCodeRange } from './code-rdf.js';
14
13
  export { CG } from './shacl-shapes.js';
15
- export const nodeIri = (id) => `urn:context-graph:node:${encodeURIComponent(id)}`;
16
- export const factIri = (id) => `urn:context-graph:fact:${encodeURIComponent(id)}`;
14
+ import { factIri } from './rdf-records.js';
15
+ import { RdfPartitions } from './rdf-partitions.js';
16
+ import { validatePartitions } from './partitioned-shacl.js';
17
+ import { projectRdfFacts } from './rdf-projection.js';
18
+ export { factIri, nodeIri } from './rdf-records.js';
17
19
  const engine = new QueryEngine();
18
20
  const SparqlParser = createRequire(import.meta.url)('sparqljs').Parser;
19
21
  /** Lossless RDF dataset: one named graph per cited assertion, with provenance in the default graph. */
20
22
  export class RdfEvidenceGraph {
21
23
  snapshot;
22
- dataset = new Store();
24
+ indexed;
23
25
  constructor(snapshot) {
24
26
  this.snapshot = snapshot;
25
- for (const [ordinal, fact] of snapshot.facts.entries()) {
26
- const record = iri(factIri(fact.id));
27
- const subject = iri(nodeIri(fact.subject.id));
28
- const object = iri(nodeIri(fact.object.id));
29
- const predicate = iri(CG + fact.predicate);
30
- this.dataset.addQuad(subject, predicate, object, record);
31
- this.dataset.addQuad(record, iri(RDF + 'type'), iri(RDF + 'Statement'));
32
- this.dataset.addQuad(record, iri(CG + 'ordinal'), literal(String(ordinal), iri(XSD + 'integer')));
33
- for (const [p, value] of [['subject', subject], ['predicate', predicate], ['object', object]]) {
34
- this.dataset.addQuad(record, iri(RDF + p), value);
35
- }
36
- for (const [p, value] of Object.entries({
37
- authority: contextPackEvidenceAuthority(fact), path: fact.provenance.path,
38
- commit: fact.provenance.commitSha, sourceDigest: fact.provenance.digest,
39
- extractor: `${fact.extractor.id}@${fact.extractor.version}`,
40
- snapshotId: fact.snapshotId, record: stableJson(fact),
41
- ...(fact.sourceAuthority ? { sourceAuthority: fact.sourceAuthority } : {}),
42
- ...(fact.extractionReview ? { extractionReview: fact.extractionReview } : {}),
43
- ...(fact.sourceRole !== undefined ? { sourceRole: fact.sourceRole } : {}),
44
- ...(fact.sourcePolicyDigest !== undefined ? { sourcePolicyDigest: fact.sourcePolicyDigest } : {}),
45
- }))
46
- this.dataset.addQuad(record, iri(CG + p), literal(value));
47
- if (fact.sourceOwnership !== undefined) {
48
- this.dataset.addQuad(record, iri(CG + 'ownershipStatus'), literal(fact.sourceOwnership.status));
49
- for (const ownerDocument of fact.sourceOwnership.ownerDocuments) {
50
- this.dataset.addQuad(record, iri(CG + 'ownerDocument'), literal(ownerDocument));
51
- }
52
- for (const mappingId of fact.sourceOwnership.mappingIds) {
53
- this.dataset.addQuad(record, iri(CG + 'ownershipMapping'), literal(mappingId));
54
- }
55
- }
56
- for (const [p, value] of [['startLine', fact.provenance.startLine], ['endLine', fact.provenance.endLine]]) {
57
- this.dataset.addQuad(record, iri(CG + p), literal(String(value), iri(XSD + 'integer')));
58
- }
59
- this.dataset.addQuad(record, iri(CG + 'confidence'), literal(String(fact.confidence), iri(XSD + 'decimal')));
60
- addCodeEvidence(this.dataset, record, fact, nodeIri);
61
- addBindingEvidence(this.dataset, record, fact, factIri);
62
- addCallEvidence(this.dataset, record, fact, factIri);
63
- addObligationEvidence(this.dataset, record, fact);
64
- for (const node of [fact.subject, fact.object]) {
65
- const term = iri(nodeIri(node.id));
66
- this.dataset.addQuad(term, iri(RDF + 'type'), iri(CG + 'Node'));
67
- this.dataset.addQuad(term, iri(CG + 'identifier'), literal(node.id));
68
- this.dataset.addQuad(term, iri(CG + 'kind'), literal(node.kind));
69
- this.dataset.addQuad(term, iri(CG + 'label'), literal(node.label));
70
- for (const [key, value] of Object.entries(node.attributes ?? {})) {
71
- const datatype = typeof value === 'boolean' ? 'boolean' : typeof value === 'number' ? 'decimal' : 'string';
72
- this.dataset.addQuad(term, iri(`${CG}attribute/${encodeURIComponent(key)}`), literal(String(value), iri(XSD + datatype)), record);
73
- }
74
- }
75
- }
76
- this.dataset.addQuad(iri('urn:context-graph:snapshot'), iri(CG + 'manifest'), literal(stableJson(snapshot.manifest)));
77
- for (const scope of snapshot.manifest.scope ?? []) {
78
- this.dataset.addQuad(iri('urn:context-graph:snapshot'), iri(CG + 'scope'), literal(scope));
79
- }
80
- if (snapshot.manifest.configurationDigest) {
81
- this.dataset.addQuad(iri('urn:context-graph:snapshot'), iri(CG + 'configurationDigest'), literal(snapshot.manifest.configurationDigest));
82
- }
83
27
  }
84
28
  async validate(options = {}) {
85
- // These graph-independent shapes already read the dataset as a union. Index
86
- // that union once so every property lookup avoids scanning one graph per fact.
87
- // Keep every triple; the original named dataset retains assertion provenance
88
- // and remains the source for queries and serialization.
89
29
  const counts = { factCount: this.snapshot.facts.length };
90
- const validationGraph = await measureValidationPhase('build-validation-union', counts, options.onProgress, () => {
91
- const union = new Store();
92
- for (const { subject, predicate, object } of this.dataset)
93
- union.addQuad(subject, predicate, object);
94
- return union;
30
+ const { partitions, types } = await measureValidationPhase('build-validation-union', counts, options.onProgress, () => {
31
+ const started = performance.now();
32
+ const partitions = new RdfPartitions(this.snapshot);
33
+ return { partitions, types: partitions.types(completed => {
34
+ if (completed % 10 === 0)
35
+ options.onProgress?.({ phase: 'build-validation-union', status: 'progress',
36
+ elapsedMs: Math.round(performance.now() - started), ...counts, completed, total: partitions.count });
37
+ }) };
95
38
  });
96
- const report = await measureValidationPhase('validate-shacl', counts, options.onProgress, () => new SHACLValidator(new Store(evidenceShapes)).validate(validationGraph));
97
- const issues = report.results.map((result) => ({
98
- category: 'extraction-gap',
99
- focus: result.focusNode?.value,
100
- path: result.path?.value,
101
- message: result.message.map((message) => message.value).join('; '),
39
+ const started = performance.now();
40
+ const report = await measureValidationPhase('validate-shacl', counts, options.onProgress, () => validatePartitions(partitions, types, completed => {
41
+ if (completed % 10 === 0)
42
+ options.onProgress?.({ phase: 'validate-shacl', status: 'progress',
43
+ elapsedMs: Math.round(performance.now() - started), ...counts, completed, total: partitions.count });
102
44
  }));
45
+ const issues = report.issues;
103
46
  const byId = new Map(this.snapshot.facts.map(fact => [fact.id, fact]));
104
47
  for (const fact of this.snapshot.facts) {
105
48
  for (const message of obligationEvidenceIssues(fact, this.snapshot.manifest.repositoryId))
@@ -140,7 +83,12 @@ export class RdfEvidenceGraph {
140
83
  if (ast.type !== 'query' || ast.queryType !== 'SELECT' || ast.from || hasService(ast)) {
141
84
  throw new Error('Only local read queries are supported; remote datasets and updates are disabled');
142
85
  }
143
- const stream = await engine.queryBindings(sparql, { sources: [this.dataset], unionDefaultGraph: true });
86
+ const indexed = this.indexed ??= new RdfPartitions(this.snapshot);
87
+ const source = {
88
+ match: (s, p, o, g) => Readable.from(indexed.match(s, p, o, g)),
89
+ countQuads: (s, p, o, g) => indexed.countMatches(s, p, o, g),
90
+ };
91
+ const stream = await engine.queryBindings(sparql, { sources: [{ type: 'rdfjs', value: source }], unionDefaultGraph: true });
144
92
  const rows = [];
145
93
  let truncated = false;
146
94
  for await (const binding of stream) {
@@ -153,18 +101,39 @@ export class RdfEvidenceGraph {
153
101
  }
154
102
  return { rows, truncated };
155
103
  }
156
- /** The traversal projection is read through SPARQL, not a separate fact source. */
104
+ /** Lossless optimized projection, checked for equivalence with the public SPARQL path. */
157
105
  async facts() {
158
- const result = await this.select(`SELECT ?record WHERE {
159
- ?fact <${CG}record> ?record; <${CG}ordinal> ?ordinal
160
- } ORDER BY ?ordinal`, 100_000);
161
- if (result.truncated)
162
- throw new Error('Graph exceeds the 100000-fact projection bound');
163
- return result.rows.map((row) => JSON.parse(row.record));
106
+ return projectRdfFacts(this.snapshot.facts);
164
107
  }
165
- serialize() {
108
+ /** Complete N-Quads in bounded groups; RDF triple order is not a snapshot identity. */
109
+ *serializeChunks() {
166
110
  const writer = new Writer({ format: 'N-Quads' });
167
- writer.addQuads(this.dataset.getQuads(null, null, null, null));
168
- return new Promise((resolve, reject) => writer.end((error, text) => error ? reject(error) : resolve(text)));
111
+ let buffer = '';
112
+ for (const batch of new RdfPartitions(this.snapshot).named())
113
+ for (const quad of batch) {
114
+ buffer += writer.quadToString(quad.subject, quad.predicate, quad.object, quad.graph);
115
+ if (buffer.length >= 64 * 1024) {
116
+ yield buffer;
117
+ buffer = '';
118
+ }
119
+ }
120
+ if (buffer)
121
+ yield buffer;
122
+ }
123
+ /** Export large datasets with backpressure, without a whole-dataset string/quad array. */
124
+ async serializeTo(destination) {
125
+ await pipeline(this.serializeChunks(), destination);
126
+ }
127
+ /** Compatibility convenience for small exports; use serializeTo for large graphs. */
128
+ async serialize() {
129
+ const chunks = [];
130
+ let length = 0;
131
+ for (const chunk of this.serializeChunks()) {
132
+ length += chunk.length;
133
+ if (length > constants.MAX_STRING_LENGTH)
134
+ throw new Error('RDF exceeds the JavaScript string limit; use serializeTo() or serializeChunks()');
135
+ chunks.push(chunk);
136
+ }
137
+ return chunks.join('');
169
138
  }
170
139
  }
@@ -1,7 +1,10 @@
1
1
  export type GraphValidationPhase = 'validate-identities' | 'build-rdf-evidence' | 'build-validation-union' | 'validate-shacl';
2
2
  export interface GraphValidationProgress {
3
3
  readonly phase: GraphValidationPhase;
4
- readonly status: 'started' | 'completed' | 'failed';
4
+ readonly status: 'started' | 'completed' | 'failed' | 'progress';
5
+ /** Completed bounded RDF subject groups, when available. */
6
+ readonly completed?: number;
7
+ readonly total?: number;
5
8
  /** Wall time for this phase, not a completion estimate or in-phase heartbeat. */
6
9
  readonly elapsedMs: number;
7
10
  readonly factCount: number;
@@ -1,2 +1,3 @@
1
+ import { persistedJsonDigest } from './util.js';
1
2
  /** Identity of persisted JSON, excluding undefined optional fields that JSON cannot retain. */
2
- export declare const jsonDigest: (value: unknown) => string;
3
+ export declare const jsonDigest: typeof persistedJsonDigest;
@@ -1,3 +1,3 @@
1
- import { sha256, stableJson } from './util.js';
1
+ import { persistedJsonDigest } from './util.js';
2
2
  /** Identity of persisted JSON, excluding undefined optional fields that JSON cannot retain. */
3
- export const jsonDigest = (value) => sha256(stableJson(JSON.parse(JSON.stringify(value))));
3
+ export const jsonDigest = persistedJsonDigest;
@@ -103,7 +103,7 @@ function formatHydrationPlan(plan) {
103
103
  async function main() {
104
104
  const args = process.argv.slice(2);
105
105
  if (args.includes('--help') || !args.length) {
106
- process.stdout.write('weftlet mappings plan|mappings propose|mappings read|documents plan|init|config show|config validate|doctor|hydrate|cache adopt|overlay|impact scan|graph neighbors|graph paths|graph compatibility|graph coverage|sparql|validate|govern|visualize|diagnostics show|invariant check|clean-compare|ontology compile\nmappings plan|propose --request <json-file> [--allow-model for propose] previews or runs one opt-in proposal; mappings read --snapshot <id> --key <key> reads saved unreviewed results.\nUse --config <file>; queries accept --snapshot <id>. impact scan accepts --changed-file, --capability, --max-facts, --max-depth, --continuation.\ngraph neighbors --snapshot <id> --node <exact-id> [--direction incoming|outgoing|both] [--predicate <relation>] [--max-facts <1..200>] [--continuation <token>] [--json] reads one-hop saved evidence; repeat --predicate to select multiple relations.\ngraph paths --snapshot <id> --from <exact-id> --to <exact-id> [--direction incoming|outgoing|both] [--predicate <relation>] [--max-depth <1..32>] [--max-paths <1..20>] [--max-steps <1..10000>] [--continuation <token>] [--json] explores cited simple paths; incomplete searches report frontier and limits.\ngraph coverage --snapshot <id> [--project <exact-package-root>] [--max-items <1..200>] [--continuation <token>] [--json] pages saved package/file coverage; absence or zero references is not proof of no impact.\ngraph compatibility --snapshot <id> --producer-fact <assertion-id> --consumer-fact <assertion-id> [--json] compares two documented numeric obligations; advisory only, with explicit unknowns and no implementation or CI verdict.\ndocuments plan --entry <document> [--entry <document>] [--root <path>] [--document-policy <json-file>] [--ref <commit>] [--json] previews prospective committed link coverage; it does not change hydration inputs or estimate model/cache work. Source classification/ownership completeness is separate from traversal readiness. The preview policy is an operation input. Set documentTraversal in configuration to activate traversal hydration; hydrate --plan describes its execution inputs. Safety limits: --max-read-documents, --max-read-bytes, --max-document-bytes.\nhydrate --plan [--json] [--ref <commit>] [--force-semantic] previews scope, cache and budgets without model calls or graph writes. Traversal coverage and role ambiguity block new model work; direct-source selection retains its existing behavior.\nhydrate --json --quiet emits structured semantic failures on stderr with no progress lines; failure stdout stays empty. hydrate --plan maps opaque source IDs to local document paths.\nhydrate|overlay --validation-details explicitly includes local node/fact identities, extractor names and source citations in identity-conflict errors; default reports use opaque references. This flag writes no diagnostic file. Use --json --quiet for structured identity-conflict stderr and empty failure stdout. Validation timings report phase boundaries, not a live heartbeat or percentage.\ncache adopt --from-config <previous.json> [--config <current.json>] [--ref <commit>] [--dry-run] returns JSON and copies only validated compatible artifacts, without model calls or snapshot writes. --dry-run performs no cache writes. Agent configurations accept the same --owner-doc, --task and --capability selection as hydrate.\ninit [--yes] [--directory <project>] [--config <file>] [--repository-id <id>] [--code-root <path>] [--semantic-source <path>] [--evidence-root <path>] [--model <name>] [--no-gitignore]\nSource flags may be repeated. init never overwrites an existing config. See README.md for examples.\n');
106
+ process.stdout.write('weftlet mappings plan|mappings propose|mappings read|documents plan|init|config show|config validate|doctor|hydrate|cache adopt|overlay|impact scan|graph neighbors|graph paths|graph compatibility|graph coverage|sparql|validate|govern|visualize|diagnostics show|invariant check|clean-compare|ontology compile\nmappings plan|propose --request <json-file> [--allow-model for propose] previews or runs one opt-in proposal; mappings read --snapshot <id> --key <key> reads saved unreviewed results.\nUse --config <file>; queries accept --snapshot <id>. impact scan accepts --changed-file, --capability, --max-facts, --max-depth, --continuation.\ngraph neighbors --snapshot <id> --node <exact-id> [--direction incoming|outgoing|both] [--predicate <relation>] [--max-facts <1..200>] [--continuation <token>] [--json] reads one-hop saved evidence; repeat --predicate to select multiple relations.\ngraph paths --snapshot <id> --from <exact-id> --to <exact-id> [--direction incoming|outgoing|both] [--predicate <relation>] [--max-depth <1..32>] [--max-paths <1..20>] [--max-steps <1..10000>] [--continuation <token>] [--json] explores cited simple paths; incomplete searches report frontier and limits.\ngraph coverage --snapshot <id> [--project <exact-package-root>] [--max-items <1..200>] [--continuation <token>] [--json] pages saved package/file coverage; absence or zero references is not proof of no impact.\ngraph compatibility --snapshot <id> --producer-fact <assertion-id> --consumer-fact <assertion-id> [--json] compares two documented numeric obligations; advisory only, with explicit unknowns and no implementation or CI verdict.\ndocuments plan --entry <document> [--entry <document>] [--root <path>] [--document-policy <json-file>] [--ref <commit>] [--json] previews prospective committed link coverage; it does not change hydration inputs or estimate model/cache work. Source classification/ownership completeness is separate from traversal readiness. The preview policy is an operation input. Set documentTraversal in configuration to activate traversal hydration; hydrate --plan describes its execution inputs. Safety limits: --max-read-documents, --max-read-bytes, --max-document-bytes.\nhydrate --plan [--json] [--ref <commit>] [--force-semantic] previews scope, cache and budgets without model calls or graph writes. Traversal coverage and role ambiguity block new model work; direct-source selection retains its existing behavior.\nhydrate --json --quiet emits structured semantic failures on stderr with no progress lines; failure stdout stays empty. hydrate --plan maps opaque source IDs to local document paths.\nhydrate|overlay --validation-details explicitly includes local node/fact identities, extractor names and source citations in identity-conflict errors; default reports use opaque references. This flag writes no diagnostic file. Use --json --quiet for structured identity-conflict stderr and empty failure stdout. Validation reports phase boundaries, elapsed time and completed groups; group counts are not completion-time estimates.\ncache adopt --from-config <previous.json> [--config <current.json>] [--ref <commit>] [--dry-run] returns JSON and copies only validated compatible artifacts, without model calls or snapshot writes. --dry-run performs no cache writes. Agent configurations accept the same --owner-doc, --task and --capability selection as hydrate.\ninit [--yes] [--directory <project>] [--config <file>] [--repository-id <id>] [--code-root <path>] [--semantic-source <path>] [--evidence-root <path>] [--model <name>] [--no-gitignore]\nSource flags may be repeated. init never overwrites an existing config. See README.md for examples.\n');
107
107
  return;
108
108
  }
109
109
  if (await runDocumentCoverage(args, defaultConfigPath))
@@ -4,7 +4,10 @@ export declare class FileSnapshotStore implements SnapshotStore {
4
4
  constructor(root: string);
5
5
  write(snapshot: GraphSnapshot): Promise<void>;
6
6
  read(snapshotId: string): Promise<GraphSnapshot | undefined>;
7
+ private readManifest;
7
8
  readHead(headName: string): Promise<GraphSnapshot | undefined>;
9
+ readHeadManifest(headName: string): Promise<GraphSnapshot['manifest'] | undefined>;
10
+ private headId;
8
11
  updateHead(headName: string, snapshotId: string): Promise<void>;
9
12
  }
10
13
  export declare class SemanticArtifactStore {