@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,49 @@
|
|
|
1
|
+
const unknown = (...reasons) => ({ status: 'unknown', reasons });
|
|
2
|
+
function interval(constraint) {
|
|
3
|
+
if (constraint.operator === 'at-most')
|
|
4
|
+
return [-Infinity, constraint.value];
|
|
5
|
+
if (constraint.operator === 'at-least')
|
|
6
|
+
return [constraint.value, Infinity];
|
|
7
|
+
return [constraint.value, constraint.value];
|
|
8
|
+
}
|
|
9
|
+
function contextProblems(producer, consumer) {
|
|
10
|
+
const problems = [];
|
|
11
|
+
if (!producer.contract.version || !consumer.contract.version)
|
|
12
|
+
problems.push('unknown-version');
|
|
13
|
+
if (!producer.domain || !consumer.domain || !producer.stage || !consumer.stage ||
|
|
14
|
+
!producer.constraint || !consumer.constraint || !producer.constraint.unit || !consumer.constraint.unit)
|
|
15
|
+
return [...problems, 'missing-context'];
|
|
16
|
+
if (producer.domain !== consumer.domain)
|
|
17
|
+
problems.push('different-domain');
|
|
18
|
+
if (producer.constraint.metric !== consumer.constraint.metric)
|
|
19
|
+
problems.push('different-metric');
|
|
20
|
+
if (producer.constraint.unit !== consumer.constraint.unit)
|
|
21
|
+
problems.push('different-unit');
|
|
22
|
+
for (const { value } of [producer.constraint, consumer.constraint]) {
|
|
23
|
+
if (!Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER)
|
|
24
|
+
problems.push('unsupported-number');
|
|
25
|
+
}
|
|
26
|
+
return [...new Set(problems)];
|
|
27
|
+
}
|
|
28
|
+
/** Arithmetic on declared numeric sets only. No unit conversion, policy approval or implementation inference. */
|
|
29
|
+
export function compareDeclaredConstraints(producer, consumer) {
|
|
30
|
+
if (producer.role !== 'guarantee' || consumer.role !== 'requirement')
|
|
31
|
+
return unknown('wrong-roles');
|
|
32
|
+
const keys = ['scope', 'id', 'kind', 'version'];
|
|
33
|
+
if (keys.some(key => producer.contract[key] !== consumer.contract[key]))
|
|
34
|
+
return unknown('different-contracts');
|
|
35
|
+
const problems = contextProblems(producer, consumer);
|
|
36
|
+
if (problems.length)
|
|
37
|
+
return unknown(...problems);
|
|
38
|
+
if (producer.stage !== consumer.stage)
|
|
39
|
+
return consumer.revalidation === 'required'
|
|
40
|
+
? { status: 'revalidation-required', reasons: ['different-stage', 'consumer-revalidation'] } : unknown('different-stage');
|
|
41
|
+
const [pl, pu] = interval(producer.constraint), [cl, cu] = interval(consumer.constraint);
|
|
42
|
+
if (pu < cl || cu < pl)
|
|
43
|
+
return { status: 'disjoint-constraints', reasons: ['disjoint'] };
|
|
44
|
+
if (cl <= pl && pu <= cu)
|
|
45
|
+
return { status: 'compatible-constraints', reasons: ['subset'] };
|
|
46
|
+
if (consumer.revalidation === 'required')
|
|
47
|
+
return { status: 'revalidation-required', reasons: ['overlap-only', 'consumer-revalidation'] };
|
|
48
|
+
return { status: 'insufficient-guarantee', reasons: ['overlap-only'] };
|
|
49
|
+
}
|
package/dist/graph/coverage.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type ContextPackCoverage, type ContextPackLane, type GraphFact } from '@weftlet/core';
|
|
1
|
+
import { type ContextPackCoverage, type ContextPackLane, type GraphFact, type GraphSnapshot } from '@weftlet/core';
|
|
2
2
|
export declare const contextPackLanes: readonly ContextPackLane[];
|
|
3
3
|
export declare function isDeclaredFact(fact: GraphFact): boolean;
|
|
4
4
|
export declare function contextPackLaneForFact(fact: GraphFact): ContextPackLane | undefined;
|
|
5
5
|
export declare function coverageDescription(coverage: ContextPackCoverage): string;
|
|
6
6
|
/** Reporting only: never changes a fact's authority, identity, primary lane or stored bytes. */
|
|
7
7
|
export declare function isImplementationEvidence(fact: GraphFact): boolean;
|
|
8
|
-
export declare function coverageForFacts(facts: readonly GraphFact[]): ContextPackCoverage;
|
|
8
|
+
export declare function coverageForFacts(facts: readonly GraphFact[], snapshot?: GraphSnapshot): ContextPackCoverage;
|
package/dist/graph/coverage.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { repositoryFileNode } from '@weftlet/core';
|
|
2
2
|
import { contextPackEvidenceAuthority } from './authority.js';
|
|
3
|
+
import { projectCodeCoverage, codeCoverageDescription } from './code-coverage-projection.js';
|
|
3
4
|
export const contextPackLanes = [
|
|
4
5
|
'productIntent',
|
|
5
6
|
'contractsAndInvariants',
|
|
@@ -34,6 +35,8 @@ export function contextPackLaneForFact(fact) {
|
|
|
34
35
|
if (isValidationFact(fact)) {
|
|
35
36
|
return 'validationEvidence';
|
|
36
37
|
}
|
|
38
|
+
if (kinds.has('CodeSymbol'))
|
|
39
|
+
return 'implementationAndConsumers';
|
|
37
40
|
if (kinds.has('Contract') ||
|
|
38
41
|
kinds.has('Invariant') ||
|
|
39
42
|
kinds.has('PersistenceModel') ||
|
|
@@ -51,9 +54,10 @@ export function contextPackLaneForFact(fact) {
|
|
|
51
54
|
return 'implementationAndConsumers';
|
|
52
55
|
}
|
|
53
56
|
export function coverageDescription(coverage) {
|
|
54
|
-
|
|
57
|
+
const page = coverage.scope === 'returned-page' && coverage.countsAreNonExclusive
|
|
55
58
|
? 'Counts describe evidence on this returned page and may overlap; a code relationship can also describe a contract. They are not a percentage, a count of display groups, or proof that behavior is correct.'
|
|
56
59
|
: 'Coverage scope and overlap metadata are unavailable for this Context Pack; re-query saved evidence for an explicit report.';
|
|
60
|
+
return `${page} ${codeCoverageDescription(coverage.code)}`;
|
|
57
61
|
}
|
|
58
62
|
/** Reporting only: never changes a fact's authority, identity, primary lane or stored bytes. */
|
|
59
63
|
export function isImplementationEvidence(fact) {
|
|
@@ -69,12 +73,12 @@ export function isImplementationEvidence(fact) {
|
|
|
69
73
|
const nodes = [fact.subject, fact.object];
|
|
70
74
|
// Explicit adapter authority also supports logical component/provider identities.
|
|
71
75
|
if (fact.authority === 'implemented')
|
|
72
|
-
return nodes.some(node => ['Component', 'Provider'].includes(node.kind));
|
|
76
|
+
return nodes.some(node => ['Component', 'Provider', 'CodeSymbol'].includes(node.kind));
|
|
73
77
|
// Legacy deterministic adapters omitted authority. Require their cited physical file,
|
|
74
78
|
// rather than treating every fact in the implementation display group as code evidence.
|
|
75
79
|
return nodes.some(node => node.kind === 'Component' && node.id === file.id);
|
|
76
80
|
}
|
|
77
|
-
export function coverageForFacts(facts) {
|
|
81
|
+
export function coverageForFacts(facts, snapshot) {
|
|
78
82
|
const counts = {
|
|
79
83
|
productIntent: 0, contractsAndInvariants: 0, implementationAndConsumers: 0, validationEvidence: 0,
|
|
80
84
|
};
|
|
@@ -86,6 +90,7 @@ export function coverageForFacts(facts) {
|
|
|
86
90
|
counts.implementationAndConsumers++;
|
|
87
91
|
}
|
|
88
92
|
return { scope: 'returned-page', countsAreNonExclusive: true, counts,
|
|
93
|
+
...(snapshot ? { code: projectCodeCoverage(snapshot).overview } : {}),
|
|
89
94
|
gaps: [
|
|
90
95
|
...contextPackLanes.filter(lane => counts[lane] === 0)
|
|
91
96
|
.map(lane => `No ${lane} evidence appears on this returned page.`),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ContextPack, DiagnosticKind, DiagnosticReport, GraphDiagnostic, GraphFact, GraphSnapshot, ImpactScanRequest } from "@weftlet/core";
|
|
2
2
|
export declare const diagnosticKinds: readonly DiagnosticKind[];
|
|
3
3
|
export declare const MAX_DIAGNOSTICS_PER_KIND = 5;
|
|
4
|
-
export declare function selectDiagnostics(snapshot: Pick<GraphSnapshot, 'unresolved' | 'diagnostics'>, request: ImpactScanRequest, facts: readonly GraphFact[], focus: readonly string[], queryDiagnostics: readonly GraphDiagnostic[]): DiagnosticReport;
|
|
4
|
+
export declare function selectDiagnostics(snapshot: Pick<GraphSnapshot, 'unresolved' | 'diagnostics'>, request: ImpactScanRequest, facts: readonly GraphFact[], focus: readonly string[], queryDiagnostics: readonly GraphDiagnostic[], exactNodeIds?: ReadonlySet<string>): DiagnosticReport;
|
|
5
5
|
export declare function diagnosticSummary(pack: Pick<ContextPack, 'diagnostics'>): readonly string[];
|
|
@@ -6,7 +6,13 @@ export const diagnosticKinds = [
|
|
|
6
6
|
'drift',
|
|
7
7
|
];
|
|
8
8
|
export const MAX_DIAGNOSTICS_PER_KIND = 5;
|
|
9
|
-
function
|
|
9
|
+
function overlapsReturnedEvidence(item, facts) {
|
|
10
|
+
return item.provenance.some(span => facts.some(fact => fact.provenance.path === span.path && fact.provenance.startLine <= span.endLine &&
|
|
11
|
+
fact.provenance.endLine >= span.startLine));
|
|
12
|
+
}
|
|
13
|
+
function relevantDiagnostic(item, request, facts, focus, exactNodeIds) {
|
|
14
|
+
if (exactNodeIds)
|
|
15
|
+
return item.relatedNodeIds.some(id => exactNodeIds.has(id)) || overlapsReturnedEvidence(item, facts);
|
|
10
16
|
const hasSelectors = request.ownerDocument ||
|
|
11
17
|
request.changedFiles?.length ||
|
|
12
18
|
request.symbols?.length ||
|
|
@@ -23,19 +29,17 @@ function relevantDiagnostic(item, request, facts, focus) {
|
|
|
23
29
|
return true;
|
|
24
30
|
// A source document can contain many independent capabilities. Match the cited passage,
|
|
25
31
|
// not every diagnostic from a philosophy document that contributed one selected fact.
|
|
26
|
-
if (item
|
|
27
|
-
fact.provenance.startLine <= span.endLine &&
|
|
28
|
-
fact.provenance.endLine >= span.startLine)))
|
|
32
|
+
if (overlapsReturnedEvidence(item, facts))
|
|
29
33
|
return true;
|
|
30
34
|
return (item.provenance.some((span) => span.path === request.ownerDocument) &&
|
|
31
35
|
(focus.length === 0 || focus.some((term) => item.reason.toLowerCase().includes(term))));
|
|
32
36
|
}
|
|
33
|
-
export function selectDiagnostics(snapshot, request, facts, focus, queryDiagnostics) {
|
|
37
|
+
export function selectDiagnostics(snapshot, request, facts, focus, queryDiagnostics, exactNodeIds) {
|
|
34
38
|
const report = { unclassifiedLegacyCount: snapshot.unresolved.length };
|
|
35
39
|
const unique = [...new Map((snapshot.diagnostics ?? []).map((item) => [item.id, item])).values()];
|
|
36
40
|
for (const kind of diagnosticKinds) {
|
|
37
41
|
const candidates = unique.filter((item) => item.kind === kind);
|
|
38
|
-
const relevant = candidates.filter((item) => relevantDiagnostic(item, request, facts, focus));
|
|
42
|
+
const relevant = candidates.filter((item) => relevantDiagnostic(item, request, facts, focus, exactNodeIds));
|
|
39
43
|
const ordered = [
|
|
40
44
|
...queryDiagnostics.filter((item) => item.kind === kind),
|
|
41
45
|
...relevant.sort((a, b) => a.id.localeCompare(b.id)),
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { codeEvidenceDescription } from './code-description.js';
|
|
2
|
+
/** Display the same structured assertion returned by queries; unknown values are never inferred. */
|
|
3
|
+
export function factEvidenceDescription(fact) {
|
|
4
|
+
const obligation = fact.contractObligation?.declaration;
|
|
5
|
+
if (!obligation)
|
|
6
|
+
return codeEvidenceDescription(fact);
|
|
7
|
+
const constraint = obligation.constraint;
|
|
8
|
+
return `declared ${obligation.role}: ${obligation.statement}; ${obligation.contract.kind} contract ` +
|
|
9
|
+
`${obligation.contract.scope}/${obligation.contract.id}; version ${obligation.contract.version ?? 'unknown'}; ` +
|
|
10
|
+
`domain ${obligation.domain ?? 'unknown'}; stage ${obligation.stage ?? 'unknown'}; ` +
|
|
11
|
+
(constraint ? `${constraint.metric} ${constraint.operator} ${constraint.value}; unit ${constraint.unit ?? 'unknown'}; ` : 'constraint unknown; ') +
|
|
12
|
+
`consumer revalidation ${obligation.revalidation ?? 'unknown'}; fulfillment, delivery and compatibility not evaluated`;
|
|
13
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { sha256, stableJson } from '@weftlet/core';
|
|
2
2
|
const publicKinds = new Set([
|
|
3
3
|
'Project', 'Document', 'Capability', 'Workflow', 'Component', 'Contract', 'Evidence',
|
|
4
|
-
'Invariant', 'Snapshot', 'Provider', 'PersistenceModel', 'Scope',
|
|
4
|
+
'Invariant', 'Snapshot', 'Provider', 'PersistenceModel', 'Scope', 'CodeSymbol',
|
|
5
5
|
]);
|
|
6
6
|
/** Find the single-kind constraint's conflicts without constructing RDF or changing any fact. */
|
|
7
7
|
export function inspectGraphIdentities(facts, options) {
|
|
@@ -0,0 +1,38 @@
|
|
|
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 { documentSelectionDescription } from './document-role-description.js';
|
|
6
|
+
import { diagnosticSummary } from './diagnostics.js';
|
|
7
|
+
const quoted = (value) => JSON.stringify(value).replace(/[\\`*_{}\[\]()<>!|]/g, '\\$&');
|
|
8
|
+
export function renderGraphNeighborhood(result) {
|
|
9
|
+
const { snapshot, focus, selection, frontier, evidenceState } = result;
|
|
10
|
+
return [
|
|
11
|
+
'# Exact graph neighborhood',
|
|
12
|
+
`Snapshot: ${quoted(snapshot.snapshotId)} (${snapshot.commitSha})`,
|
|
13
|
+
`Configuration: ${snapshot.configurationDigest ?? 'legacy/unknown'}; scope: ${snapshot.scope?.map(quoted).join(', ') ?? 'legacy/unknown'}`,
|
|
14
|
+
...[documentSelectionDescription(snapshot.documentSelection)].filter(Boolean),
|
|
15
|
+
`Node: ${quoted(focus.nodeId)}; ${focus.status}`,
|
|
16
|
+
`Direction: ${selection.direction}; predicates: ${selection.predicates.join(', ') || 'all'}`,
|
|
17
|
+
`Incident assertions: ${selection.incidentFacts}; matching: ${selection.matchingFacts}; filtered out: ${selection.filteredFacts}`,
|
|
18
|
+
`Page: ${selection.returnedFacts} assertions at offset ${selection.offset}; remaining: ${selection.remainingFacts}; truncated: ${selection.truncated}`,
|
|
19
|
+
...(selection.continuation ? [`Continuation: ${selection.continuation}`] : []),
|
|
20
|
+
...(evidenceState ? [
|
|
21
|
+
`Stale relative to checkout: ${evidenceState.stale}; configuration matches: ${evidenceState.configurationMatches}; extraction compatible: ${evidenceState.extractionCompatible ?? 'unknown'}`,
|
|
22
|
+
`Excluded working changes: ${evidenceState.excludedWorkingChanges.map(quoted).join(', ') || '(none)'}`,
|
|
23
|
+
...(evidenceState.overlayFreshness ? [`Overlay freshness: ${evidenceState.overlayFreshness}`] : []),
|
|
24
|
+
] : []),
|
|
25
|
+
'This is one-hop saved evidence. It does not validate the current diff or establish complete repository coverage.',
|
|
26
|
+
coverageDescription(result.coverage),
|
|
27
|
+
...result.coverage.gaps.map(gap => `GAP: ${gap}`),
|
|
28
|
+
'', '## Cited assertions',
|
|
29
|
+
...result.facts.map(fact => `- [${contextPackEvidenceAuthority(fact)}] ${quoted(fact.subject.id)} —${fact.predicate}→ ${quoted(fact.object.id)} ` +
|
|
30
|
+
`(${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
|
+
'', '## Scoped diagnostics',
|
|
32
|
+
...diagnosticSummary(result).map(item => `- ${quoted(item)}`),
|
|
33
|
+
'', '## Further exploration',
|
|
34
|
+
`Selected neighbors: ${frontier.totalNeighbors}; not yet returned across these pages: ${frontier.remainingNeighbors}.`,
|
|
35
|
+
...frontier.neighbors.map(node => `- ${quoted(node.nodeId)}: ${node.otherIncidentFacts} incident assertions outside this focus neighborhood; use graph.neighbors with the same snapshot to inspect them.`),
|
|
36
|
+
'Counts concern the saved scope. Absence or an exhausted page is not proof that a product dependency does not exist.',
|
|
37
|
+
].join('\n');
|
|
38
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Shared validation/defaults for public API, CLI and MCP. Never interpret IDs as text search. */
|
|
2
|
+
export declare function parseGraphNeighborsRequest(value: unknown): {
|
|
3
|
+
continuation?: string | undefined;
|
|
4
|
+
snapshotId: string;
|
|
5
|
+
nodeId: string;
|
|
6
|
+
direction: "incoming" | "outgoing" | "both";
|
|
7
|
+
predicates: ("references" | "realizes" | "implementedBy" | "consumes" | "emits" | "persistsTo" | "governedBy" | "definedBy" | "verifiedBy" | "operationallyOwnedBy" | "decisionAuthority" | "routesThrough" | "authorizedBy" | "sourcedFrom" | "contains" | "calls")[];
|
|
8
|
+
maxFacts: number;
|
|
9
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { graphPredicates } from '@weftlet/core';
|
|
2
|
+
const fields = new Set(['snapshotId', 'nodeId', 'direction', 'predicates', 'maxFacts', 'continuation']);
|
|
3
|
+
function identifier(value, name, maximum) {
|
|
4
|
+
if (typeof value !== 'string' || !value.trim() || value.length > maximum) {
|
|
5
|
+
throw new Error(`${name} must be a non-empty string of at most ${maximum} characters`);
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
/** Shared validation/defaults for public API, CLI and MCP. Never interpret IDs as text search. */
|
|
10
|
+
export function parseGraphNeighborsRequest(value) {
|
|
11
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
12
|
+
throw new Error('graph.neighbors arguments must be an object');
|
|
13
|
+
}
|
|
14
|
+
const record = value;
|
|
15
|
+
const unexpected = Object.keys(record).filter(field => !fields.has(field));
|
|
16
|
+
if (unexpected.length)
|
|
17
|
+
throw new Error(`Unknown graph.neighbors fields: ${unexpected.join(', ')}`);
|
|
18
|
+
const snapshotId = identifier(record.snapshotId, 'snapshotId', 256);
|
|
19
|
+
const nodeId = identifier(record.nodeId, 'nodeId', 8192);
|
|
20
|
+
const direction = record.direction === undefined ? 'both' : record.direction;
|
|
21
|
+
if (direction !== 'incoming' && direction !== 'outgoing' && direction !== 'both') {
|
|
22
|
+
throw new Error('direction must be incoming, outgoing or both');
|
|
23
|
+
}
|
|
24
|
+
const predicates = record.predicates === undefined ? [] : record.predicates;
|
|
25
|
+
if (!Array.isArray(predicates) || predicates.length > graphPredicates.length ||
|
|
26
|
+
predicates.some(item => !graphPredicates.includes(item))) {
|
|
27
|
+
throw new Error('predicates must be an array of supported graph predicates');
|
|
28
|
+
}
|
|
29
|
+
const maxFacts = record.maxFacts === undefined ? 60 : record.maxFacts;
|
|
30
|
+
if (!Number.isInteger(maxFacts) || Number(maxFacts) < 1 || Number(maxFacts) > 200) {
|
|
31
|
+
throw new Error('maxFacts must be an integer between 1 and 200');
|
|
32
|
+
}
|
|
33
|
+
const continuation = record.continuation === undefined ? undefined
|
|
34
|
+
: identifier(record.continuation, 'continuation', 2048);
|
|
35
|
+
return { snapshotId, nodeId, direction, predicates: [...new Set(predicates)].sort(),
|
|
36
|
+
maxFacts: Number(maxFacts), ...(continuation === undefined ? {} : { continuation }) };
|
|
37
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { GraphExplorationService, GraphNeighborhood, GraphNeighborsRequest, SnapshotStore } from '@weftlet/core';
|
|
2
|
+
export declare const GRAPH_NEIGHBORHOOD_VERSION = "exact-neighborhood-v1";
|
|
3
|
+
/** One-hop, exact-ID exploration over the same lossless RDF projection as impact.scan. */
|
|
4
|
+
export declare class SnapshotGraphExplorationService implements GraphExplorationService {
|
|
5
|
+
private readonly store;
|
|
6
|
+
constructor(store: Pick<SnapshotStore, 'read'>);
|
|
7
|
+
neighbors(input: GraphNeighborsRequest): Promise<GraphNeighborhood>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
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 { parseGraphNeighborsRequest } from './neighborhood-request.js';
|
|
6
|
+
import { renderGraphNeighborhood } from './neighborhood-renderer.js';
|
|
7
|
+
import { selectDiagnostics } from './diagnostics.js';
|
|
8
|
+
export const GRAPH_NEIGHBORHOOD_VERSION = 'exact-neighborhood-v1';
|
|
9
|
+
function pageOffset(token, binding, length, pageSize) {
|
|
10
|
+
if (token === undefined)
|
|
11
|
+
return 0;
|
|
12
|
+
let value;
|
|
13
|
+
try {
|
|
14
|
+
if (!/^[A-Za-z0-9_-]+$/.test(token))
|
|
15
|
+
throw new Error();
|
|
16
|
+
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
|
+
throw new Error();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new Error('Invalid graph.neighbors continuation');
|
|
23
|
+
}
|
|
24
|
+
if (value.version !== GRAPH_NEIGHBORHOOD_VERSION || value.binding !== binding) {
|
|
25
|
+
throw new Error('Continuation does not match this snapshot, node, filters or page size; restart graph.neighbors');
|
|
26
|
+
}
|
|
27
|
+
if (!Number.isSafeInteger(value.offset) || Number(value.offset) < 1 || Number(value.offset) >= length ||
|
|
28
|
+
Number(value.offset) % pageSize !== 0)
|
|
29
|
+
throw new Error('Invalid graph.neighbors continuation offset');
|
|
30
|
+
return Number(value.offset);
|
|
31
|
+
}
|
|
32
|
+
function opposite(fact, nodeId) {
|
|
33
|
+
return fact.subject.id === nodeId ? fact.object.id : fact.subject.id;
|
|
34
|
+
}
|
|
35
|
+
/** One-hop, exact-ID exploration over the same lossless RDF projection as impact.scan. */
|
|
36
|
+
export class SnapshotGraphExplorationService {
|
|
37
|
+
store;
|
|
38
|
+
constructor(store) {
|
|
39
|
+
this.store = store;
|
|
40
|
+
}
|
|
41
|
+
async neighbors(input) {
|
|
42
|
+
const request = parseGraphNeighborsRequest(input);
|
|
43
|
+
const saved = await this.store.read(request.snapshotId);
|
|
44
|
+
if (!saved)
|
|
45
|
+
throw new Error('No graph snapshot is available for that identifier');
|
|
46
|
+
if (saved.manifest.snapshotId !== request.snapshotId)
|
|
47
|
+
throw new Error('Saved snapshot identity does not match the requested identifier');
|
|
48
|
+
const all = await new RdfEvidenceGraph(saved).facts();
|
|
49
|
+
const adjacency = new Map();
|
|
50
|
+
const factIds = new Set();
|
|
51
|
+
for (const fact of all) {
|
|
52
|
+
if (factIds.has(fact.id))
|
|
53
|
+
throw new Error('Duplicate fact identities prevent stable graph.neighbors pagination');
|
|
54
|
+
factIds.add(fact.id);
|
|
55
|
+
for (const nodeId of new Set([fact.subject.id, fact.object.id])) {
|
|
56
|
+
const adjacent = adjacency.get(nodeId) ?? [];
|
|
57
|
+
adjacent.push(fact);
|
|
58
|
+
adjacency.set(nodeId, adjacent);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const incident = adjacency.get(request.nodeId) ?? [];
|
|
62
|
+
const selected = incident.filter(fact => (request.direction === 'both' || (request.direction === 'outgoing' ? fact.subject.id : fact.object.id) === request.nodeId) &&
|
|
63
|
+
(!request.predicates.length || request.predicates.includes(fact.predicate)))
|
|
64
|
+
.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
65
|
+
const { continuation: _continuation, ...query } = request;
|
|
66
|
+
// Bind to actual projected facts too: even a corrupt store reusing a manifest digest
|
|
67
|
+
// must not silently turn a saved continuation into a different neighborhood/frontier.
|
|
68
|
+
const binding = stableJsonDigest({ version: GRAPH_NEIGHBORHOOD_VERSION, query, manifest: saved.manifest,
|
|
69
|
+
facts: all, diagnostics: saved.diagnostics, unresolved: saved.unresolved });
|
|
70
|
+
const offset = pageOffset(request.continuation, binding, selected.length, request.maxFacts);
|
|
71
|
+
const facts = selected.slice(offset, offset + request.maxFacts);
|
|
72
|
+
const end = offset + facts.length;
|
|
73
|
+
const next = end < selected.length ? Buffer.from(JSON.stringify({
|
|
74
|
+
version: GRAPH_NEIGHBORHOOD_VERSION, binding, offset: end,
|
|
75
|
+
})).toString('base64url') : undefined;
|
|
76
|
+
const neighborIds = (items) => new Set(items.map(fact => opposite(fact, request.nodeId))
|
|
77
|
+
.filter(id => id !== request.nodeId));
|
|
78
|
+
const totalNeighbors = neighborIds(selected).size;
|
|
79
|
+
const neighbors = [...neighborIds(facts)].sort().map(nodeId => ({
|
|
80
|
+
nodeId,
|
|
81
|
+
factIds: facts.filter(fact => opposite(fact, request.nodeId) === nodeId).map(fact => fact.id),
|
|
82
|
+
otherIncidentFacts: adjacency.get(nodeId).filter(fact => fact.subject.id !== request.nodeId && fact.object.id !== request.nodeId).length,
|
|
83
|
+
}));
|
|
84
|
+
const manifest = saved.manifest;
|
|
85
|
+
const plain = {
|
|
86
|
+
snapshot: snapshotContext(manifest),
|
|
87
|
+
focus: { nodeId: request.nodeId, status: incident.length ? 'present' : 'absent-from-snapshot' },
|
|
88
|
+
facts,
|
|
89
|
+
coverage: coverageForFacts(facts, saved),
|
|
90
|
+
diagnostics: selectDiagnostics(saved, {}, facts, [], [], new Set([request.nodeId, ...facts.flatMap(fact => [fact.subject.id, fact.object.id])])),
|
|
91
|
+
selection: { version: GRAPH_NEIGHBORHOOD_VERSION, direction: request.direction, predicates: request.predicates,
|
|
92
|
+
maxFacts: request.maxFacts, incidentFacts: incident.length, matchingFacts: selected.length,
|
|
93
|
+
filteredFacts: incident.length - selected.length, offset, returnedFacts: facts.length,
|
|
94
|
+
remainingFacts: selected.length - end, truncated: Boolean(next), ...(next ? { continuation: next } : {}) },
|
|
95
|
+
frontier: { scope: 'selected-one-hop-neighborhood', totalNeighbors,
|
|
96
|
+
remainingNeighbors: totalNeighbors - neighborIds(selected.slice(0, end)).size, neighbors },
|
|
97
|
+
};
|
|
98
|
+
return { ...plain, markdown: renderGraphNeighborhood(plain) };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Store, type NamedNode } from 'n3';
|
|
2
|
+
import { type GraphFact } from '@weftlet/core';
|
|
3
|
+
export declare function addObligationEvidence(dataset: Store, record: NamedNode, fact: GraphFact): void;
|
|
4
|
+
/** Structural integrity only: declarations cannot prove fulfillment, compatibility or runtime delivery. */
|
|
5
|
+
export declare function obligationEvidenceIssues(fact: GraphFact, repositoryId: string): readonly string[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { DataFactory } from 'n3';
|
|
2
|
+
import { obligationContractNode, parseObligationDeclaration, repositoryFileNode } from '@weftlet/core';
|
|
3
|
+
import { CG, RDF, XSD } from './shacl-shapes.js';
|
|
4
|
+
const { namedNode: iri, literal } = DataFactory;
|
|
5
|
+
export function addObligationEvidence(dataset, record, fact) {
|
|
6
|
+
const evidence = fact.contractObligation;
|
|
7
|
+
if (!evidence && fact.extractor.id !== 'document-obligation-extractor')
|
|
8
|
+
return;
|
|
9
|
+
dataset.addQuad(record, iri(RDF + 'type'), iri(CG + 'ContractObligation'));
|
|
10
|
+
if (!evidence)
|
|
11
|
+
return;
|
|
12
|
+
const prop = (node, key, value, datatype = 'string') => {
|
|
13
|
+
if (value !== undefined)
|
|
14
|
+
dataset.addQuad(node, iri(CG + key), literal(String(value), iri(XSD + datatype)));
|
|
15
|
+
};
|
|
16
|
+
const item = evidence.declaration ?? {};
|
|
17
|
+
const contract = item.contract, constraint = item.constraint;
|
|
18
|
+
prop(record, 'obligationVersion', item.schemaVersion, 'integer');
|
|
19
|
+
for (const [key, value] of Object.entries({ obligationMethod: evidence.method, obligationRepository: evidence.repositoryId,
|
|
20
|
+
obligationId: item.id, obligationRole: item.role, obligationStatement: item.statement,
|
|
21
|
+
obligationScope: contract?.scope, obligationContract: contract?.id, obligationTransport: contract?.kind,
|
|
22
|
+
obligationContractVersion: contract?.version, obligationDomain: item.domain, obligationStage: item.stage,
|
|
23
|
+
obligationRevalidation: item.revalidation, obligationMetric: constraint?.metric,
|
|
24
|
+
obligationOperator: constraint?.operator, obligationUnit: constraint?.unit }))
|
|
25
|
+
prop(record, key, value);
|
|
26
|
+
prop(record, 'obligationValue', constraint?.value, 'double');
|
|
27
|
+
if (evidence.endpointSource) {
|
|
28
|
+
const endpoint = iri(`${record.value}:obligation-endpoint`);
|
|
29
|
+
dataset.addQuad(record, iri(CG + 'obligationEndpoint'), endpoint);
|
|
30
|
+
dataset.addQuad(endpoint, iri(RDF + 'type'), iri(CG + 'ObligationEndpoint'));
|
|
31
|
+
for (const [key, value] of Object.entries(evidence.endpointSource))
|
|
32
|
+
prop(endpoint, key, value, typeof value === 'number' ? 'integer' : 'string');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function identityMatches(fact, declaration, repositoryId) {
|
|
36
|
+
const expected = repositoryFileNode(declaration.endpoint);
|
|
37
|
+
return fact.contractObligation.repositoryId === repositoryId &&
|
|
38
|
+
fact.object.id === obligationContractNode(repositoryId, declaration.contract).id && fact.object.kind === 'Contract' &&
|
|
39
|
+
fact.subject.id === expected.id && fact.subject.kind === expected.kind &&
|
|
40
|
+
fact.predicate === (declaration.role === 'guarantee' ? 'emits' : 'consumes');
|
|
41
|
+
}
|
|
42
|
+
/** Structural integrity only: declarations cannot prove fulfillment, compatibility or runtime delivery. */
|
|
43
|
+
export function obligationEvidenceIssues(fact, repositoryId) {
|
|
44
|
+
const evidence = fact.contractObligation;
|
|
45
|
+
if (!evidence)
|
|
46
|
+
return fact.extractor.id === 'document-obligation-extractor' ? ['Obligation extraction requires declaration metadata'] : [];
|
|
47
|
+
let declaration;
|
|
48
|
+
try {
|
|
49
|
+
declaration = parseObligationDeclaration(evidence.declaration);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return ['Malformed obligation declaration'];
|
|
53
|
+
}
|
|
54
|
+
const issues = [], endpoint = evidence.endpointSource;
|
|
55
|
+
if (!identityMatches(fact, declaration, repositoryId))
|
|
56
|
+
issues.push('Obligation role or scoped contract/endpoint identity does not match its declaration');
|
|
57
|
+
if (!endpoint || endpoint.path !== declaration.endpoint || endpoint.commitSha !== fact.provenance.commitSha ||
|
|
58
|
+
endpoint.endLine < endpoint.startLine)
|
|
59
|
+
issues.push('Obligation endpoint citation does not match its declared file/revision');
|
|
60
|
+
if (fact.codeBinding || fact.codeCall || fact.codeOccurrence)
|
|
61
|
+
issues.push('Obligation declarations must remain separate from implementation evidence');
|
|
62
|
+
if (fact.sourceRole === 'validation')
|
|
63
|
+
issues.push('Validation evidence cannot declare product obligations');
|
|
64
|
+
if (fact.authority === 'draft' && fact.sourceAuthority !== 'draft')
|
|
65
|
+
issues.push('Draft obligations require draft source authority');
|
|
66
|
+
return issues;
|
|
67
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Shapes target only obligation records, keeping ordinary-fact validation cost unchanged. */
|
|
2
|
+
export declare const obligationShapes = "\ncg:ContractObligationShape a sh:NodeShape; sh:targetClass cg:ContractObligation;\n sh:property [ sh:path cg:obligationVersion; sh:minCount 1; sh:maxCount 1; sh:hasValue 1 ];\n sh:property [ sh:path cg:obligationMethod; sh:minCount 1; sh:maxCount 1; sh:hasValue \"explicit-markdown\" ];\n sh:property [ sh:path cg:obligationRole; sh:minCount 1; sh:maxCount 1; sh:in (\"guarantee\" \"requirement\") ];\n sh:property [ sh:path cg:obligationId; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];\n sh:property [ sh:path cg:obligationStatement; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];\n sh:property [ sh:path cg:obligationRepository; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];\n sh:property [ sh:path cg:obligationScope; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];\n sh:property [ sh:path cg:obligationContract; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];\n sh:property [ sh:path cg:obligationTransport; sh:minCount 1; sh:maxCount 1; sh:in (\"queue\" \"stored-data\") ];\n sh:property [ sh:path cg:obligationContractVersion; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];\n sh:property [ sh:path cg:obligationDomain; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];\n sh:property [ sh:path cg:obligationStage; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];\n sh:property [ sh:path cg:obligationRevalidation; sh:maxCount 1; sh:in (\"required\" \"not-required\") ];\n sh:property [ sh:path cg:obligationEndpoint; sh:minCount 1; sh:maxCount 1; sh:class cg:ObligationEndpoint ];\n sh:property [ sh:path cg:authority; sh:in (\"inferred\" \"draft\") ];\n sh:property [ sh:path cg:extractionReview; sh:minCount 1; sh:hasValue \"deterministic\" ];\n sh:property [ sh:path rdf:predicate; sh:in (cg:emits cg:consumes) ].\ncg:ObligationEndpointShape a sh:NodeShape; sh:targetClass cg:ObligationEndpoint;\n sh:property [ sh:path cg:path; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];\n sh:property [ sh:path cg:digest; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];\n sh:property [ sh:path cg:commitSha; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];\n sh:property [ sh:path cg:startLine; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:integer; sh:minInclusive 1 ];\n sh:property [ sh:path cg:endLine; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:integer; sh:minInclusive 1 ].\ncg:ObligationConstraintShape a sh:NodeShape; sh:targetSubjectsOf cg:obligationMetric, cg:obligationOperator, cg:obligationValue, cg:obligationUnit;\n sh:property [ sh:path cg:obligationMetric; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];\n sh:property [ sh:path cg:obligationOperator; sh:minCount 1; sh:maxCount 1; sh:in (\"at-most\" \"at-least\" \"equal\") ];\n sh:property [ sh:path cg:obligationValue; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:double ];\n sh:property [ sh:path cg:obligationUnit; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ].\n";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Shapes target only obligation records, keeping ordinary-fact validation cost unchanged. */
|
|
2
|
+
export const obligationShapes = `
|
|
3
|
+
cg:ContractObligationShape a sh:NodeShape; sh:targetClass cg:ContractObligation;
|
|
4
|
+
sh:property [ sh:path cg:obligationVersion; sh:minCount 1; sh:maxCount 1; sh:hasValue 1 ];
|
|
5
|
+
sh:property [ sh:path cg:obligationMethod; sh:minCount 1; sh:maxCount 1; sh:hasValue "explicit-markdown" ];
|
|
6
|
+
sh:property [ sh:path cg:obligationRole; sh:minCount 1; sh:maxCount 1; sh:in ("guarantee" "requirement") ];
|
|
7
|
+
sh:property [ sh:path cg:obligationId; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];
|
|
8
|
+
sh:property [ sh:path cg:obligationStatement; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];
|
|
9
|
+
sh:property [ sh:path cg:obligationRepository; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];
|
|
10
|
+
sh:property [ sh:path cg:obligationScope; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];
|
|
11
|
+
sh:property [ sh:path cg:obligationContract; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];
|
|
12
|
+
sh:property [ sh:path cg:obligationTransport; sh:minCount 1; sh:maxCount 1; sh:in ("queue" "stored-data") ];
|
|
13
|
+
sh:property [ sh:path cg:obligationContractVersion; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];
|
|
14
|
+
sh:property [ sh:path cg:obligationDomain; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];
|
|
15
|
+
sh:property [ sh:path cg:obligationStage; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];
|
|
16
|
+
sh:property [ sh:path cg:obligationRevalidation; sh:maxCount 1; sh:in ("required" "not-required") ];
|
|
17
|
+
sh:property [ sh:path cg:obligationEndpoint; sh:minCount 1; sh:maxCount 1; sh:class cg:ObligationEndpoint ];
|
|
18
|
+
sh:property [ sh:path cg:authority; sh:in ("inferred" "draft") ];
|
|
19
|
+
sh:property [ sh:path cg:extractionReview; sh:minCount 1; sh:hasValue "deterministic" ];
|
|
20
|
+
sh:property [ sh:path rdf:predicate; sh:in (cg:emits cg:consumes) ].
|
|
21
|
+
cg:ObligationEndpointShape a sh:NodeShape; sh:targetClass cg:ObligationEndpoint;
|
|
22
|
+
sh:property [ sh:path cg:path; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];
|
|
23
|
+
sh:property [ sh:path cg:digest; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];
|
|
24
|
+
sh:property [ sh:path cg:commitSha; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];
|
|
25
|
+
sh:property [ sh:path cg:startLine; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:integer; sh:minInclusive 1 ];
|
|
26
|
+
sh:property [ sh:path cg:endLine; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:integer; sh:minInclusive 1 ].
|
|
27
|
+
cg:ObligationConstraintShape a sh:NodeShape; sh:targetSubjectsOf cg:obligationMetric, cg:obligationOperator, cg:obligationValue, cg:obligationUnit;
|
|
28
|
+
sh:property [ sh:path cg:obligationMetric; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ];
|
|
29
|
+
sh:property [ sh:path cg:obligationOperator; sh:minCount 1; sh:maxCount 1; sh:in ("at-most" "at-least" "equal") ];
|
|
30
|
+
sh:property [ sh:path cg:obligationValue; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:double ];
|
|
31
|
+
sh:property [ sh:path cg:obligationUnit; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ].
|
|
32
|
+
`;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function parseGraphPathsRequest(value: unknown): {
|
|
2
|
+
continuation?: string | undefined;
|
|
3
|
+
snapshotId: string;
|
|
4
|
+
fromNodeId: string;
|
|
5
|
+
toNodeId: string;
|
|
6
|
+
direction: "incoming" | "outgoing" | "both";
|
|
7
|
+
predicates: ("references" | "realizes" | "implementedBy" | "consumes" | "emits" | "persistsTo" | "governedBy" | "definedBy" | "verifiedBy" | "operationallyOwnedBy" | "decisionAuthority" | "routesThrough" | "authorizedBy" | "sourcedFrom" | "contains" | "calls")[];
|
|
8
|
+
maxDepth: number;
|
|
9
|
+
maxPaths: number;
|
|
10
|
+
maxSteps: number;
|
|
11
|
+
};
|