@weftlet/toolkit 0.6.1 → 0.7.0
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 +168 -1
- package/THIRD_PARTY_NOTICES.md +1 -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 +20 -3
- 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 +36 -22
- 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/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/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 +5 -0
- package/dist/graph/code-rdf.js +72 -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/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.js +26 -0
- 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/index.d.ts +23 -0
- package/dist/index.js +22 -0
- package/dist/infrastructure/json-digest.d.ts +2 -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/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 +38 -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/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 +437 -14
- package/docs/extraction-manifest.json +248 -21
- package/package.json +8 -4
- package/templates/context-graph-development/SKILL.md +30 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { DataFactory } from 'n3';
|
|
2
|
+
import { stableJson } from '../infrastructure/util.js';
|
|
3
|
+
import { CG, RDF, XSD } from './shacl-shapes.js';
|
|
4
|
+
import { invalidCodeRange } from './code-rdf.js';
|
|
5
|
+
const { namedNode: iri, literal } = DataFactory;
|
|
6
|
+
export function addBindingEvidence(dataset, record, fact, factIri) {
|
|
7
|
+
const binding = fact.codeBinding;
|
|
8
|
+
if (!binding)
|
|
9
|
+
return;
|
|
10
|
+
const prop = (node, key, value) => {
|
|
11
|
+
if (value !== undefined)
|
|
12
|
+
dataset.addQuad(node, iri(CG + key), literal(String(value), iri(XSD + (typeof value === 'number' ? 'integer' : 'string'))));
|
|
13
|
+
};
|
|
14
|
+
dataset.addQuad(record, iri(RDF + 'type'), iri(CG + 'DocumentCodeBinding'));
|
|
15
|
+
prop(record, 'bindingVersion', binding.schemaVersion);
|
|
16
|
+
prop(record, 'bindingMethod', binding.method);
|
|
17
|
+
for (const [key, value] of Object.entries(binding.selector ?? {}))
|
|
18
|
+
prop(record, `bindingSelector/${key}`, value);
|
|
19
|
+
const citations = (key, spans, proofs) => spans.forEach((span, index) => {
|
|
20
|
+
const term = iri(`${record.value}:${key}:${index}`);
|
|
21
|
+
dataset.addQuad(record, iri(CG + key), term);
|
|
22
|
+
dataset.addQuad(term, iri(RDF + 'type'), iri(CG + 'BindingCitation'));
|
|
23
|
+
for (const [name, value] of Object.entries(span))
|
|
24
|
+
prop(term, name, value);
|
|
25
|
+
if (proofs)
|
|
26
|
+
dataset.addQuad(term, iri(CG + 'bindingFact'), iri(factIri(proofs[index].factId)));
|
|
27
|
+
});
|
|
28
|
+
citations('bindingDocument', binding.documentCitations ?? []);
|
|
29
|
+
citations('bindingSelection', (binding.selectedOccurrences ?? []).map(item => item.provenance), binding.selectedOccurrences);
|
|
30
|
+
citations('bindingDefinition', (binding.definitions ?? []).map(item => item.provenance), binding.definitions);
|
|
31
|
+
citations('bindingSubject', (binding.subjectEvidence ?? []).map(item => item.provenance), binding.subjectEvidence);
|
|
32
|
+
}
|
|
33
|
+
function proofIssues(proof, byId) {
|
|
34
|
+
const issues = [], original = byId.get(proof.factId);
|
|
35
|
+
if (!original || stableJson(original.provenance) !== stableJson(proof.provenance))
|
|
36
|
+
issues.push('Binding proof is absent or has different provenance');
|
|
37
|
+
if (invalidCodeRange(proof.provenance) || proof.provenance.endLine < proof.provenance.startLine)
|
|
38
|
+
issues.push('Invalid binding proof range');
|
|
39
|
+
return issues;
|
|
40
|
+
}
|
|
41
|
+
function selectionIssues(proof, fact, byId) {
|
|
42
|
+
const issues = [...proofIssues(proof, byId)], original = byId.get(proof.factId);
|
|
43
|
+
const selector = fact.codeBinding.selector;
|
|
44
|
+
if (!original?.codeOccurrence)
|
|
45
|
+
return [...issues, 'Binding selection must cite an indexed occurrence'];
|
|
46
|
+
const target = original.codeOccurrence.role === 'definition' ? original.subject : original.object;
|
|
47
|
+
if (target.id !== fact.object.id || proof.provenance.path !== selector?.path)
|
|
48
|
+
issues.push('Binding selection does not identify the target symbol in its selected file');
|
|
49
|
+
if (selector?.line !== undefined && (selector.line !== proof.provenance.startLine || selector.column !== proof.provenance.startColumn))
|
|
50
|
+
issues.push('Binding selected occurrence differs from its exact selector');
|
|
51
|
+
if (selector?.line === undefined && original.codeOccurrence.role !== 'definition')
|
|
52
|
+
issues.push('A name selector requires a definition');
|
|
53
|
+
if (proof.provenance.commitSha !== fact.provenance.commitSha)
|
|
54
|
+
issues.push('Binding selection uses another revision');
|
|
55
|
+
return issues;
|
|
56
|
+
}
|
|
57
|
+
function definitionIssues(proof, fact, byId) {
|
|
58
|
+
const issues = [...proofIssues(proof, byId)], original = byId.get(proof.factId);
|
|
59
|
+
if (original?.codeOccurrence?.role !== 'definition' || original.subject.id !== fact.object.id)
|
|
60
|
+
issues.push('Binding definition does not define its target symbol');
|
|
61
|
+
if (proof.provenance.commitSha !== fact.provenance.commitSha)
|
|
62
|
+
issues.push('Binding definition uses another revision');
|
|
63
|
+
return issues;
|
|
64
|
+
}
|
|
65
|
+
function subjectIssues(proof, fact, byId) {
|
|
66
|
+
const issues = [...proofIssues(proof, byId)], original = byId.get(proof.factId);
|
|
67
|
+
if (!original || ![original.subject, original.object].some(node => node.id === fact.subject.id) ||
|
|
68
|
+
original.authority !== proof.authority || original.sourceAuthority !== proof.sourceAuthority || original.extractionReview !== proof.extractionReview) {
|
|
69
|
+
issues.push('Binding subject evidence or its authority was changed');
|
|
70
|
+
}
|
|
71
|
+
return issues;
|
|
72
|
+
}
|
|
73
|
+
function documentIssues(fact) {
|
|
74
|
+
const documents = fact.codeBinding.documentCitations ?? [];
|
|
75
|
+
if (!documents.some(span => stableJson(span) === stableJson(fact.provenance)) || documents.some(span => span.path !== fact.provenance.path || span.digest !== fact.provenance.digest || span.commitSha !== fact.provenance.commitSha ||
|
|
76
|
+
span.endLine < span.startLine || invalidCodeRange(span)))
|
|
77
|
+
return ['Binding document citations do not match their source'];
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
function mappingIssues(fact) {
|
|
81
|
+
const binding = fact.codeBinding, issues = [];
|
|
82
|
+
if (fact.predicate !== 'references' && !(binding.subjectEvidence?.length))
|
|
83
|
+
issues.push('Business mapping requires cited subject evidence');
|
|
84
|
+
if (fact.predicate !== 'references' && !['Capability', 'Workflow', 'Contract', 'Invariant'].includes(fact.subject.kind))
|
|
85
|
+
issues.push('Business mapping requires a business subject');
|
|
86
|
+
if (fact.authority === 'draft' && fact.sourceAuthority !== 'draft')
|
|
87
|
+
issues.push('Draft bindings require draft source authority');
|
|
88
|
+
if (fact.sourceRole === 'validation' && fact.predicate === 'implementedBy')
|
|
89
|
+
issues.push('Validation evidence cannot establish an implementation mapping');
|
|
90
|
+
if (binding.subjectEvidence?.some(proof => proof.extractionReview === 'unreviewed') && fact.extractionReview !== 'unreviewed')
|
|
91
|
+
issues.push('Binding cannot approve unreviewed subject evidence');
|
|
92
|
+
return issues;
|
|
93
|
+
}
|
|
94
|
+
/** Supplements shape validation by comparing proofs with the actual same-snapshot assertions. */
|
|
95
|
+
export function bindingEvidenceIssues(fact, byId) {
|
|
96
|
+
const binding = fact.codeBinding;
|
|
97
|
+
if (!binding)
|
|
98
|
+
return [];
|
|
99
|
+
const issues = [...documentIssues(fact), ...mappingIssues(fact)], selector = binding.selector;
|
|
100
|
+
const positive = (n) => typeof n === 'number' && Number.isSafeInteger(n) && n > 0;
|
|
101
|
+
if ((selector?.line !== undefined || selector?.column !== undefined) && (!positive(selector.line) || !positive(selector.column))) {
|
|
102
|
+
issues.push('Exact occurrence selectors require positive line and column');
|
|
103
|
+
}
|
|
104
|
+
return [...issues,
|
|
105
|
+
...(binding.selectedOccurrences ?? []).flatMap(proof => selectionIssues(proof, fact, byId)),
|
|
106
|
+
...(binding.definitions ?? []).flatMap(proof => definitionIssues(proof, fact, byId)),
|
|
107
|
+
...(binding.subjectEvidence ?? []).flatMap(proof => subjectIssues(proof, fact, byId)),
|
|
108
|
+
];
|
|
109
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type Store, type NamedNode } from 'n3';
|
|
2
|
+
import { type GraphFact } from '@weftlet/core';
|
|
3
|
+
export declare function addCallEvidence(dataset: Store, record: NamedNode, fact: GraphFact, factIri: (id: string) => string): void;
|
|
4
|
+
export declare function callEvidenceIssues(fact: GraphFact, byId: ReadonlyMap<string, GraphFact>): readonly string[];
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { DataFactory } from 'n3';
|
|
2
|
+
import { repositoryFileNode } from '@weftlet/core';
|
|
3
|
+
import { CG, RDF, XSD } from './shacl-shapes.js';
|
|
4
|
+
import { stableJson } from '../infrastructure/util.js';
|
|
5
|
+
import { invalidCodeRange } from './code-rdf.js';
|
|
6
|
+
const { namedNode: iri, literal } = DataFactory;
|
|
7
|
+
function containsRange(outer, inner) {
|
|
8
|
+
const compare = (a, b, c, d) => a - c || (b ?? 0) - (d ?? 0);
|
|
9
|
+
return compare(outer.startLine, outer.startColumn, inner.startLine, inner.startColumn) <= 0 &&
|
|
10
|
+
compare(outer.endLine, outer.endColumn, inner.endLine, inner.endColumn) >= 0;
|
|
11
|
+
}
|
|
12
|
+
export function addCallEvidence(dataset, record, fact, factIri) {
|
|
13
|
+
const call = fact.codeCall;
|
|
14
|
+
if (!call && fact.predicate !== 'calls')
|
|
15
|
+
return;
|
|
16
|
+
// Type calls even when metadata is missing so SHACL rejects them without a global OR scan.
|
|
17
|
+
dataset.addQuad(record, iri(RDF + 'type'), iri(CG + 'CodeCall'));
|
|
18
|
+
if (!call)
|
|
19
|
+
return;
|
|
20
|
+
const prop = (node, key, value) => {
|
|
21
|
+
if (value !== undefined)
|
|
22
|
+
dataset.addQuad(node, iri(CG + key), literal(String(value), iri(XSD +
|
|
23
|
+
(typeof value === 'number' ? 'integer' : typeof value === 'boolean' ? 'boolean' : 'string'))));
|
|
24
|
+
};
|
|
25
|
+
for (const [key, value] of Object.entries({ callVersion: call.schemaVersion, callMethod: call.method,
|
|
26
|
+
callSyntax: call.syntax, callDispatch: call.dispatch, callOptional: call.optional, callCaller: call.caller,
|
|
27
|
+
callTarget: call.target, callRuntime: call.runtime }))
|
|
28
|
+
prop(record, key, value);
|
|
29
|
+
const proofs = (key, values) => values.forEach((proof, index) => {
|
|
30
|
+
const term = iri(`${record.value}:${key}:${index}`);
|
|
31
|
+
dataset.addQuad(record, iri(CG + key), term);
|
|
32
|
+
dataset.addQuad(term, iri(RDF + 'type'), iri(CG + 'CallProof'));
|
|
33
|
+
dataset.addQuad(term, iri(CG + 'callFact'), iri(factIri(proof.factId)));
|
|
34
|
+
for (const [key, value] of Object.entries(proof.provenance))
|
|
35
|
+
prop(term, key, value);
|
|
36
|
+
});
|
|
37
|
+
proofs('callCallerDefinition', call.callerDefinitions ?? []);
|
|
38
|
+
proofs('callOccurrence', call.targetOccurrences ?? []);
|
|
39
|
+
proofs('callTargetDefinition', call.targetDefinitions ?? []);
|
|
40
|
+
}
|
|
41
|
+
function proofIssues(fact, proof, byId, role, nodeId) {
|
|
42
|
+
const issues = [];
|
|
43
|
+
const original = byId.get(proof.factId);
|
|
44
|
+
if (!original || stableJson(original.provenance) !== stableJson(proof.provenance) || original.codeOccurrence?.role !== role ||
|
|
45
|
+
(role === 'definition' ? original.subject.id : original.object.id) !== nodeId ||
|
|
46
|
+
proof.provenance.commitSha !== fact.provenance.commitSha || invalidCodeRange(proof.provenance)) {
|
|
47
|
+
issues.push('Call proof is missing, changed, from another revision or does not identify its endpoint');
|
|
48
|
+
}
|
|
49
|
+
return issues;
|
|
50
|
+
}
|
|
51
|
+
function occurrenceIssues(fact, proof, byId) {
|
|
52
|
+
const issues = proofIssues(fact, proof, byId, 'reference', fact.object.id);
|
|
53
|
+
if (proof.provenance.path !== fact.provenance.path || proof.provenance.digest !== fact.provenance.digest ||
|
|
54
|
+
!containsRange(fact.provenance, proof.provenance))
|
|
55
|
+
issues.push('Target occurrence must lie within the cited call site');
|
|
56
|
+
return issues;
|
|
57
|
+
}
|
|
58
|
+
function callerIssues(fact, byId) {
|
|
59
|
+
const call = fact.codeCall, callers = call.callerDefinitions ?? [], issues = [];
|
|
60
|
+
if (call.caller !== 'indexed-callable') {
|
|
61
|
+
if (callers.length || fact.subject.id !== repositoryFileNode(fact.provenance.path).id)
|
|
62
|
+
issues.push('File-scope calls require their canonical physical file');
|
|
63
|
+
return issues;
|
|
64
|
+
}
|
|
65
|
+
if (!callers.length || fact.subject.kind !== 'CodeSymbol')
|
|
66
|
+
issues.push('Indexed caller requires callable definition evidence');
|
|
67
|
+
for (const proof of callers) {
|
|
68
|
+
issues.push(...proofIssues(fact, proof, byId, 'definition', fact.subject.id));
|
|
69
|
+
if (proof.provenance.path !== fact.provenance.path || proof.provenance.digest !== fact.provenance.digest)
|
|
70
|
+
issues.push('Caller and call site must share source bytes');
|
|
71
|
+
}
|
|
72
|
+
return issues;
|
|
73
|
+
}
|
|
74
|
+
export function callEvidenceIssues(fact, byId) {
|
|
75
|
+
const call = fact.codeCall;
|
|
76
|
+
if (!call)
|
|
77
|
+
return fact.predicate === 'calls' ? ['A call requires static syntax and indexed proof metadata'] : [];
|
|
78
|
+
const issues = [];
|
|
79
|
+
if (fact.codeBinding || fact.codeOccurrence)
|
|
80
|
+
issues.push('A call assertion must remain separate from its supporting reference');
|
|
81
|
+
const targets = call.targetDefinitions ?? [], occurrences = call.targetOccurrences ?? [];
|
|
82
|
+
issues.push(...callerIssues(fact, byId));
|
|
83
|
+
if ((call.target === 'external') !== (targets.length === 0))
|
|
84
|
+
issues.push('Call target resolution disagrees with its indexed definitions');
|
|
85
|
+
if (call.target === 'indexed-class' && call.syntax !== 'construct')
|
|
86
|
+
issues.push('A class target requires construction syntax');
|
|
87
|
+
if ((call.syntax === 'construct') !== (call.dispatch === 'construct'))
|
|
88
|
+
issues.push('Construction and dispatch disagree');
|
|
89
|
+
for (const proof of targets)
|
|
90
|
+
issues.push(...proofIssues(fact, proof, byId, 'definition', fact.object.id));
|
|
91
|
+
for (const proof of occurrences)
|
|
92
|
+
issues.push(...occurrenceIssues(fact, proof, byId));
|
|
93
|
+
return issues;
|
|
94
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Included in the shared named-dataset/union shape set. */
|
|
2
|
+
export declare const callShapes = "\ncg:CodeCallShape a sh:NodeShape; sh:targetClass cg:CodeCall;\n sh:property [ sh:path rdf:predicate; sh:hasValue cg:calls ];\n sh:property [ sh:path rdf:object; sh:class cg:CodeSymbol ];\n sh:property [ sh:path cg:callVersion; sh:minCount 1; sh:maxCount 1; sh:hasValue 1 ];\n sh:property [ sh:path cg:callMethod; sh:minCount 1; sh:maxCount 1; sh:hasValue \"typescript-ast+scip\" ];\n sh:property [ sh:path cg:callSyntax; sh:minCount 1; sh:maxCount 1; sh:in (\"call\" \"construct\") ];\n sh:property [ sh:path cg:callDispatch; sh:minCount 1; sh:maxCount 1; sh:in (\"lexical\" \"member\" \"construct\") ];\n sh:property [ sh:path cg:callOptional; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:boolean ];\n sh:property [ sh:path cg:callCaller; sh:minCount 1; sh:maxCount 1; sh:in (\"indexed-callable\" \"file-scope\") ];\n sh:property [ sh:path cg:callTarget; sh:minCount 1; sh:maxCount 1; sh:in (\"indexed-body\" \"indexed-class\" \"declaration-only\" \"external\") ];\n sh:property [ sh:path cg:callRuntime; sh:minCount 1; sh:maxCount 1; sh:hasValue \"not-observed\" ];\n sh:property [ sh:path cg:columnEncoding; sh:minCount 1; sh:hasValue \"utf-16\" ];\n sh:property [ sh:path cg:authority; sh:in (\"implemented\" \"draft\") ];\n sh:property [ sh:path cg:extractionReview; sh:minCount 1; sh:hasValue \"deterministic\" ];\n sh:property [ sh:path cg:callOccurrence; sh:minCount 1; sh:class cg:CallProof ];\n sh:property [ sh:path cg:callCallerDefinition; sh:class cg:CallProof ];\n sh:property [ sh:path cg:callTargetDefinition; sh:class cg:CallProof ].\ncg:CallProofShape a sh:NodeShape; sh:targetClass cg:CallProof;\n sh:property [ sh:path cg:callFact; sh:minCount 1; sh:maxCount 1; sh:class rdf:Statement ];\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 ];\n sh:property [ sh:path cg:columnEncoding; sh:minCount 1; sh:hasValue \"utf-16\" ].\n";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Included in the shared named-dataset/union shape set. */
|
|
2
|
+
export const callShapes = `
|
|
3
|
+
cg:CodeCallShape a sh:NodeShape; sh:targetClass cg:CodeCall;
|
|
4
|
+
sh:property [ sh:path rdf:predicate; sh:hasValue cg:calls ];
|
|
5
|
+
sh:property [ sh:path rdf:object; sh:class cg:CodeSymbol ];
|
|
6
|
+
sh:property [ sh:path cg:callVersion; sh:minCount 1; sh:maxCount 1; sh:hasValue 1 ];
|
|
7
|
+
sh:property [ sh:path cg:callMethod; sh:minCount 1; sh:maxCount 1; sh:hasValue "typescript-ast+scip" ];
|
|
8
|
+
sh:property [ sh:path cg:callSyntax; sh:minCount 1; sh:maxCount 1; sh:in ("call" "construct") ];
|
|
9
|
+
sh:property [ sh:path cg:callDispatch; sh:minCount 1; sh:maxCount 1; sh:in ("lexical" "member" "construct") ];
|
|
10
|
+
sh:property [ sh:path cg:callOptional; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:boolean ];
|
|
11
|
+
sh:property [ sh:path cg:callCaller; sh:minCount 1; sh:maxCount 1; sh:in ("indexed-callable" "file-scope") ];
|
|
12
|
+
sh:property [ sh:path cg:callTarget; sh:minCount 1; sh:maxCount 1; sh:in ("indexed-body" "indexed-class" "declaration-only" "external") ];
|
|
13
|
+
sh:property [ sh:path cg:callRuntime; sh:minCount 1; sh:maxCount 1; sh:hasValue "not-observed" ];
|
|
14
|
+
sh:property [ sh:path cg:columnEncoding; sh:minCount 1; sh:hasValue "utf-16" ];
|
|
15
|
+
sh:property [ sh:path cg:authority; sh:in ("implemented" "draft") ];
|
|
16
|
+
sh:property [ sh:path cg:extractionReview; sh:minCount 1; sh:hasValue "deterministic" ];
|
|
17
|
+
sh:property [ sh:path cg:callOccurrence; sh:minCount 1; sh:class cg:CallProof ];
|
|
18
|
+
sh:property [ sh:path cg:callCallerDefinition; sh:class cg:CallProof ];
|
|
19
|
+
sh:property [ sh:path cg:callTargetDefinition; sh:class cg:CallProof ].
|
|
20
|
+
cg:CallProofShape a sh:NodeShape; sh:targetClass cg:CallProof;
|
|
21
|
+
sh:property [ sh:path cg:callFact; sh:minCount 1; sh:maxCount 1; sh:class rdf:Statement ];
|
|
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
|
+
sh:property [ sh:path cg:columnEncoding; sh:minCount 1; sh:hasValue "utf-16" ].
|
|
28
|
+
`;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CodeCoverageOverview, type CodeFileCoverage, type CodeProjectCoverage, type GraphSnapshot } from '@weftlet/core';
|
|
2
|
+
export declare const codeCoverageLimits: readonly ["Inventory covers regular committed TypeScript-family files and nearest package.json boundaries, not compiler projects or verified workspace membership.", "Code roots, requested entry files and observed SCIP documents are separate measurements. Neither selection nor an import alone proves a file was indexed.", "Connections count retained indexed code references across package boundaries, not asynchronous product links, runtime delivery or business impact.", "Zero observed connections or unresolved references does not prove isolation or complete resolution. Other languages, dynamic targets and absent evidence remain outside this measurement.", "Saved or draft coverage never establishes current-diff validity; use graph.neighbors on file/symbol IDs to inspect cited implementation evidence."];
|
|
3
|
+
/** Derived only from saved inventory and retained occurrence facts; does not invent graph edges. */
|
|
4
|
+
export declare function projectCodeCoverage(snapshot: GraphSnapshot): {
|
|
5
|
+
overview: CodeCoverageOverview;
|
|
6
|
+
projects: readonly CodeProjectCoverage[];
|
|
7
|
+
files: readonly CodeFileCoverage[];
|
|
8
|
+
};
|
|
9
|
+
export declare function codeCoverageDescription(code?: CodeCoverageOverview): string;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { repositoryFileNode } from '@weftlet/core';
|
|
2
|
+
import { sha256, stableJson } from '../infrastructure/util.js';
|
|
3
|
+
export const codeCoverageLimits = [
|
|
4
|
+
'Inventory covers regular committed TypeScript-family files and nearest package.json boundaries, not compiler projects or verified workspace membership.',
|
|
5
|
+
'Code roots, requested entry files and observed SCIP documents are separate measurements. Neither selection nor an import alone proves a file was indexed.',
|
|
6
|
+
'Connections count retained indexed code references across package boundaries, not asynchronous product links, runtime delivery or business impact.',
|
|
7
|
+
'Zero observed connections or unresolved references does not prove isolation or complete resolution. Other languages, dynamic targets and absent evidence remain outside this measurement.',
|
|
8
|
+
'Saved or draft coverage never establishes current-diff validity; use graph.neighbors on file/symbol IDs to inspect cited implementation evidence.',
|
|
9
|
+
];
|
|
10
|
+
function validInventory(snapshot) {
|
|
11
|
+
const inventory = snapshot.manifest.codeInventory;
|
|
12
|
+
if (!inventory || inventory.version !== 'committed-code-inventory-v1' || inventory.scipIndexDigest !== snapshot.manifest.scipIndexDigest)
|
|
13
|
+
return;
|
|
14
|
+
const { digest, ...plain } = inventory;
|
|
15
|
+
if (sha256(stableJson(plain)) !== digest)
|
|
16
|
+
return;
|
|
17
|
+
return inventory;
|
|
18
|
+
}
|
|
19
|
+
function totals(files) {
|
|
20
|
+
const count = (test) => files.filter(test).length;
|
|
21
|
+
const sum = (key) => files.reduce((n, file) => n + file[key], 0);
|
|
22
|
+
return { files: files.length, codeRootSelected: count(file => file.codeRootSelected),
|
|
23
|
+
requested: count(file => file.requested === 'included'), excludedFromRequest: count(file => file.requested === 'excluded'),
|
|
24
|
+
requestUnknown: count(file => file.requested === 'unknown'), indexed: count(file => file.indexed === true),
|
|
25
|
+
indexUnknown: count(file => file.indexed === undefined), requestedNotIndexed: count(file => file.requested === 'included' && file.indexed === false),
|
|
26
|
+
indexedOutsideRequest: count(file => file.requested === 'excluded' && file.indexed === true),
|
|
27
|
+
references: sum('references'), unresolvedReferences: sum('unresolvedReferences'), declarationOnlyReferences: sum('declarationOnlyReferences'),
|
|
28
|
+
implementationGaps: sum('implementationGaps') };
|
|
29
|
+
}
|
|
30
|
+
function definitionProjects(facts, byPath) {
|
|
31
|
+
const definitions = new Map();
|
|
32
|
+
for (const fact of facts)
|
|
33
|
+
if (fact.codeOccurrence?.role === 'definition') {
|
|
34
|
+
const owner = byPath.get(fact.provenance.path)?.projectRoot;
|
|
35
|
+
if (owner === undefined)
|
|
36
|
+
continue;
|
|
37
|
+
const roots = definitions.get(fact.subject.id) ?? new Set();
|
|
38
|
+
roots.add(owner);
|
|
39
|
+
definitions.set(fact.subject.id, roots);
|
|
40
|
+
}
|
|
41
|
+
return definitions;
|
|
42
|
+
}
|
|
43
|
+
function measureReferences(facts, byPath) {
|
|
44
|
+
const definitions = definitionProjects(facts, byPath);
|
|
45
|
+
const connections = new Map();
|
|
46
|
+
const project = (root) => {
|
|
47
|
+
if (!connections.has(root))
|
|
48
|
+
connections.set(root, { peers: new Set(), incoming: new Set(), outgoing: new Set() });
|
|
49
|
+
return connections.get(root);
|
|
50
|
+
};
|
|
51
|
+
const crossReferences = new Set(), seen = new Set();
|
|
52
|
+
for (const fact of facts) {
|
|
53
|
+
if (fact.codeOccurrence?.role !== 'reference' || seen.has(fact.id))
|
|
54
|
+
continue;
|
|
55
|
+
seen.add(fact.id);
|
|
56
|
+
const file = byPath.get(fact.provenance.path);
|
|
57
|
+
if (!file)
|
|
58
|
+
continue;
|
|
59
|
+
file.references++;
|
|
60
|
+
const targets = definitions.get(fact.object.id);
|
|
61
|
+
if (!targets?.size || fact.object.codeSymbol?.resolution === 'no-indexed-definition')
|
|
62
|
+
file.unresolvedReferences++;
|
|
63
|
+
if (fact.object.codeSymbol?.resolution === 'declaration-only')
|
|
64
|
+
file.declarationOnlyReferences++;
|
|
65
|
+
for (const root of targets ?? [])
|
|
66
|
+
if (root !== file.projectRoot) {
|
|
67
|
+
crossReferences.add(fact.id);
|
|
68
|
+
project(file.projectRoot).peers.add(root);
|
|
69
|
+
project(file.projectRoot).outgoing.add(fact.id);
|
|
70
|
+
project(root).peers.add(file.projectRoot);
|
|
71
|
+
project(root).incoming.add(fact.id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { project, crossReferences };
|
|
75
|
+
}
|
|
76
|
+
/** Derived only from saved inventory and retained occurrence facts; does not invent graph edges. */
|
|
77
|
+
export function projectCodeCoverage(snapshot) {
|
|
78
|
+
const inventory = validInventory(snapshot);
|
|
79
|
+
if (!inventory)
|
|
80
|
+
return { overview: { scope: 'saved-snapshot', status: snapshot.manifest.codeInventory ? 'invalid-inventory' : 'unavailable',
|
|
81
|
+
limitations: codeCoverageLimits }, projects: [], files: [] };
|
|
82
|
+
const files = inventory.files.map(file => ({ ...file, nodeId: repositoryFileNode(file.path).id,
|
|
83
|
+
references: 0, unresolvedReferences: 0, declarationOnlyReferences: 0, implementationGaps: 0 }));
|
|
84
|
+
const byPath = new Map(files.map(file => [file.path, file]));
|
|
85
|
+
const { project, crossReferences } = measureReferences(snapshot.facts, byPath);
|
|
86
|
+
for (const gap of snapshot.diagnostics ?? [])
|
|
87
|
+
if (gap.kind === 'implementationGaps') {
|
|
88
|
+
for (const path of new Set(gap.provenance.map(span => span.path))) {
|
|
89
|
+
const file = byPath.get(path);
|
|
90
|
+
if (file)
|
|
91
|
+
file.implementationGaps++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const groups = new Map();
|
|
95
|
+
for (const file of files) {
|
|
96
|
+
const group = groups.get(file.projectRoot) ?? [];
|
|
97
|
+
group.push(file);
|
|
98
|
+
groups.set(file.projectRoot, group);
|
|
99
|
+
}
|
|
100
|
+
const projects = inventory.projects.map(item => ({ root: item.root, ...(item.manifest ? { manifestPath: item.manifest.path } : {}),
|
|
101
|
+
counts: totals(groups.get(item.root) ?? []), connectedProjects: project(item.root).peers.size,
|
|
102
|
+
outgoingReferences: project(item.root).outgoing.size, incomingReferences: project(item.root).incoming.size }));
|
|
103
|
+
return { overview: { scope: 'saved-snapshot', status: 'recorded', inventoryDigest: inventory.digest, selection: inventory.selection,
|
|
104
|
+
indexObservation: inventory.indexObservation, projects: projects.length, totals: totals(files),
|
|
105
|
+
crossProjectReferences: crossReferences.size, otherIndexedFiles: inventory.otherIndexedFiles, limitations: codeCoverageLimits }, projects, files };
|
|
106
|
+
}
|
|
107
|
+
export function codeCoverageDescription(code) {
|
|
108
|
+
if (!code || code.status !== 'recorded' || !code.totals)
|
|
109
|
+
return `Saved repository code coverage: ${code?.status ?? 'unavailable'}; absence is not zero coverage.`;
|
|
110
|
+
const n = code.totals;
|
|
111
|
+
return `Saved repository code coverage (TypeScript-family files): ${n.files} inventoried; ${n.codeRootSelected} selected by code roots; ` +
|
|
112
|
+
`${n.requested} requested entries, ${n.excludedFromRequest} outside entries, ${n.requestUnknown} selection unknown; ` +
|
|
113
|
+
`${n.indexed} observed in SCIP, ${n.indexUnknown} index observation unknown, ${n.requestedNotIndexed} requested but not observed. ` +
|
|
114
|
+
`${code.crossProjectReferences} observed cross-package references; ${n.unresolvedReferences} references lack an indexed definition; ` +
|
|
115
|
+
`${n.declarationOnlyReferences} reference declaration-only targets; ${n.implementationGaps} cited implementation gaps. ` +
|
|
116
|
+
'These are saved-snapshot measurements, not completeness or no-impact claims. Use graph.coverage for package/file pages.';
|
|
117
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { codeCoverageDescription } from './code-coverage-projection.js';
|
|
2
|
+
const quoted = (value) => JSON.stringify(value).replace(/[\\`*_{}\[\]()<>!|]/g, '\\$&');
|
|
3
|
+
export function renderCodeCoverage(report) {
|
|
4
|
+
const { snapshot, evidenceState, selection, overview } = report;
|
|
5
|
+
return ['# Saved repository code coverage', `Snapshot: ${quoted(snapshot.snapshotId)} (${snapshot.commitSha}); configuration ${snapshot.configurationDigest ?? 'unknown'}`,
|
|
6
|
+
codeCoverageDescription(overview),
|
|
7
|
+
`View: ${report.view}; project: ${quoted(selection.projectRoot ?? '(all boundaries)')}; ${report.projectStatus ?? 'all'}; offset ${selection.offset}; ${selection.remaining}/${selection.total} items remain.`,
|
|
8
|
+
...(selection.continuation ? [`Continuation (same request): ${selection.continuation}`] : []),
|
|
9
|
+
...(evidenceState ? [`Stale: ${evidenceState.stale}; configuration matches: ${evidenceState.configurationMatches}; extraction compatible: ${evidenceState.extractionCompatible ?? 'unknown'}; overlay freshness: ${evidenceState.overlayFreshness ?? 'not an overlay'}`,
|
|
10
|
+
`Excluded working changes: ${evidenceState.excludedWorkingChanges.map(quoted).join(', ') || '(none)'}`] : []),
|
|
11
|
+
'', ...report.projects.map(p => `- ${quoted(p.root)}: ${p.counts.files} files; ${p.counts.requested} requested; ${p.counts.indexed} indexed; ${p.counts.indexUnknown} index unknown; ${p.connectedProjects} connected package boundaries. Use this exact projectRoot to page files.`),
|
|
12
|
+
...report.files.map(f => `- ${quoted(f.path)} (${quoted(f.nodeId)}): code roots ${f.codeRootSelected}; requested ${f.requested}; indexed ${f.indexed ?? 'unknown'}; ${f.unresolvedReferences} unresolved references; ${f.declarationOnlyReferences} declaration-only references; ${f.implementationGaps} cited gaps. Inspect graph.neighbors at this file ID for retained evidence.`),
|
|
13
|
+
'', '## Measurement limits', ...overview.limitations.map(item => `- ${item}`),
|
|
14
|
+
].join('\n');
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function parseCodeCoverageRequest(value) {
|
|
2
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
3
|
+
throw new Error('graph.coverage arguments must be an object');
|
|
4
|
+
const item = value;
|
|
5
|
+
if (Object.keys(item).some(key => !['snapshotId', 'projectRoot', 'maxItems', 'continuation'].includes(key)))
|
|
6
|
+
throw new Error('Unknown graph.coverage field');
|
|
7
|
+
const text = (key, max) => {
|
|
8
|
+
const s = item[key];
|
|
9
|
+
if (typeof s !== 'string' || !s.trim() || s.length > max)
|
|
10
|
+
throw new Error(`${key} must be a nonempty string of at most ${max} characters`);
|
|
11
|
+
return s;
|
|
12
|
+
};
|
|
13
|
+
const maxItems = item.maxItems === undefined ? 50 : item.maxItems;
|
|
14
|
+
if (!Number.isInteger(maxItems) || Number(maxItems) < 1 || Number(maxItems) > 200)
|
|
15
|
+
throw new Error('maxItems must be an integer between 1 and 200');
|
|
16
|
+
return { snapshotId: text('snapshotId', 256), maxItems: Number(maxItems),
|
|
17
|
+
...(item.projectRoot === undefined ? {} : { projectRoot: text('projectRoot', 8192) }),
|
|
18
|
+
...(item.continuation === undefined ? {} : { continuation: text('continuation', 2048) }) };
|
|
19
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CodeCoverageReport, CodeCoverageRequest, CodeCoverageService, SnapshotStore } from '@weftlet/core';
|
|
2
|
+
export declare class SnapshotCodeCoverageService implements CodeCoverageService {
|
|
3
|
+
private readonly store;
|
|
4
|
+
constructor(store: Pick<SnapshotStore, 'read'>);
|
|
5
|
+
coverage(input: CodeCoverageRequest): Promise<CodeCoverageReport>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { sha256, stableJson } from '../infrastructure/util.js';
|
|
2
|
+
import { projectCodeCoverage } from './code-coverage-projection.js';
|
|
3
|
+
import { parseCodeCoverageRequest } from './code-coverage-request.js';
|
|
4
|
+
import { renderCodeCoverage } from './code-coverage-renderer.js';
|
|
5
|
+
import { RdfEvidenceGraph } from './rdf.js';
|
|
6
|
+
import { snapshotContext } from './snapshot-context.js';
|
|
7
|
+
const version = 'code-coverage-query-v1';
|
|
8
|
+
function pageOffset(token, binding, length, pageSize) {
|
|
9
|
+
if (token === undefined)
|
|
10
|
+
return 0;
|
|
11
|
+
try {
|
|
12
|
+
if (!/^[A-Za-z0-9_-]+$/.test(token))
|
|
13
|
+
throw Error();
|
|
14
|
+
const item = JSON.parse(Buffer.from(token, 'base64url').toString('utf8'));
|
|
15
|
+
if (Object.keys(item).sort().join(',') !== 'binding,offset,version' || item.version !== version || item.binding !== binding ||
|
|
16
|
+
!Number.isSafeInteger(item.offset) || item.offset < 1 || item.offset >= length || item.offset % pageSize)
|
|
17
|
+
throw Error();
|
|
18
|
+
return item.offset;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw Error('Invalid graph.coverage continuation or snapshot/query mismatch; restart graph.coverage');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export class SnapshotCodeCoverageService {
|
|
25
|
+
store;
|
|
26
|
+
constructor(store) {
|
|
27
|
+
this.store = store;
|
|
28
|
+
}
|
|
29
|
+
async coverage(input) {
|
|
30
|
+
const request = parseCodeCoverageRequest(input), saved = await this.store.read(request.snapshotId);
|
|
31
|
+
if (!saved)
|
|
32
|
+
throw Error('No graph snapshot is available for that identifier');
|
|
33
|
+
if (saved.manifest.snapshotId !== request.snapshotId)
|
|
34
|
+
throw Error('Saved snapshot identity does not match the requested identifier');
|
|
35
|
+
const facts = await new RdfEvidenceGraph(saved).facts();
|
|
36
|
+
if (new Set(facts.map(fact => fact.id)).size !== facts.length)
|
|
37
|
+
throw Error('Duplicate fact identities prevent stable code coverage');
|
|
38
|
+
const projected = projectCodeCoverage({ ...saved, facts });
|
|
39
|
+
const { continuation, ...query } = request;
|
|
40
|
+
const binding = sha256(stableJson({ version, query, manifest: saved.manifest, facts, diagnostics: saved.diagnostics }));
|
|
41
|
+
const fileView = request.projectRoot !== undefined;
|
|
42
|
+
const files = fileView ? projected.files.filter(f => f.projectRoot === request.projectRoot) : [];
|
|
43
|
+
const length = fileView ? files.length : projected.projects.length;
|
|
44
|
+
const offset = pageOffset(continuation, binding, length, request.maxItems), end = Math.min(length, offset + request.maxItems);
|
|
45
|
+
const project = projected.projects.find(p => p.root === request.projectRoot);
|
|
46
|
+
const next = end < length ? Buffer.from(JSON.stringify({ version, binding, offset: end })).toString('base64url') : undefined;
|
|
47
|
+
const plain = { snapshot: snapshotContext(saved.manifest), overview: projected.overview,
|
|
48
|
+
view: fileView ? 'files' : 'projects', ...(fileView ? { project, projectStatus: projected.overview.status !== 'recorded' ? 'unknown' : project ? 'present' : 'absent' } : {}),
|
|
49
|
+
projects: fileView ? [] : projected.projects.slice(offset, end), files: fileView ? files.slice(offset, end) : [],
|
|
50
|
+
selection: { ...(fileView ? { projectRoot: request.projectRoot } : {}), maxItems: request.maxItems, offset, total: length,
|
|
51
|
+
remaining: length - end, truncated: !!next, ...(next ? { continuation: next } : {}) } };
|
|
52
|
+
return { ...plain, markdown: renderCodeCoverage(plain) };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { GraphFact, ContextPack } from '@weftlet/core';
|
|
2
|
+
/** Human-readable labels for the same typed evidence returned to CLI/MCP clients. */
|
|
3
|
+
export declare function codeEvidenceDescription(fact: GraphFact): string | undefined;
|
|
4
|
+
export declare function unexpandedCodeDescription(exploration: ContextPack['exploration']): readonly string[];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Human-readable labels for the same typed evidence returned to CLI/MCP clients. */
|
|
2
|
+
export function codeEvidenceDescription(fact) {
|
|
3
|
+
if (fact.codeCall) {
|
|
4
|
+
const call = fact.codeCall;
|
|
5
|
+
return `static ${call.syntax}; ${call.dispatch} selection; ${call.target}; ${call.caller}; ` +
|
|
6
|
+
`${call.optional ? 'optional invocation; ' : ''}runtime dispatch/value mutation not evaluated; execution not observed`;
|
|
7
|
+
}
|
|
8
|
+
const binding = fact.codeBinding;
|
|
9
|
+
if (binding) {
|
|
10
|
+
const sites = binding.definitions.map(proof => `${proof.provenance.path}:${proof.provenance.startLine}:${proof.provenance.startColumn ?? 1}@${proof.provenance.commitSha}`).join(', ');
|
|
11
|
+
const selected = binding.selectedOccurrences.map(proof => `${proof.provenance.path}:${proof.provenance.startLine}:${proof.provenance.startColumn ?? 1}@${proof.provenance.commitSha}`).join(', ');
|
|
12
|
+
return `explicit mapping declaration; target ${binding.selector.symbol}; selected ${selected}; definitions ${sites}; not proof of fulfillment or test execution`;
|
|
13
|
+
}
|
|
14
|
+
const code = fact.codeOccurrence;
|
|
15
|
+
if (!code)
|
|
16
|
+
return undefined;
|
|
17
|
+
const symbol = code.role === 'definition' ? fact.subject : fact.object;
|
|
18
|
+
return `static ${code.role}; ${symbol.codeSymbol?.resolution ?? 'unknown resolution'}; ` +
|
|
19
|
+
`${code.enclosure} lexical enclosure; ${code.locationKind}; UTF-16 columns`;
|
|
20
|
+
}
|
|
21
|
+
export function unexpandedCodeDescription(exploration) {
|
|
22
|
+
const count = exploration?.unexpandedCodeTargetCount ?? 0;
|
|
23
|
+
return count ? [`Unresolved code targets left unexpanded: ${count}; up to 100 example IDs are included in exploration metadata. Use graph.neighbors with their exact IDs to inspect shared references; no implementation or call is established.`] : [];
|
|
24
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
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;
|
|
4
|
+
export declare function invalidCodeRange(range: Pick<SourceSpan, 'startLine' | 'endLine' | 'startColumn' | 'endColumn' | 'columnEncoding'>): boolean;
|
|
5
|
+
export declare function codeEvidenceIssues(fact: GraphFact): readonly string[];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { DataFactory } from 'n3';
|
|
2
|
+
import { repositoryFileNode } from '@weftlet/core';
|
|
3
|
+
import { CG, RDF, XSD } from './shacl-shapes.js';
|
|
4
|
+
const { namedNode: iri, literal } = DataFactory;
|
|
5
|
+
export function addCodeEvidence(dataset, record, fact, nodeIri) {
|
|
6
|
+
const property = (subject, key, value) => {
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return;
|
|
9
|
+
dataset.addQuad(subject, iri(CG + key), literal(String(value), iri(XSD + (typeof value === 'number' ? 'integer' : 'string'))));
|
|
10
|
+
};
|
|
11
|
+
for (const key of ['startColumn', 'endColumn', 'columnEncoding'])
|
|
12
|
+
property(record, key, fact.provenance[key]);
|
|
13
|
+
const code = fact.codeOccurrence;
|
|
14
|
+
if (code !== undefined) {
|
|
15
|
+
dataset.addQuad(record, iri(RDF + 'type'), iri(CG + 'CodeOccurrence'));
|
|
16
|
+
for (const [key, value] of Object.entries({ codeVersion: code.schemaVersion, codeRole: code.role, codeLocationKind: code.locationKind,
|
|
17
|
+
codeNativeRoles: code.nativeRoles, codeEnclosure: code.enclosure }))
|
|
18
|
+
property(record, key, value);
|
|
19
|
+
if (code.enclosingSymbolId !== undefined)
|
|
20
|
+
dataset.addQuad(record, iri(CG + 'enclosingSymbol'), iri(nodeIri(code.enclosingSymbolId)));
|
|
21
|
+
if (code.declarationRange !== undefined) {
|
|
22
|
+
const range = iri(record.value + ':declaration-range');
|
|
23
|
+
dataset.addQuad(record, iri(CG + 'declarationRange'), range);
|
|
24
|
+
dataset.addQuad(range, iri(RDF + 'type'), iri(CG + 'CodeRange'));
|
|
25
|
+
for (const [key, value] of Object.entries(code.declarationRange))
|
|
26
|
+
property(range, key, value);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const node of [fact.subject, fact.object]) {
|
|
30
|
+
if (node.kind !== 'CodeSymbol' && node.codeSymbol === undefined)
|
|
31
|
+
continue;
|
|
32
|
+
const term = iri(nodeIri(node.id));
|
|
33
|
+
dataset.addQuad(term, iri(RDF + 'type'), iri(CG + 'CodeSymbol'));
|
|
34
|
+
const value = node.codeSymbol;
|
|
35
|
+
if (value === undefined)
|
|
36
|
+
continue; // The shape reports missing required metadata.
|
|
37
|
+
for (const [key, item] of Object.entries({ codeVersion: value.schemaVersion, codeProvider: value.provider,
|
|
38
|
+
codeRepository: value.repositoryId, nativeSymbol: value.nativeSymbol, codeIdentityScope: value.identityScope,
|
|
39
|
+
codeDocument: value.documentPath, codeIndex: value.indexDigest, codeResolution: value.resolution }))
|
|
40
|
+
property(term, key, item);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function invalidCodeRange(range) {
|
|
44
|
+
if (range.startColumn === undefined && range.endColumn === undefined && range.columnEncoding === undefined)
|
|
45
|
+
return false;
|
|
46
|
+
return !Number.isSafeInteger(range.startColumn) || !Number.isSafeInteger(range.endColumn) ||
|
|
47
|
+
range.startColumn < 1 || range.endColumn < 1 || range.columnEncoding !== 'utf-16' ||
|
|
48
|
+
range.endLine < range.startLine || (range.endLine === range.startLine && range.endColumn < range.startColumn);
|
|
49
|
+
}
|
|
50
|
+
export function codeEvidenceIssues(fact) {
|
|
51
|
+
const code = fact.codeOccurrence;
|
|
52
|
+
if (!code)
|
|
53
|
+
return [];
|
|
54
|
+
const issues = [];
|
|
55
|
+
const file = repositoryFileNode(fact.provenance.path);
|
|
56
|
+
if ((code.nativeRoles & 1) !== (code.role === 'definition' ? 1 : 0))
|
|
57
|
+
issues.push('Code role disagrees with native definition flag');
|
|
58
|
+
if (code.role === 'definition' && (fact.object.id !== file.id || fact.object.kind !== file.kind)) {
|
|
59
|
+
issues.push('Code definition must identify its canonical cited physical file');
|
|
60
|
+
}
|
|
61
|
+
if (code.role === 'reference') {
|
|
62
|
+
if (code.declarationRange !== undefined)
|
|
63
|
+
issues.push('A reference cannot declare a definition range');
|
|
64
|
+
const sourceId = code.enclosure === 'native-range' ? code.enclosingSymbolId : file.id;
|
|
65
|
+
if (fact.subject.id !== sourceId)
|
|
66
|
+
issues.push('Reference source disagrees with lexical enclosure');
|
|
67
|
+
}
|
|
68
|
+
const zeroWidth = fact.provenance.startLine === fact.provenance.endLine && fact.provenance.startColumn === fact.provenance.endColumn;
|
|
69
|
+
if (zeroWidth !== (code.locationKind === 'zero-width-anchor'))
|
|
70
|
+
issues.push('Code location kind disagrees with source span');
|
|
71
|
+
return issues;
|
|
72
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Other declarations of the same endpoint/property remain visible; never choose a winning assertion. */
|
|
2
|
+
function competes(selected, other) {
|
|
3
|
+
const left = selected.contractObligation?.declaration, right = other.contractObligation?.declaration;
|
|
4
|
+
if (!left || !right || other.id === selected.id || selected.object.id !== other.object.id)
|
|
5
|
+
return false;
|
|
6
|
+
return left.role === right.role && left.endpoint === right.endpoint && left.domain === right.domain &&
|
|
7
|
+
left.stage === right.stage && left.constraint?.metric === right.constraint?.metric;
|
|
8
|
+
}
|
|
9
|
+
export function compatibilityContext(all, selected) {
|
|
10
|
+
const contractNodeIds = [...new Set(selected.filter(fact => fact.contractObligation).map(fact => fact.object.id))].sort();
|
|
11
|
+
const endpointNodeIds = [...new Set(selected.filter(fact => fact.contractObligation).map(fact => fact.subject.id))].sort();
|
|
12
|
+
const selectedIds = new Set(selected.map(fact => fact.id));
|
|
13
|
+
const competing = [...new Set(all.filter(other => !selectedIds.has(other.id) && selected.some(fact => competes(fact, other))).map(fact => fact.id))].sort();
|
|
14
|
+
return { contractNodeIds, endpointNodeIds,
|
|
15
|
+
otherIncidentFacts: all.filter(fact => !selectedIds.has(fact.id) &&
|
|
16
|
+
[fact.subject.id, fact.object.id].some(id => contractNodeIds.includes(id))).length,
|
|
17
|
+
competingFactIds: competing.slice(0, 20), competingFacts: competing.length, omittedCompetingFacts: Math.max(0, competing.length - 20) };
|
|
18
|
+
}
|