@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.
- package/README.md +32 -26
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/dist/application/hydrator.js +18 -15
- package/dist/application/overlay.js +9 -9
- package/dist/application/semantic-run.d.ts +1 -1
- package/dist/extraction/ontology.d.ts +2 -0
- package/dist/extraction/ontology.js +28 -13
- package/dist/graph/code-coverage.js +2 -2
- package/dist/graph/code-rdf.d.ts +3 -2
- package/dist/graph/code-rdf.js +21 -14
- package/dist/graph/neighborhood.js +3 -3
- package/dist/graph/partitioned-shacl.d.ts +14 -0
- package/dist/graph/partitioned-shacl.js +66 -0
- package/dist/graph/paths.js +3 -3
- 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 +66 -97
- package/dist/graph/validation-progress.d.ts +4 -1
- package/dist/infrastructure/json-digest.d.ts +2 -1
- package/dist/infrastructure/json-digest.js +2 -2
- package/dist/interfaces/cli.js +1 -1
- 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/docs/1.0-WORK-QUEUE.md +18 -8
- package/docs/ADR-040-bounded-graph-io.md +90 -0
- package/docs/RELEASING.md +3 -3
- package/docs/VALIDATION.md +84 -0
- package/docs/benchmarks/large-graph-2026-11-02/result.json +156 -0
- package/docs/extraction-manifest.json +26 -10
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Proposed future capabilities are recorded in the [Weftlet 2.0 product plan](docs
|
|
|
14
14
|
## Set up a project
|
|
15
15
|
|
|
16
16
|
<!-- weftlet:release:start -->
|
|
17
|
-
Package versions in this checkout/archive: `@weftlet/toolkit@0.7.
|
|
17
|
+
Package versions in this checkout/archive: `@weftlet/toolkit@0.7.1` and `@weftlet/core@0.7.1`.
|
|
18
18
|
<!-- weftlet:release:end -->
|
|
19
19
|
|
|
20
20
|
Weftlet uses `@weftlet/toolkit`; its neutral `@weftlet/core` dependency is installed
|
|
@@ -228,24 +228,31 @@ its sidecar policy is not automatically adopted by hydration. See
|
|
|
228
228
|
|
|
229
229
|
Starting with **0.5.0**, identity preflight runs before ontology/RDF work.
|
|
230
230
|
`hydrate --json --quiet` reports conflicts using opaque references; `--validation-details` explicitly
|
|
231
|
-
includes local identities, producer versions and citations. Validation emits phase
|
|
232
|
-
|
|
231
|
+
includes local identities, producer versions and citations. Validation emits phase timings,
|
|
232
|
+
graph counts and completed validation groups. These are not completion-time estimates. Full SHACL remains required after a
|
|
233
233
|
passing preflight. See [ADR 021](docs/ADR-021-identity-preflight-and-validation-timing.md).
|
|
234
234
|
|
|
235
235
|
Version 0.5.0 also accepts `semantic.timeoutMs: 600000` for an approved ten-minute extraction
|
|
236
236
|
attempt; the default remains two minutes. See [provider limits and reuse](docs/PROVIDERS.md#limits-and-reuse)
|
|
237
237
|
before retrying with compatible cached artifacts.
|
|
238
238
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
validation incremental, and there is no validation-result cache, Rust engine or GraphDB service.
|
|
243
|
-
Saved queries do not rerun SHACL.
|
|
239
|
+
The toolkit uses JavaScript `rdf-validate-shacl` with bounded N3 subject groups. Each hydration
|
|
240
|
+
validates the complete graph; semantic artifact reuse does not make validation incremental.
|
|
241
|
+
There is no validation-result cache, Rust engine or GraphDB service. Saved queries do not rerun SHACL.
|
|
244
242
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
243
|
+
Complete identity groups preserve conflicting assertions, and graph-wide class membership retains
|
|
244
|
+
cross-record proof checks. The same strict shapes run on every target. The RDF encoder also serves
|
|
245
|
+
streamed N-Quads exports and SPARQL; traversal projections have SPARQL equivalence tests. Hashing and
|
|
246
|
+
snapshot JSON persistence/reload avoid a whole-graph string. Existing canonical digests, cache keys
|
|
247
|
+
and immutable snapshot bytes are preserved. See [ADR 040](docs/ADR-040-bounded-graph-io.md) for the
|
|
248
|
+
partition correctness boundary, public corpus regression and remaining memory limits.
|
|
249
|
+
|
|
250
|
+
For large programmatic RDF exports, use `await graph.serializeTo(writable)` or consume
|
|
251
|
+
`graph.serializeChunks()`. The `serialize()` convenience returns a single string and therefore
|
|
252
|
+
remains subject to JavaScript's string-size limit.
|
|
253
|
+
|
|
254
|
+
The 0.3.0 optimization built a complete temporary validation union. The measurements below describe
|
|
255
|
+
that earlier implementation; they are not timings for the current richer code graph.
|
|
249
256
|
|
|
250
257
|
In bounded, single-run public experiments, 2,000 facts with unique subject/object nodes took
|
|
251
258
|
31.50 seconds in the original SHACL path, compared with 0.153 seconds to build the union and
|
|
@@ -260,12 +267,11 @@ pnpm build
|
|
|
260
267
|
pnpm benchmark:shacl
|
|
261
268
|
```
|
|
262
269
|
|
|
263
|
-
The [T05 closeout](docs/ADR-027-validation-closeout.md) measures cold, repeated and changed inputs,
|
|
264
|
-
plus worker, exact-result-key and Rust prototypes
|
|
270
|
+
The historical [T05 closeout](docs/ADR-027-validation-closeout.md) measures cold, repeated and changed inputs,
|
|
271
|
+
plus worker, exact-result-key and Rust prototypes:
|
|
265
272
|
the 10,000-fact public cases measured about 2.9–5.7 seconds, with material memory use. These are
|
|
266
273
|
fixture measurements, not a private consumer capacity guarantee. The prototypes are development
|
|
267
|
-
tools, not runtime options; every hydration still performs full validation.
|
|
268
|
-
phase boundaries because synchronous work blocks same-thread timers. Reproduce the lifecycle study
|
|
274
|
+
tools, not runtime options; every hydration still performs full validation. Reproduce that lifecycle study
|
|
269
275
|
from a development checkout with `pnpm benchmark:validation`; raw results and commands are linked
|
|
270
276
|
from the ADR.
|
|
271
277
|
|
|
@@ -365,14 +371,14 @@ one-hop reads of saved evidence: no model or indexer is invoked. Each assertion
|
|
|
365
371
|
citations, authority and review state, including multiple assertions about the same edge.
|
|
366
372
|
Page completion does not establish complete product coverage. See [the query contract](docs/ADR-029-exact-graph-neighborhoods.md).
|
|
367
373
|
|
|
368
|
-
|
|
374
|
+
Starting with 0.7.0, Weftlet preserves typed `CodeSymbol` definitions and individual SCIP
|
|
369
375
|
references, including local symbols, repeated same-file uses and one-based UTF-16 columns.
|
|
370
376
|
From a physical-file node, request incoming `definedBy` facts to discover its symbols; from a
|
|
371
377
|
symbol, request incoming/outgoing `references` to inspect exact occurrences. Native lexical
|
|
372
378
|
enclosure is not a call graph. Resolution distinguishes indexed definitions, declaration-only
|
|
373
379
|
targets and absent indexed definitions. Local ordinal IDs are scoped to the exact index; refresh
|
|
374
380
|
code evidence before comparing revisions. See [code evidence and limits](docs/ADR-032-scip-code-evidence.md).
|
|
375
|
-
|
|
381
|
+
Weftlet also resolves explicit Markdown symbol selectors in selected documents:
|
|
376
382
|
|
|
377
383
|
```md
|
|
378
384
|
[helper](../src/worker.ts#weftlet-symbol=Engine.execute)
|
|
@@ -397,14 +403,14 @@ semantic/document-traversal scope and SCIP selection; they require no extra prov
|
|
|
397
403
|
Validation documents stay outside intent extraction and can declare `verifiedBy`, not `implementedBy`.
|
|
398
404
|
See [ADR 033](docs/ADR-033-document-symbol-bindings.md) for bounds and draft-overlay behavior.
|
|
399
405
|
|
|
400
|
-
|
|
406
|
+
Version 0.7.0 also derives
|
|
401
407
|
`calls` from actual TypeScript call/new syntax and exact SCIP identities. Query a function's
|
|
402
408
|
incoming/outgoing `calls` to inspect callers/callees, or follow a bounded path using `calls` and
|
|
403
409
|
`implementedBy`. Each assertion retains callsite and endpoint proofs, with explicit bodyless,
|
|
404
410
|
external, optional and member-dispatch limits. Runtime execution is not implied. Values, callbacks
|
|
405
411
|
and dynamic dispatch without a known target remain references and cited gaps. See
|
|
406
|
-
[ADR 035](docs/ADR-035-static-call-evidence.md).
|
|
407
|
-
|
|
412
|
+
[ADR 035](docs/ADR-035-static-call-evidence.md). Optional inferred-mapping proposals are available separately; see
|
|
413
|
+
[ADR 039](docs/ADR-039-mapping-proposals.md).
|
|
408
414
|
|
|
409
415
|
Selected Markdown documents can now declare producer guarantees and consumer requirements in
|
|
410
416
|
optional `weftlet-obligation` JSON fences. Both sides name an exact scoped/versioned queue or
|
|
@@ -413,9 +419,9 @@ neighborhoods and paths, with the original statement, citations and any document
|
|
|
413
419
|
constraint/unit and consumer revalidation requirement. Missing fields remain unknown. Declarations
|
|
414
420
|
do not establish fulfillment, runtime delivery or compatibility; ordinary prose is not automatically
|
|
415
421
|
converted into this format. See [the declaration format and example](docs/ADR-036-contract-obligation-declarations.md).
|
|
416
|
-
|
|
422
|
+
These declarations are supported starting with 0.7.0.
|
|
417
423
|
|
|
418
|
-
For two retrieved obligation facts, the
|
|
424
|
+
For two retrieved obligation facts, the published CLI supports:
|
|
419
425
|
|
|
420
426
|
```sh
|
|
421
427
|
weftlet graph compatibility --snapshot <snapshot-id> --producer-fact <fact-id> --consumer-fact <fact-id> --json
|
|
@@ -427,7 +433,7 @@ Results distinguish compatible limits, an insufficient guarantee, disjoint limit
|
|
|
427
433
|
consumer revalidation and unknowns. Both original citations and source approvals remain visible.
|
|
428
434
|
This advisory assessment does not establish code fulfillment or a CI policy violation. See
|
|
429
435
|
[comparison semantics and limits](docs/ADR-037-selected-contract-compatibility.md). The command
|
|
430
|
-
reads saved evidence during edits without rebuilding it
|
|
436
|
+
reads saved evidence during edits without rebuilding it and is available starting with 0.7.0.
|
|
431
437
|
|
|
432
438
|
Saved repository code coverage is separate from the returned-page relationship counts:
|
|
433
439
|
|
|
@@ -483,8 +489,8 @@ ahead of repeated detail and shared-import context. Ranking changes ordering, no
|
|
|
483
489
|
the reachable scope. Responses identify the ranking version; after a ranking upgrade, restart
|
|
484
490
|
scans whose old continuation tokens are rejected. No snapshot refresh is needed for new ordering.
|
|
485
491
|
|
|
486
|
-
|
|
487
|
-
|
|
492
|
+
The published toolkit includes a [coverage reporting correction](docs/ADR-024-implementation-coverage.md)
|
|
493
|
+
that counts code relationships displayed under contracts as implementation
|
|
488
494
|
evidence. New reports identify `coverage.scope: "returned-page"` and `countsAreNonExclusive: true`.
|
|
489
495
|
Counts can overlap and describe returned evidence; they are not completeness percentages or proof
|
|
490
496
|
that behavior is correct. Document/model claims and test-only evidence do not establish production
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -363,6 +363,7 @@ Do not edit this file manually.
|
|
|
363
363
|
| `@comunica/utils-metadata` | 5.4.0 | MIT | [link](https://comunica.dev/) |
|
|
364
364
|
| `@comunica/utils-query-operation` | 5.4.0 | MIT | [link](https://comunica.dev/) |
|
|
365
365
|
| `@dabh/diagnostics` | 2.0.9 | MIT | [link](https://github.com/DABH/diagnostics) |
|
|
366
|
+
| `@discoveryjs/json-ext` | 1.1.0 | MIT | [link](https://github.com/discoveryjs/json-ext#readme) |
|
|
366
367
|
| `@hono/node-server` | 2.1.5 | MIT | [link](https://github.com/honojs/node-server) |
|
|
367
368
|
| `@jeswr/prefixcc` | 1.2.1 | MIT | [link](https://github.com/jeswr/prefixcc.js#readme) |
|
|
368
369
|
| `@jeswr/stream-to-string` | 2.0.0 | MIT | [link](https://github.com/jeswr/stream-to-string/README.md) |
|
|
@@ -376,6 +377,7 @@ Do not edit this file manually.
|
|
|
376
377
|
| `@rdfjs/to-ntriples` | 3.0.1 | MIT | [link](https://github.com/rdfjs-base/to-ntriples) |
|
|
377
378
|
| `@rdfjs/types` | 1.1.2, 2.0.1 | MIT | [link](http://rdf.js.org/) |
|
|
378
379
|
| `@so-ric/colorspace` | 1.1.6 | MIT | [link](https://github.com/so-ric/colorspace) |
|
|
380
|
+
| `@streamparser/json` | 0.0.26 | MIT | [link](https://github.com/juanjoDiaz/jsonparse2#readme) |
|
|
379
381
|
| `@tpluscode/rdf-ns-builders` | 5.1.0 | MIT | [link](https://github.com/tpluscode/rdf-ns-builders#readme) |
|
|
380
382
|
| `@traqula/algebra-sparql-1-1` | 1.4.0 | MIT | [link](https://github.com/comunica/traqula#readme) |
|
|
381
383
|
| `@traqula/algebra-sparql-1-2` | 1.4.0 | MIT | [link](https://github.com/comunica/traqula#readme) |
|
|
@@ -2,7 +2,7 @@ import { scipSourcePatterns } from '../configuration/scip-project.js';
|
|
|
2
2
|
import { buildCodeInventory } from '../extraction/code-inventory.js';
|
|
3
3
|
import { contractPatternRoots } from "../repository/glob.js";
|
|
4
4
|
import { repositoryFileExtractorIdentity, repositoryContractExtractorIdentity, scipSymbolExtractorIdentity } from '../extraction/static.js';
|
|
5
|
-
import { sha256, stableJson } from "../infrastructure/util.js";
|
|
5
|
+
import { sha256, stableJson, stableJsonDigest } from "../infrastructure/util.js";
|
|
6
6
|
import { assembleDocumentCodeBindings } from './document-code-bindings.js';
|
|
7
7
|
import { documentCodeBindingIdentity } from '../extraction/document-code-bindings.js';
|
|
8
8
|
import { documentObligationIdentity } from '../extraction/document-obligations.js';
|
|
@@ -43,7 +43,7 @@ export class ContextGraphHydrator {
|
|
|
43
43
|
.sort((left, right) => stableJson(left).localeCompare(stableJson(right)));
|
|
44
44
|
return {
|
|
45
45
|
commitSha,
|
|
46
|
-
digest:
|
|
46
|
+
digest: stableJsonDigest(references),
|
|
47
47
|
references,
|
|
48
48
|
};
|
|
49
49
|
}
|
|
@@ -71,10 +71,11 @@ export class ContextGraphHydrator {
|
|
|
71
71
|
}
|
|
72
72
|
const { sources } = selection;
|
|
73
73
|
await this.dependencies.ontologyCompiler.checkReady?.();
|
|
74
|
-
const prior = await snapshotStore.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
const prior = snapshotStore.readHeadManifest ? await snapshotStore.readHeadManifest(options.headName)
|
|
75
|
+
: (await snapshotStore.readHead(options.headName))?.manifest;
|
|
76
|
+
const baseSha = prior?.commitSha === commitSha
|
|
77
|
+
? prior.baseSha
|
|
78
|
+
: (prior?.commitSha ?? (await repository.firstParent(commitSha)));
|
|
78
79
|
const allRoots = [
|
|
79
80
|
...new Set([
|
|
80
81
|
...scopePaths,
|
|
@@ -98,8 +99,8 @@ export class ContextGraphHydrator {
|
|
|
98
99
|
const runtimeSuffix = config.runtimeEvidence.length > 0
|
|
99
100
|
? `runtime-${runtimeRevision.digest.slice(0, 12)}`
|
|
100
101
|
: undefined;
|
|
101
|
-
const configurationDigest =
|
|
102
|
-
const scopeDigest =
|
|
102
|
+
const configurationDigest = stableJsonDigest(config);
|
|
103
|
+
const scopeDigest = stableJsonDigest(scopePaths).slice(0, 12);
|
|
103
104
|
const effectiveSuffix = [`graph-v${graphAssemblyVersion}`, `links-${documentLinkerIdentity.version}`, `files-${repositoryFileExtractorIdentity.version}`,
|
|
104
105
|
`contracts-${repositoryContractExtractorIdentity.version}`,
|
|
105
106
|
`symbols-${scipSymbolExtractorIdentity.version}`,
|
|
@@ -130,7 +131,7 @@ export class ContextGraphHydrator {
|
|
|
130
131
|
const artifacts = hydrated.map((result) => result.artifact);
|
|
131
132
|
const diagnostics = hydrated.flatMap(({ source, artifact }) => (artifact.diagnostics ?? []).map((item) => ({
|
|
132
133
|
...item,
|
|
133
|
-
id:
|
|
134
|
+
id: stableJsonDigest({ artifact: artifact.key, diagnostic: item.id, path: source.path }),
|
|
134
135
|
provenance: item.provenance.map((span) => ({
|
|
135
136
|
...span,
|
|
136
137
|
path: source.path,
|
|
@@ -140,7 +141,7 @@ export class ContextGraphHydrator {
|
|
|
140
141
|
})));
|
|
141
142
|
const semanticFacts = classifyHydratedFacts(hydrated.flatMap(({ source, artifact }) => artifact.facts.map((fact) => ({
|
|
142
143
|
...fact,
|
|
143
|
-
id:
|
|
144
|
+
id: stableJsonDigest({ artifact: artifact.key, fact: fact.id, path: source.path }),
|
|
144
145
|
provenance: {
|
|
145
146
|
...fact.provenance,
|
|
146
147
|
path: source.path,
|
|
@@ -166,7 +167,7 @@ export class ContextGraphHydrator {
|
|
|
166
167
|
const path = /^(?:component:file:|document:)(.+)$/.exec(node.id)?.[1];
|
|
167
168
|
if (path && !tracked.has(path))
|
|
168
169
|
diagnostics.push({
|
|
169
|
-
id:
|
|
170
|
+
id: stableJsonDigest({ fact: fact.id, missing: path }),
|
|
170
171
|
kind: 'implementationGaps',
|
|
171
172
|
reason: `Cited implementation path ${path} is absent from the committed repository.`,
|
|
172
173
|
relatedNodeIds: [node.id], provenance: [fact.provenance], evidenceScope: 'snapshot-query',
|
|
@@ -222,7 +223,7 @@ export class ContextGraphHydrator {
|
|
|
222
223
|
codeInventory,
|
|
223
224
|
...(selection.documentSelection ? { documentSelection: selection.documentSelection } : {}),
|
|
224
225
|
};
|
|
225
|
-
const contentDigest =
|
|
226
|
+
const contentDigest = stableJsonDigest(logical);
|
|
226
227
|
const snapshot = {
|
|
227
228
|
manifest: {
|
|
228
229
|
schemaVersion: 1,
|
|
@@ -282,7 +283,9 @@ export class ContextGraphHydrator {
|
|
|
282
283
|
return { snapshot, semanticExtracted, semanticReused };
|
|
283
284
|
}
|
|
284
285
|
async compareClean(options) {
|
|
285
|
-
const
|
|
286
|
+
const store = this.dependencies.snapshotStore;
|
|
287
|
+
const current = store.readHeadManifest ? await store.readHeadManifest(options.headName)
|
|
288
|
+
: (await store.readHead(options.headName))?.manifest;
|
|
286
289
|
if (!current)
|
|
287
290
|
throw new Error('No incremental snapshot exists to compare');
|
|
288
291
|
const clean = await this.hydrate({
|
|
@@ -290,8 +293,8 @@ export class ContextGraphHydrator {
|
|
|
290
293
|
headName: `${options.headName}-clean`,
|
|
291
294
|
snapshotSuffix: `clean-${Date.now()}`,
|
|
292
295
|
});
|
|
293
|
-
if (clean.snapshot.manifest.contentDigest !== current.
|
|
294
|
-
throw new Error(`Clean hydration drift: ${current.
|
|
296
|
+
if (clean.snapshot.manifest.contentDigest !== current.contentDigest) {
|
|
297
|
+
throw new Error(`Clean hydration drift: ${current.contentDigest} != ${clean.snapshot.manifest.contentDigest}`);
|
|
295
298
|
}
|
|
296
299
|
return clean;
|
|
297
300
|
}
|
|
@@ -7,7 +7,7 @@ import { buildCodeInventory } from '../extraction/code-inventory.js';
|
|
|
7
7
|
import { GitRepository } from "../repository/git.js";
|
|
8
8
|
import { withCommittedTree } from "../repository/committed-tree.js";
|
|
9
9
|
import { RepositoryContractExtractor, RepositoryFileExtractor, ScipSymbolExtractor, ScipTypeScriptIndexer, repositoryFileExtractorIdentity, repositoryContractExtractorIdentity, scipSymbolExtractorIdentity } from "../extraction/static.js";
|
|
10
|
-
import { sha256,
|
|
10
|
+
import { sha256, stableJsonDigest } from "../infrastructure/util.js";
|
|
11
11
|
import { contextPackEvidenceAuthority } from "../graph/query.js";
|
|
12
12
|
import { inWorkingScope, readWorkingContent } from "../repository/working-content.js";
|
|
13
13
|
import { extractRoutingDeclarations, routingExtractorIdentity } from '../extraction/routing.js';
|
|
@@ -34,7 +34,7 @@ export async function buildWorkingOverlay(repository, config, store, storageRoot
|
|
|
34
34
|
const base = await store.read(baseSnapshotId);
|
|
35
35
|
if (!base || base.manifest.overlayDigest)
|
|
36
36
|
throw new Error('Overlay requires a committed base snapshot');
|
|
37
|
-
if (base.manifest.configurationDigest !==
|
|
37
|
+
if (base.manifest.configurationDigest !== stableJsonDigest(config)) {
|
|
38
38
|
throw new Error('Overlay configuration must match its base snapshot');
|
|
39
39
|
}
|
|
40
40
|
const head = await repository.resolveRef('HEAD');
|
|
@@ -67,10 +67,10 @@ export async function buildWorkingOverlay(repository, config, store, storageRoot
|
|
|
67
67
|
files: captured.map(({ path, content }) => ({ path, digest: content === null ? null : sha256(content) })),
|
|
68
68
|
sourcePatterns: roots, excludedPaths: excluded,
|
|
69
69
|
};
|
|
70
|
-
const overlayDigest =
|
|
70
|
+
const overlayDigest = stableJsonDigest({ version: 2, ...overlayCapture });
|
|
71
71
|
// Capture identity describes the same working bytes across extractor upgrades. Publication
|
|
72
72
|
// identity also binds the extractor so rebuilding an old base cannot replace an old overlay.
|
|
73
|
-
const snapshotId = `${head}-overlay-${
|
|
73
|
+
const snapshotId = `${head}-overlay-${stableJsonDigest({
|
|
74
74
|
baseContentDigest: base.manifest.contentDigest, overlayDigest,
|
|
75
75
|
assembly: graphAssemblyIdentity,
|
|
76
76
|
repositoryFileExtractor: repositoryFileExtractorIdentity,
|
|
@@ -79,7 +79,7 @@ export async function buildWorkingOverlay(repository, config, store, storageRoot
|
|
|
79
79
|
...(routingSources.length ? { routingExtractor: routingExtractorIdentity } : {}),
|
|
80
80
|
...(config.documentTraversal ? { documentSelection: documentSelectionIdentity,
|
|
81
81
|
documentEvidence: documentEvidenceExtractorIdentity } : {}),
|
|
82
|
-
})
|
|
82
|
+
}).slice(0, 32)}`;
|
|
83
83
|
const affected = new Set(captured.map((file) => file.path));
|
|
84
84
|
const extracted = await withCommittedTree(repository.root, head, async (root) => {
|
|
85
85
|
for (const file of captured) {
|
|
@@ -113,7 +113,7 @@ export async function buildWorkingOverlay(repository, config, store, storageRoot
|
|
|
113
113
|
const documentSelection = documents?.documentSelection ? {
|
|
114
114
|
...documents.documentSelection,
|
|
115
115
|
// Temporary Git commit timestamps are not evidence identity. Bind drafts to captured bytes.
|
|
116
|
-
digest:
|
|
116
|
+
digest: stableJsonDigest({ ...documents.documentSelection, digest: undefined, baseSha: head, overlayDigest }),
|
|
117
117
|
} : undefined;
|
|
118
118
|
const scip = await new ScipTypeScriptIndexer(config.scip, storageRoot).index(context);
|
|
119
119
|
const codeInventory = buildCodeInventory(await draftRepository.listEntriesAt(context.commitSha), await draftRepository.listFilesAt(context.commitSha, config.codeRoots), config.codeRoots, config.scip.sourceProject, scip);
|
|
@@ -138,7 +138,7 @@ export async function buildWorkingOverlay(repository, config, store, storageRoot
|
|
|
138
138
|
const facts = draftFacts([...codeFacts, ...routing.facts, ...obligations.facts, ...(documents.documentEvidence?.facts ?? []), ...bindings.facts], snapshotId, head);
|
|
139
139
|
const diagnostics = [...extraction.diagnostics, ...bindings.diagnostics, ...obligations.diagnostics, ...routing.diagnostics, ...(documents?.documentEvidence?.diagnostics ?? [])].map(({ id: _id, ...item }) => {
|
|
140
140
|
const draft = { ...item, provenance: item.provenance.map((span) => ({ ...span, commitSha: head })) };
|
|
141
|
-
return { ...draft, id:
|
|
141
|
+
return { ...draft, id: stableJsonDigest(draft) };
|
|
142
142
|
}).sort((a, b) => a.id.localeCompare(b.id));
|
|
143
143
|
return { facts, diagnostics, documents, documentSelection, retainedSubjects, codeInventory };
|
|
144
144
|
});
|
|
@@ -161,9 +161,9 @@ export async function buildWorkingOverlay(repository, config, store, storageRoot
|
|
|
161
161
|
[scipSymbolExtractorIdentity.id]: scipSymbolExtractorIdentity.version,
|
|
162
162
|
[staticCallIdentity.id]: staticCallIdentity.version,
|
|
163
163
|
...(routingSources.length ? { [routingExtractorIdentity.id]: routingExtractorIdentity.version } : {}) },
|
|
164
|
-
generatedAt: new Date().toISOString(), contentDigest:
|
|
164
|
+
generatedAt: new Date().toISOString(), contentDigest: stableJsonDigest({ facts: allFacts, diagnostics: extracted.diagnostics,
|
|
165
165
|
codeInventory: extracted.codeInventory,
|
|
166
|
-
...(extracted.documentSelection ? { documentSelection: extracted.documentSelection } : {}) })
|
|
166
|
+
...(extracted.documentSelection ? { documentSelection: extracted.documentSelection } : {}) }),
|
|
167
167
|
},
|
|
168
168
|
facts: allFacts,
|
|
169
169
|
invariants: base.invariants.map((rule) => ({
|
|
@@ -7,7 +7,7 @@ export interface HydrationProgressEvent {
|
|
|
7
7
|
readonly total?: number;
|
|
8
8
|
readonly sourceId?: string;
|
|
9
9
|
readonly elapsedMs?: number;
|
|
10
|
-
readonly status?: 'started' | 'completed' | 'failed';
|
|
10
|
+
readonly status?: 'started' | 'completed' | 'failed' | 'progress';
|
|
11
11
|
readonly factCount?: number;
|
|
12
12
|
readonly nodeCount?: number;
|
|
13
13
|
}
|
|
@@ -29,4 +29,6 @@ export declare class DirectRdfOntologyCompiler implements OntologyCompiler {
|
|
|
29
29
|
constructor(storageRoot: string);
|
|
30
30
|
compile(snapshotId: string, artifacts: readonly SemanticArtifact[]): Promise<OntologyCompileResult>;
|
|
31
31
|
}
|
|
32
|
+
/** Exact legacy stableJson([{content, path}, ...]) digest, including UTF-8 decoding/escaping. */
|
|
33
|
+
export declare function digestRdfFiles(root: string, files: readonly string[]): Promise<string>;
|
|
32
34
|
export {};
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// Modified for standalone extraction; see docs/extraction-manifest.json.
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
3
5
|
import { withCommittedTree } from "../repository/committed-tree.js";
|
|
4
6
|
import { DataFactory, Store, Writer } from 'n3';
|
|
5
7
|
import { CG, nodeIri } from "../graph/rdf.js";
|
|
6
|
-
import { cp, mkdir, readdir,
|
|
8
|
+
import { cp, mkdir, readdir, rm, writeFile } from 'node:fs/promises';
|
|
7
9
|
import { execFile } from 'node:child_process';
|
|
8
10
|
import { promisify } from 'node:util';
|
|
9
11
|
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
10
12
|
import { runProcess } from "../infrastructure/process.js";
|
|
11
|
-
import { sha256, stableJson } from "../infrastructure/util.js";
|
|
12
13
|
const probeDocker = async (cwd) => {
|
|
13
14
|
const { stdout } = await promisify(execFile)('docker', ['info', '--format', '{{.OSType}}'], {
|
|
14
15
|
cwd,
|
|
@@ -130,11 +131,7 @@ export class OpenCaesarOntologyCompiler {
|
|
|
130
131
|
const rdfFiles = await listRdfFiles(rdfRoot);
|
|
131
132
|
if (rdfFiles.length === 0)
|
|
132
133
|
throw new Error('openCAESAR produced no RDF files');
|
|
133
|
-
|
|
134
|
-
path: relative(rdfRoot, path),
|
|
135
|
-
content: await readFile(path, 'utf8'),
|
|
136
|
-
})));
|
|
137
|
-
return { root: rdfRoot, rdfFiles, digest: sha256(stableJson(digestParts)) };
|
|
134
|
+
return { root: rdfRoot, rdfFiles, digest: await digestRdfFiles(rdfRoot, rdfFiles) };
|
|
138
135
|
}
|
|
139
136
|
}
|
|
140
137
|
/** Default compiler preserves asserted semantics without Java, Docker, or an OWL reasoner. */
|
|
@@ -152,13 +149,31 @@ export class DirectRdfOntologyCompiler {
|
|
|
152
149
|
}
|
|
153
150
|
}
|
|
154
151
|
const writer = new Writer({ format: 'N-Triples' });
|
|
155
|
-
|
|
156
|
-
const text = await new Promise((accept, reject) => writer.end((error, value) => error ? reject(error) : accept(value)));
|
|
157
|
-
const content = text.split('\n').filter(Boolean).sort().join('\n') + '\n';
|
|
152
|
+
const lines = Array.from(dataset, quad => writer.quadToString(quad.subject, quad.predicate, quad.object, quad.graph)).sort();
|
|
158
153
|
const root = join(this.storageRoot, 'work', `rdf-${randomUUID()}`);
|
|
159
154
|
await mkdir(root, { recursive: true });
|
|
160
155
|
const path = join(root, 'semantic.nt');
|
|
161
|
-
|
|
162
|
-
|
|
156
|
+
const digest = createHash('sha256');
|
|
157
|
+
function* content() {
|
|
158
|
+
// The legacy empty dataset encodes a single newline too.
|
|
159
|
+
for (const line of lines.length ? lines : ['\n']) {
|
|
160
|
+
digest.update(line);
|
|
161
|
+
yield line;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
await pipeline(content(), createWriteStream(path, { flags: 'wx' }));
|
|
165
|
+
return { root, rdfFiles: [path], digest: digest.digest('hex') };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Exact legacy stableJson([{content, path}, ...]) digest, including UTF-8 decoding/escaping. */
|
|
169
|
+
export async function digestRdfFiles(root, files) {
|
|
170
|
+
const hash = createHash('sha256').update('[');
|
|
171
|
+
for (const [index, path] of files.entries()) {
|
|
172
|
+
hash.update(`${index ? ',' : ''}{"content":"`);
|
|
173
|
+
for await (const chunk of createReadStream(path, { encoding: 'utf8' })) {
|
|
174
|
+
hash.update(JSON.stringify(chunk).slice(1, -1));
|
|
175
|
+
}
|
|
176
|
+
hash.update(`","path":${JSON.stringify(relative(root, path))}}`);
|
|
163
177
|
}
|
|
178
|
+
return hash.update(']').digest('hex');
|
|
164
179
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stableJsonDigest } from '../infrastructure/util.js';
|
|
2
2
|
import { projectCodeCoverage } from './code-coverage-projection.js';
|
|
3
3
|
import { parseCodeCoverageRequest } from './code-coverage-request.js';
|
|
4
4
|
import { renderCodeCoverage } from './code-coverage-renderer.js';
|
|
@@ -37,7 +37,7 @@ export class SnapshotCodeCoverageService {
|
|
|
37
37
|
throw Error('Duplicate fact identities prevent stable code coverage');
|
|
38
38
|
const projected = projectCodeCoverage({ ...saved, facts });
|
|
39
39
|
const { continuation, ...query } = request;
|
|
40
|
-
const binding =
|
|
40
|
+
const binding = stableJsonDigest({ version, query, manifest: saved.manifest, facts, diagnostics: saved.diagnostics });
|
|
41
41
|
const fileView = request.projectRoot !== undefined;
|
|
42
42
|
const files = fileView ? projected.files.filter(f => f.projectRoot === request.projectRoot) : [];
|
|
43
43
|
const length = fileView ? files.length : projected.projects.length;
|
package/dist/graph/code-rdf.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Store, type NamedNode } from 'n3';
|
|
2
|
-
import { type GraphFact, type SourceSpan } from '@weftlet/core';
|
|
3
|
-
export declare function addCodeEvidence(dataset: Store, record: NamedNode, fact: GraphFact, nodeIri: (id: string) => string): void;
|
|
2
|
+
import { type GraphFact, type GraphNode, type SourceSpan } from '@weftlet/core';
|
|
3
|
+
export declare function addCodeEvidence(dataset: Store, record: NamedNode, fact: GraphFact, nodeIri: (id: string) => string, includeNodes?: boolean): void;
|
|
4
|
+
export declare function addCodeNodeEvidence(dataset: Store, node: GraphNode, nodeIri: (id: string) => string): void;
|
|
4
5
|
export declare function invalidCodeRange(range: Pick<SourceSpan, 'startLine' | 'endLine' | 'startColumn' | 'endColumn' | 'columnEncoding'>): boolean;
|
|
5
6
|
export declare function codeEvidenceIssues(fact: GraphFact): readonly string[];
|
package/dist/graph/code-rdf.js
CHANGED
|
@@ -2,7 +2,7 @@ import { DataFactory } from 'n3';
|
|
|
2
2
|
import { repositoryFileNode } from '@weftlet/core';
|
|
3
3
|
import { CG, RDF, XSD } from './shacl-shapes.js';
|
|
4
4
|
const { namedNode: iri, literal } = DataFactory;
|
|
5
|
-
export function addCodeEvidence(dataset, record, fact, nodeIri) {
|
|
5
|
+
export function addCodeEvidence(dataset, record, fact, nodeIri, includeNodes = true) {
|
|
6
6
|
const property = (subject, key, value) => {
|
|
7
7
|
if (value === undefined)
|
|
8
8
|
return;
|
|
@@ -26,19 +26,26 @@ export function addCodeEvidence(dataset, record, fact, nodeIri) {
|
|
|
26
26
|
property(range, key, value);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (value
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
if (includeNodes)
|
|
30
|
+
for (const node of [fact.subject, fact.object])
|
|
31
|
+
addCodeNodeEvidence(dataset, node, nodeIri);
|
|
32
|
+
}
|
|
33
|
+
export function addCodeNodeEvidence(dataset, node, nodeIri) {
|
|
34
|
+
const property = (subject, key, value) => {
|
|
35
|
+
if (value !== undefined)
|
|
36
|
+
dataset.addQuad(subject, iri(CG + key), literal(String(value), iri(XSD + (typeof value === 'number' ? 'integer' : 'string'))));
|
|
37
|
+
};
|
|
38
|
+
if (node.kind !== 'CodeSymbol' && node.codeSymbol === undefined)
|
|
39
|
+
return;
|
|
40
|
+
const term = iri(nodeIri(node.id));
|
|
41
|
+
dataset.addQuad(term, iri(RDF + 'type'), iri(CG + 'CodeSymbol'));
|
|
42
|
+
const value = node.codeSymbol;
|
|
43
|
+
if (value === undefined)
|
|
44
|
+
return; // The shape reports missing required metadata.
|
|
45
|
+
for (const [key, item] of Object.entries({ codeVersion: value.schemaVersion, codeProvider: value.provider,
|
|
46
|
+
codeRepository: value.repositoryId, nativeSymbol: value.nativeSymbol, codeIdentityScope: value.identityScope,
|
|
47
|
+
codeDocument: value.documentPath, codeIndex: value.indexDigest, codeResolution: value.resolution }))
|
|
48
|
+
property(term, key, item);
|
|
42
49
|
}
|
|
43
50
|
export function invalidCodeRange(range) {
|
|
44
51
|
if (range.startColumn === undefined && range.endColumn === undefined && range.columnEncoding === undefined)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { snapshotContext } from './snapshot-context.js';
|
|
2
|
-
import {
|
|
2
|
+
import { stableJsonDigest } from '../infrastructure/util.js';
|
|
3
3
|
import { RdfEvidenceGraph } from './rdf.js';
|
|
4
4
|
import { coverageForFacts } from './coverage.js';
|
|
5
5
|
import { parseGraphNeighborsRequest } from './neighborhood-request.js';
|
|
@@ -65,8 +65,8 @@ export class SnapshotGraphExplorationService {
|
|
|
65
65
|
const { continuation: _continuation, ...query } = request;
|
|
66
66
|
// Bind to actual projected facts too: even a corrupt store reusing a manifest digest
|
|
67
67
|
// must not silently turn a saved continuation into a different neighborhood/frontier.
|
|
68
|
-
const binding =
|
|
69
|
-
facts: all, diagnostics: saved.diagnostics, unresolved: saved.unresolved })
|
|
68
|
+
const binding = stableJsonDigest({ version: GRAPH_NEIGHBORHOOD_VERSION, query, manifest: saved.manifest,
|
|
69
|
+
facts: all, diagnostics: saved.diagnostics, unresolved: saved.unresolved });
|
|
70
70
|
const offset = pageOffset(request.continuation, binding, selected.length, request.maxFacts);
|
|
71
71
|
const facts = selected.slice(offset, offset + request.maxFacts);
|
|
72
72
|
const end = offset + facts.length;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Quad } from 'n3';
|
|
2
|
+
import type { RdfPartitions } from './rdf-partitions.js';
|
|
3
|
+
export declare function assertPartitionLocalShapes(shapes: readonly Quad[]): void;
|
|
4
|
+
export interface ShaclIssue {
|
|
5
|
+
category: 'extraction-gap';
|
|
6
|
+
focus?: string;
|
|
7
|
+
path?: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
/** Run the same strict validator/shapes on complete subjects with global class membership. */
|
|
11
|
+
export declare function validatePartitions(partitions: RdfPartitions, types: ReadonlyMap<string, ReadonlySet<string>>, progress?: (completed: number) => void): Promise<{
|
|
12
|
+
conforms: boolean;
|
|
13
|
+
issues: ShaclIssue[];
|
|
14
|
+
}>;
|
|
@@ -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
|
+
}
|
package/dist/graph/paths.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { snapshotContext } from './snapshot-context.js';
|
|
2
|
-
import {
|
|
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';
|
|
@@ -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 =
|
|
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 => {
|