@weftlet/toolkit 0.6.0 → 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/ontology.js +2 -2
- 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 +33 -104
- 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/artifact-paths.d.ts +4 -0
- package/dist/infrastructure/artifact-paths.js +14 -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/storage/storage.js +41 -13
- 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-028-bounded-artifact-paths.md +58 -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/CONSUMER-TRIAL.md +6 -4
- package/docs/GRAPHIFY-EVALUATION.md +87 -0
- package/docs/PRE-1.0-BASELINE.md +141 -0
- package/docs/RELEASE-1.0-PLAN.md +37 -15
- package/docs/RELEASING.md +3 -3
- package/docs/ROADMAP.md +39 -11
- package/docs/VALIDATION.md +501 -0
- package/docs/assessments/pre-1.0-2026-10-28.json +200 -0
- package/docs/extraction-manifest.json +258 -23
- package/package.json +8 -4
- package/templates/context-graph-development/SKILL.md +30 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { GraphPathQueryService, GraphPathsRequest } from '@weftlet/core';
|
|
2
|
+
import type { GitRepository } from '../repository/git.js';
|
|
3
|
+
export declare class SavedGraphPathService implements GraphPathQueryService {
|
|
4
|
+
private readonly queries;
|
|
5
|
+
private readonly repository;
|
|
6
|
+
private readonly configurationDigest;
|
|
7
|
+
private readonly expectedExtractors?;
|
|
8
|
+
constructor(queries: GraphPathQueryService, repository: GitRepository, configurationDigest: string, expectedExtractors?: Readonly<Record<string, string>> | undefined);
|
|
9
|
+
paths(request: GraphPathsRequest): Promise<{
|
|
10
|
+
markdown: string;
|
|
11
|
+
evidenceState: {
|
|
12
|
+
readonly currentCommit?: string;
|
|
13
|
+
readonly excludedWorkingChanges: readonly string[];
|
|
14
|
+
readonly capturedWorkingChanges?: readonly string[];
|
|
15
|
+
readonly changedSinceCapture?: readonly string[];
|
|
16
|
+
readonly overlayFreshness?: "current" | "stale" | "unknown";
|
|
17
|
+
readonly stale: boolean;
|
|
18
|
+
readonly configurationMatches: boolean;
|
|
19
|
+
readonly extractionCompatible?: boolean;
|
|
20
|
+
readonly incompatibleExtractors?: readonly string[];
|
|
21
|
+
readonly eligibleForCurrentDiffValidation: false;
|
|
22
|
+
};
|
|
23
|
+
snapshot: import("@weftlet/core").ContextPack["snapshot"];
|
|
24
|
+
endpoints: {
|
|
25
|
+
readonly from: {
|
|
26
|
+
readonly nodeId: string;
|
|
27
|
+
readonly status: "present" | "absent-from-snapshot";
|
|
28
|
+
};
|
|
29
|
+
readonly to: {
|
|
30
|
+
readonly nodeId: string;
|
|
31
|
+
readonly status: "present" | "absent-from-snapshot";
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
paths: readonly import("@weftlet/core").EvidencePath[];
|
|
35
|
+
facts: readonly import("@weftlet/core").GraphFact[];
|
|
36
|
+
coverage: import("@weftlet/core").ContextPack["coverage"];
|
|
37
|
+
diagnostics: import("@weftlet/core").ContextPack["diagnostics"];
|
|
38
|
+
search: {
|
|
39
|
+
readonly version: string;
|
|
40
|
+
readonly order: "fact-id-depth-first";
|
|
41
|
+
readonly direction: import("@weftlet/core").GraphDirection;
|
|
42
|
+
readonly predicates: readonly import("@weftlet/core").GraphPredicate[];
|
|
43
|
+
readonly maxDepth: number;
|
|
44
|
+
readonly maxPaths: number;
|
|
45
|
+
readonly maxSteps: number;
|
|
46
|
+
readonly searchLimit: number;
|
|
47
|
+
readonly replayedSteps: number;
|
|
48
|
+
readonly examinedSteps: number;
|
|
49
|
+
readonly totalSteps: number;
|
|
50
|
+
readonly stopReason: "exhausted" | "path-page-limit" | "work-page-limit" | "search-limit";
|
|
51
|
+
readonly exhaustedWithinDepth: boolean;
|
|
52
|
+
readonly depthLimitReached: boolean;
|
|
53
|
+
readonly truncated: boolean;
|
|
54
|
+
readonly continuation?: string;
|
|
55
|
+
};
|
|
56
|
+
frontier: import("@weftlet/core").PathFrontier;
|
|
57
|
+
}>;
|
|
58
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { renderGraphPaths } from '../graph/paths-renderer.js';
|
|
2
|
+
import { inspectSavedEvidence } from './saved-evidence.js';
|
|
3
|
+
export class SavedGraphPathService {
|
|
4
|
+
queries;
|
|
5
|
+
repository;
|
|
6
|
+
configurationDigest;
|
|
7
|
+
expectedExtractors;
|
|
8
|
+
constructor(queries, repository, configurationDigest, expectedExtractors) {
|
|
9
|
+
this.queries = queries;
|
|
10
|
+
this.repository = repository;
|
|
11
|
+
this.configurationDigest = configurationDigest;
|
|
12
|
+
this.expectedExtractors = expectedExtractors;
|
|
13
|
+
}
|
|
14
|
+
async paths(request) {
|
|
15
|
+
const result = await this.queries.paths(request);
|
|
16
|
+
const evidenceState = await inspectSavedEvidence(result.snapshot, this.repository, this.configurationDigest, this.expectedExtractors);
|
|
17
|
+
const withState = { ...result, evidenceState };
|
|
18
|
+
return { ...withState, markdown: renderGraphPaths(withState) };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import type { GraphFact, SourceSpan } from '@weftlet/core';
|
|
3
|
+
export type Callable = ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration | ts.ConstructorDeclaration;
|
|
4
|
+
export declare const isCallable: (node: ts.Node) => node is Callable;
|
|
5
|
+
export declare function callableName(node: Callable): ts.Node | undefined;
|
|
6
|
+
/** Ambient declarations may occur in .ts namespaces too; they supply no runtime body. */
|
|
7
|
+
export declare function isAmbient(node: ts.Node): boolean;
|
|
8
|
+
export declare function syntaxSpan(node: ts.Node, source: ts.SourceFile, digest: string, commitSha: string): SourceSpan;
|
|
9
|
+
export declare const positionKey: (span: SourceSpan) => string;
|
|
10
|
+
export declare function calleeToken(expression: ts.Expression): ts.Node | undefined;
|
|
11
|
+
export interface ParsedCallSource {
|
|
12
|
+
readonly source: ts.SourceFile;
|
|
13
|
+
readonly digest: string;
|
|
14
|
+
readonly byPosition: ReadonlyMap<string, readonly GraphFact[]>;
|
|
15
|
+
readonly parseErrors: readonly ts.Diagnostic[];
|
|
16
|
+
}
|
|
17
|
+
export declare function parseCallSource(path: string, text: string, digest: string, facts: readonly GraphFact[]): ParsedCallSource;
|
|
18
|
+
export declare function factsAt(node: ts.Node | undefined, parsed: ParsedCallSource, commitSha: string, role: 'definition' | 'reference'): GraphFact[];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
export const isCallable = (node) => ts.isFunctionDeclaration(node) ||
|
|
3
|
+
ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) ||
|
|
4
|
+
ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node) || ts.isConstructorDeclaration(node);
|
|
5
|
+
export function callableName(node) {
|
|
6
|
+
if ('name' in node && node.name)
|
|
7
|
+
return node.name;
|
|
8
|
+
if (ts.isConstructorDeclaration(node))
|
|
9
|
+
return node.getFirstToken();
|
|
10
|
+
const parent = node.parent;
|
|
11
|
+
if ((ts.isVariableDeclaration(parent) || ts.isPropertyDeclaration(parent) || ts.isPropertyAssignment(parent)) &&
|
|
12
|
+
parent.initializer === node && (ts.isIdentifier(parent.name) || ts.isPrivateIdentifier(parent.name)))
|
|
13
|
+
return parent.name;
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
/** Ambient declarations may occur in .ts namespaces too; they supply no runtime body. */
|
|
17
|
+
export function isAmbient(node) {
|
|
18
|
+
if (node.getSourceFile().isDeclarationFile)
|
|
19
|
+
return true;
|
|
20
|
+
for (let current = node; current; current = current.parent) {
|
|
21
|
+
if (ts.canHaveModifiers(current) && ts.getModifiers(current)?.some(modifier => modifier.kind === ts.SyntaxKind.DeclareKeyword))
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
export function syntaxSpan(node, source, digest, commitSha) {
|
|
27
|
+
const start = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
28
|
+
const end = source.getLineAndCharacterOfPosition(node.getEnd());
|
|
29
|
+
return { path: source.fileName, startLine: start.line + 1, startColumn: start.character + 1,
|
|
30
|
+
endLine: end.line + 1, endColumn: end.character + 1, columnEncoding: 'utf-16', digest, commitSha };
|
|
31
|
+
}
|
|
32
|
+
export const positionKey = (span) => `${span.path}:${span.startLine}:${span.startColumn}:${span.endLine}:${span.endColumn}`;
|
|
33
|
+
export function calleeToken(expression) {
|
|
34
|
+
while (ts.isParenthesizedExpression(expression) || ts.isNonNullExpression(expression) ||
|
|
35
|
+
ts.isAsExpression(expression) || ts.isTypeAssertionExpression(expression) || ts.isSatisfiesExpression(expression))
|
|
36
|
+
expression = expression.expression;
|
|
37
|
+
if (ts.isIdentifier(expression) || expression.kind === ts.SyntaxKind.SuperKeyword)
|
|
38
|
+
return expression;
|
|
39
|
+
if (ts.isPropertyAccessExpression(expression))
|
|
40
|
+
return expression.name;
|
|
41
|
+
if (ts.isElementAccessExpression(expression) && ts.isStringLiteral(expression.argumentExpression))
|
|
42
|
+
return expression.argumentExpression;
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
export function parseCallSource(path, text, digest, facts) {
|
|
46
|
+
const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, path.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
47
|
+
const byPosition = new Map();
|
|
48
|
+
for (const fact of facts) {
|
|
49
|
+
const key = positionKey(fact.provenance), group = byPosition.get(key) ?? [];
|
|
50
|
+
group.push(fact);
|
|
51
|
+
byPosition.set(key, group);
|
|
52
|
+
}
|
|
53
|
+
return { source, digest, byPosition,
|
|
54
|
+
parseErrors: source.parseDiagnostics };
|
|
55
|
+
}
|
|
56
|
+
export function factsAt(node, parsed, commitSha, role) {
|
|
57
|
+
if (!node)
|
|
58
|
+
return [];
|
|
59
|
+
return (parsed.byPosition.get(positionKey(syntaxSpan(node, parsed.source, parsed.digest, commitSha))) ?? [])
|
|
60
|
+
.filter(fact => fact.codeOccurrence?.role === role);
|
|
61
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export interface CanonicalDocumentDefinition {
|
|
2
2
|
readonly target: string;
|
|
3
|
+
readonly title?: string;
|
|
3
4
|
readonly startLine: number;
|
|
4
5
|
readonly endLine: number;
|
|
5
6
|
}
|
|
6
7
|
export interface CanonicalDocumentLink {
|
|
7
8
|
readonly kind: 'link' | 'ambiguous-reference';
|
|
9
|
+
readonly title?: string;
|
|
8
10
|
readonly target?: string;
|
|
9
11
|
readonly startLine: number;
|
|
10
12
|
readonly endLine: number;
|
|
@@ -15,7 +17,9 @@ export interface CanonicalDocumentLink {
|
|
|
15
17
|
readonly referenceEndLine?: number;
|
|
16
18
|
}
|
|
17
19
|
/** Explicit Markdown links only: this deliberately does not change the graph linker's policy. */
|
|
18
|
-
export declare function canonicalDocumentLinks(content: string
|
|
20
|
+
export declare function canonicalDocumentLinks(content: string, options?: {
|
|
21
|
+
readonly includeTitles?: boolean;
|
|
22
|
+
}): readonly CanonicalDocumentLink[];
|
|
19
23
|
export type CanonicalDocumentLinkResolution = {
|
|
20
24
|
readonly kind: 'path';
|
|
21
25
|
readonly path: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { posix } from 'node:path';
|
|
2
2
|
import { fromMarkdown } from 'mdast-util-from-markdown';
|
|
3
3
|
/** Explicit Markdown links only: this deliberately does not change the graph linker's policy. */
|
|
4
|
-
export function canonicalDocumentLinks(content) {
|
|
4
|
+
export function canonicalDocumentLinks(content, options = {}) {
|
|
5
5
|
const tree = fromMarkdown(content);
|
|
6
6
|
const definitions = new Map();
|
|
7
7
|
const collect = (node) => {
|
|
@@ -18,20 +18,20 @@ export function canonicalDocumentLinks(content) {
|
|
|
18
18
|
const links = [];
|
|
19
19
|
const visit = (node) => {
|
|
20
20
|
if (node.type === 'link' && node.position) {
|
|
21
|
-
links.push({ kind: 'link', target: node.url,
|
|
21
|
+
links.push({ kind: 'link', target: node.url, ...(options.includeTitles && node.title ? { title: node.title } : {}),
|
|
22
22
|
startLine: node.position.start.line, endLine: node.position.end.line });
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
if (node.type === 'linkReference' && node.position) {
|
|
26
26
|
const declared = (definitions.get(node.identifier) ?? []).flatMap(definition => definition.position
|
|
27
|
-
? [{ target: definition.url, startLine: definition.position.start.line, endLine: definition.position.end.line }]
|
|
27
|
+
? [{ target: definition.url, ...(options.includeTitles && definition.title ? { title: definition.title } : {}), startLine: definition.position.start.line, endLine: definition.position.end.line }]
|
|
28
28
|
: []);
|
|
29
29
|
const first = declared[0];
|
|
30
30
|
if (!first)
|
|
31
31
|
return;
|
|
32
32
|
const reference = { identifier: node.identifier,
|
|
33
33
|
referenceStartLine: node.position.start.line, referenceEndLine: node.position.end.line };
|
|
34
|
-
if (new Set(declared.map(definition => definition.target)).size > 1) {
|
|
34
|
+
if (new Set(declared.map(definition => JSON.stringify([definition.target, definition.title]))).size > 1) {
|
|
35
35
|
// Never silently choose the first conflicting definition, as normal Markdown rendering does.
|
|
36
36
|
links.push({ kind: 'ambiguous-reference', ...reference,
|
|
37
37
|
startLine: node.position.start.line, endLine: node.position.end.line, definitions: declared });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CodeInventory } from '@weftlet/core';
|
|
2
|
+
import type { GitTreeEntry } from '../repository/git.js';
|
|
3
|
+
import type { ScipSourceProject } from '../configuration/scip-project.js';
|
|
4
|
+
import type { ScipIndexResult } from './scip-runtime.js';
|
|
5
|
+
/** No package contents, dependencies or active checkout are read. Boundary names are physical paths. */
|
|
6
|
+
export declare function buildCodeInventory(entries: readonly GitTreeEntry[], selectedPaths: readonly string[], codeRoots: readonly string[], project: ScipSourceProject | undefined, index: ScipIndexResult): CodeInventory;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { matchesRepositoryGlob } from '../repository/glob.js';
|
|
2
|
+
import { sha256, stableJson } from '../infrastructure/util.js';
|
|
3
|
+
/** No package contents, dependencies or active checkout are read. Boundary names are physical paths. */
|
|
4
|
+
export function buildCodeInventory(entries, selectedPaths, codeRoots, project, index) {
|
|
5
|
+
const regular = entries.filter(entry => entry.type === 'blob' && ['100644', '100755'].includes(entry.mode));
|
|
6
|
+
const manifests = regular.filter(entry => entry.path === 'package.json' || entry.path.endsWith('/package.json'));
|
|
7
|
+
const roots = new Map(manifests.map(entry => [entry.path === 'package.json' ? '.' : entry.path.slice(0, -'/package.json'.length),
|
|
8
|
+
{ path: entry.path, blobId: entry.objectId }]));
|
|
9
|
+
const orderedRoots = [...roots.keys()].filter(root => root !== '.').sort((a, b) => b.length - a.length || a.localeCompare(b));
|
|
10
|
+
const indexed = index.indexedPaths === undefined ? undefined : new Set(index.indexedPaths);
|
|
11
|
+
const selected = new Set(selectedPaths);
|
|
12
|
+
const files = regular.filter(entry => /\.(?:[cm]?ts|tsx)$/.test(entry.path)).map(entry => ({
|
|
13
|
+
path: entry.path, blobId: entry.objectId, projectRoot: orderedRoots.find(root => entry.path.startsWith(`${root}/`)) ?? '.',
|
|
14
|
+
codeRootSelected: selected.has(entry.path),
|
|
15
|
+
requested: project ? project.include.some(pattern => matchesRepositoryGlob(entry.path, pattern)) ? 'included' : 'excluded' : 'unknown',
|
|
16
|
+
...(indexed ? { indexed: indexed.has(entry.path) } : {}),
|
|
17
|
+
})).sort((a, b) => a.path.localeCompare(b.path));
|
|
18
|
+
const projects = [...new Set(['.', ...roots.keys()])].sort().map(root => ({ root, ...(roots.has(root) ? { manifest: roots.get(root) } : {}) }));
|
|
19
|
+
const known = new Set(files.map(file => file.path));
|
|
20
|
+
const plain = { version: 'committed-code-inventory-v1', scipIndexDigest: index.digest,
|
|
21
|
+
selection: project ? 'source-project' : 'indexer-command',
|
|
22
|
+
indexObservation: indexed ? 'recorded' : 'unavailable', codeRoots: [...codeRoots],
|
|
23
|
+
...(project ? { includePatterns: [...project.include] } : {}), projects, files,
|
|
24
|
+
...(indexed ? { otherIndexedFiles: [...indexed].filter(path => !known.has(path)).length } : {}) };
|
|
25
|
+
return { ...plain, digest: sha256(stableJson(plain)) };
|
|
26
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ExtractionContext, GraphDiagnostic, GraphFact, SemanticSource } from '@weftlet/core';
|
|
2
|
+
import type { GitRepository } from '../repository/git.js';
|
|
3
|
+
export declare const documentCodeBindingIdentity: {
|
|
4
|
+
readonly id: "document-code-binding-extractor";
|
|
5
|
+
readonly version: "1.0.0";
|
|
6
|
+
};
|
|
7
|
+
export interface BindingSource extends SemanticSource {
|
|
8
|
+
readonly metadata: Pick<GraphFact, 'sourceAuthority' | 'sourceRole' | 'sourceOwnership' | 'sourcePolicyDigest'>;
|
|
9
|
+
}
|
|
10
|
+
/** Resolve only explicit declarations against current indexed evidence; no provider, checkout or basename search. */
|
|
11
|
+
export declare function extractDocumentCodeBindings(repository: Pick<GitRepository, 'listEntriesAt' | 'readFileAt'>, context: ExtractionContext, sources: readonly BindingSource[], codeFacts: readonly GraphFact[], subjectFacts: readonly GraphFact[]): Promise<{
|
|
12
|
+
facts: readonly GraphFact[];
|
|
13
|
+
diagnostics: readonly GraphDiagnostic[];
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { repositoryFileNode } from '@weftlet/core';
|
|
2
|
+
import { sha256, stableJson } from '../infrastructure/util.js';
|
|
3
|
+
import { documentSymbolLinks } from './symbol-selectors.js';
|
|
4
|
+
import { resolveSymbolSelector } from './symbol-resolution.js';
|
|
5
|
+
export const documentCodeBindingIdentity = { id: 'document-code-binding-extractor', version: '1.0.0' };
|
|
6
|
+
const subjectKinds = new Set(['Capability', 'Workflow', 'Contract', 'Invariant']);
|
|
7
|
+
const proof = (fact) => ({ factId: fact.id, provenance: fact.provenance });
|
|
8
|
+
/** Resolve the declared subject independently of code selection; retain every supporting assertion. */
|
|
9
|
+
function mappingSubject(subjectId, predicate, source, subjectFacts) {
|
|
10
|
+
if (!subjectId)
|
|
11
|
+
return { subject: repositoryFileNode(source.path), basis: [] };
|
|
12
|
+
const basis = subjectFacts.filter(fact => !fact.codeBinding && [fact.subject, fact.object].some(node => node.id === subjectId));
|
|
13
|
+
const nodes = basis.flatMap(fact => [fact.subject, fact.object].filter(node => node.id === subjectId));
|
|
14
|
+
if (!nodes.length || new Set(nodes.map(node => node.kind)).size !== 1 || !subjectKinds.has(nodes[0].kind) || basis.length > 100) {
|
|
15
|
+
return { problem: 'Mapping subject must identify one existing business node with at most 100 supporting assertions.' };
|
|
16
|
+
}
|
|
17
|
+
if (source.metadata.sourceRole === 'validation' && predicate === 'implementedBy') {
|
|
18
|
+
return { problem: 'A validation document cannot establish a product implementation mapping.' };
|
|
19
|
+
}
|
|
20
|
+
return { subject: nodes[0], basis };
|
|
21
|
+
}
|
|
22
|
+
/** Keep both a reference use and all destination declarations in the evidence envelope. */
|
|
23
|
+
function linkCitations(source, link, commitSha) {
|
|
24
|
+
const cite = (startLine, endLine) => ({ path: source.path, startLine, endLine,
|
|
25
|
+
digest: source.digest, commitSha });
|
|
26
|
+
const citations = [cite(link.startLine, link.endLine)];
|
|
27
|
+
if (link.referenceStartLine !== undefined)
|
|
28
|
+
citations.push(cite(link.referenceStartLine, link.referenceEndLine));
|
|
29
|
+
for (const definition of link.definitions ?? [])
|
|
30
|
+
citations.push(cite(definition.startLine, definition.endLine));
|
|
31
|
+
return [...new Map(citations.map(span => [stableJson(span), span])).values()];
|
|
32
|
+
}
|
|
33
|
+
/** Resolve only explicit declarations against current indexed evidence; no provider, checkout or basename search. */
|
|
34
|
+
export async function extractDocumentCodeBindings(repository, context, sources, codeFacts, subjectFacts) {
|
|
35
|
+
const facts = new Map(), diagnostics = new Map();
|
|
36
|
+
const selectedSources = sources.map(source => ({ source, links: documentSymbolLinks(source.path, source.content) })).filter(item => item.links.length);
|
|
37
|
+
if (!selectedSources.length)
|
|
38
|
+
return { facts: [], diagnostics: [] };
|
|
39
|
+
const entries = new Map((await repository.listEntriesAt(context.commitSha)).map(entry => [entry.path, entry]));
|
|
40
|
+
const current = codeFacts.filter(fact => fact.provenance.commitSha === context.commitSha);
|
|
41
|
+
const texts = new Map();
|
|
42
|
+
const read = async (path) => {
|
|
43
|
+
let text = texts.get(path);
|
|
44
|
+
if (text === undefined) {
|
|
45
|
+
text = await repository.readFileAt(context.commitSha, path);
|
|
46
|
+
texts.set(path, text);
|
|
47
|
+
}
|
|
48
|
+
return text;
|
|
49
|
+
};
|
|
50
|
+
const regular = (path) => { const entry = entries.get(path); return entry?.type === 'blob' && ['100644', '100755'].includes(entry.mode); };
|
|
51
|
+
const verifyDefinitions = async (proofs) => {
|
|
52
|
+
if (proofs.length > 1000)
|
|
53
|
+
return `Binding requires ${proofs.length} occurrence proofs, exceeding the 1000-proof bound; no partial binding emitted.`;
|
|
54
|
+
for (const fact of proofs) {
|
|
55
|
+
if (!regular(fact.provenance.path) || fact.provenance.digest !== sha256(await read(fact.provenance.path))) {
|
|
56
|
+
return 'Indexed definition does not match regular committed target bytes.';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
};
|
|
61
|
+
for (const { source, links } of selectedSources) {
|
|
62
|
+
for (const item of links) {
|
|
63
|
+
const documentCitations = linkCitations(source, item.link, context.commitSha);
|
|
64
|
+
const gap = (reason, related = []) => {
|
|
65
|
+
const record = { kind: 'implementationGaps', reason, provenance: documentCitations,
|
|
66
|
+
relatedNodeIds: [...new Set([repositoryFileNode(source.path).id, ...related])].sort(), evidenceScope: 'source' };
|
|
67
|
+
const id = sha256(stableJson({ ...record, extractor: documentCodeBindingIdentity }));
|
|
68
|
+
diagnostics.set(id, { id, ...record });
|
|
69
|
+
};
|
|
70
|
+
if (!regular(source.path) || sha256(source.content) !== source.digest || await read(source.path) !== source.content) {
|
|
71
|
+
gap('Binding document does not match regular committed source bytes.');
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (item.problem || !item.selector) {
|
|
75
|
+
gap(item.problem);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const selector = item.selector, entry = entries.get(selector.path);
|
|
79
|
+
if (!entry || entry.type !== 'blob' || !['100644', '100755'].includes(entry.mode)) {
|
|
80
|
+
gap(`Symbol target ${selector.path} is not a regular committed file.`, [repositoryFileNode(selector.path).id]);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const text = await read(selector.path);
|
|
84
|
+
if (current.some(fact => fact.provenance.path === selector.path && fact.provenance.digest !== sha256(text))) {
|
|
85
|
+
gap('Indexed target does not match the cited committed bytes.');
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const resolution = resolveSymbolSelector(selector, current, text);
|
|
89
|
+
if ('problem' in resolution) {
|
|
90
|
+
gap(`${resolution.problem} Candidate identities: ${resolution.candidates.length}; at most 100 IDs are included.`, resolution.candidates.slice(0, 100));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const proofProblem = await verifyDefinitions([...resolution.selected, ...resolution.definitions]);
|
|
94
|
+
if (proofProblem) {
|
|
95
|
+
gap(proofProblem, [resolution.node.id]);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const mapped = mappingSubject(item.subjectId, item.predicate, source, subjectFacts);
|
|
99
|
+
if ('problem' in mapped) {
|
|
100
|
+
gap(mapped.problem, item.subjectId ? [item.subjectId] : []);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const { subject, basis } = mapped;
|
|
104
|
+
const subjectEvidence = basis.map(fact => ({ ...proof(fact),
|
|
105
|
+
...(fact.authority === undefined ? {} : { authority: fact.authority }),
|
|
106
|
+
...(fact.sourceAuthority === undefined ? {} : { sourceAuthority: fact.sourceAuthority }),
|
|
107
|
+
...(fact.extractionReview === undefined ? {} : { extractionReview: fact.extractionReview }),
|
|
108
|
+
})).sort((a, b) => a.factId.localeCompare(b.factId));
|
|
109
|
+
const codeBinding = { schemaVersion: 1, method: 'explicit-markdown', selector,
|
|
110
|
+
documentCitations, selectedOccurrences: resolution.selected.map(proof), definitions: resolution.definitions.map(proof), subjectEvidence };
|
|
111
|
+
const assertion = { subject, predicate: item.predicate, object: resolution.node, provenance: documentCitations[0],
|
|
112
|
+
codeBinding, extractor: documentCodeBindingIdentity, confidence: Math.min(1, ...basis.map(fact => fact.confidence)),
|
|
113
|
+
authority: 'inferred', ...source.metadata,
|
|
114
|
+
extractionReview: basis.some(fact => fact.extractionReview === 'unreviewed') ? 'unreviewed' : 'deterministic' };
|
|
115
|
+
const id = sha256(stableJson(assertion));
|
|
116
|
+
facts.set(id, { id, ...assertion, snapshotId: context.snapshotId });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { facts: [...facts.values()].sort((a, b) => a.id.localeCompare(b.id)),
|
|
120
|
+
diagnostics: [...diagnostics.values()].sort((a, b) => a.id.localeCompare(b.id)) };
|
|
121
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ExtractionContext, type GraphDiagnostic, type GraphFact } from '@weftlet/core';
|
|
2
|
+
import type { GitRepository } from '../repository/git.js';
|
|
3
|
+
import type { BindingSource } from './document-code-bindings.js';
|
|
4
|
+
export declare const documentObligationIdentity: {
|
|
5
|
+
readonly id: "document-obligation-extractor";
|
|
6
|
+
readonly version: "1.0.0";
|
|
7
|
+
};
|
|
8
|
+
/** Exact opt-in declarations only. Prose interpretation, inferred transport joins and policy verdicts are separate. */
|
|
9
|
+
export declare function extractDocumentObligations(repository: Pick<GitRepository, 'listEntriesAt' | 'readFileAt'>, context: ExtractionContext, sources: readonly BindingSource[]): Promise<{
|
|
10
|
+
facts: readonly GraphFact[];
|
|
11
|
+
diagnostics: readonly GraphDiagnostic[];
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { fromMarkdown } from 'mdast-util-from-markdown';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { obligationContractNode, parseObligationDeclaration, repositoryFileNode } from '@weftlet/core';
|
|
4
|
+
import { parseUniqueJson } from '../infrastructure/unique-json.js';
|
|
5
|
+
import { sha256, stableJson } from '../infrastructure/util.js';
|
|
6
|
+
export const documentObligationIdentity = { id: 'document-obligation-extractor', version: '1.0.0' };
|
|
7
|
+
/** Git names the original blob bytes. Reject text decoding that cannot reproduce them. */
|
|
8
|
+
function matchesCommittedBlob(content, objectId) {
|
|
9
|
+
const bytes = Buffer.from(content, 'utf8');
|
|
10
|
+
const algorithm = objectId.length === 64 ? 'sha256' : 'sha1';
|
|
11
|
+
return createHash(algorithm).update(`blob ${bytes.length}\0`).update(bytes).digest('hex') === objectId;
|
|
12
|
+
}
|
|
13
|
+
function blocks(source, commitSha) {
|
|
14
|
+
const result = [];
|
|
15
|
+
const visit = (node) => {
|
|
16
|
+
if (node.type === 'code' && node.lang === 'weftlet-obligation')
|
|
17
|
+
result.push({ content: node.value,
|
|
18
|
+
provenance: { path: source.path, digest: source.digest, commitSha,
|
|
19
|
+
startLine: node.position.start.line, endLine: node.position.end.line } });
|
|
20
|
+
if ('children' in node)
|
|
21
|
+
for (const child of node.children)
|
|
22
|
+
visit(child);
|
|
23
|
+
};
|
|
24
|
+
visit(fromMarkdown(source.content));
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
/** Exact opt-in declarations only. Prose interpretation, inferred transport joins and policy verdicts are separate. */
|
|
28
|
+
export async function extractDocumentObligations(repository, context, sources) {
|
|
29
|
+
const selected = sources.map(source => ({ source, blocks: blocks(source, context.commitSha) })).filter(item => item.blocks.length);
|
|
30
|
+
if (!selected.length)
|
|
31
|
+
return { facts: [], diagnostics: [] };
|
|
32
|
+
if (selected.reduce((sum, item) => sum + item.blocks.length, 0) > 500)
|
|
33
|
+
throw new Error('Obligation sources exceed the 500-declaration bound; no partial extraction');
|
|
34
|
+
const entries = new Map((await repository.listEntriesAt(context.commitSha)).map(entry => [entry.path, entry]));
|
|
35
|
+
const regular = (path) => { const entry = entries.get(path); return entry?.type === 'blob' && ['100644', '100755'].includes(entry.mode); };
|
|
36
|
+
const texts = new Map();
|
|
37
|
+
const read = async (path) => {
|
|
38
|
+
if (!texts.has(path))
|
|
39
|
+
texts.set(path, await repository.readFileAt(context.commitSha, path));
|
|
40
|
+
return texts.get(path);
|
|
41
|
+
};
|
|
42
|
+
const facts = [], diagnostics = [];
|
|
43
|
+
const gap = (reason, provenance, relatedNodeIds) => {
|
|
44
|
+
const item = { reason: `Contract obligation: ${reason}`, provenance, relatedNodeIds,
|
|
45
|
+
kind: 'implementationGaps', evidenceScope: 'source' };
|
|
46
|
+
diagnostics.push({ ...item, id: sha256(stableJson({ ...item, extractor: documentObligationIdentity })) });
|
|
47
|
+
};
|
|
48
|
+
for (const { source, blocks } of selected) {
|
|
49
|
+
if (!regular(source.path) || sha256(source.content) !== source.digest || await read(source.path) !== source.content ||
|
|
50
|
+
!matchesCommittedBlob(source.content, entries.get(source.path).objectId)) {
|
|
51
|
+
gap('source does not match regular committed bytes.', blocks.map(block => block.provenance), [repositoryFileNode(source.path).id]);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (source.metadata.sourceRole === 'validation') {
|
|
55
|
+
gap('validation sources cannot declare producer guarantees or consumer requirements.', blocks.map(block => block.provenance), [repositoryFileNode(source.path).id]);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
for (const block of blocks) {
|
|
59
|
+
let declaration;
|
|
60
|
+
try {
|
|
61
|
+
if (Buffer.byteLength(block.content) > 65_536)
|
|
62
|
+
throw new Error('Declaration exceeds 64 KiB');
|
|
63
|
+
declaration = parseObligationDeclaration(parseUniqueJson(block.content));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
gap('invalid declaration: use bounded schemaVersion 1 JSON with unique supported keys; no relationship emitted.', [block.provenance], [repositoryFileNode(source.path).id]);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const subject = repositoryFileNode(declaration.endpoint), object = obligationContractNode(context.repositoryId, declaration.contract);
|
|
70
|
+
if (!regular(declaration.endpoint)) {
|
|
71
|
+
gap('endpoint is missing or is not a regular committed file; no relationship emitted.', [block.provenance], [subject.id, object.id]);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const endpoint = await read(declaration.endpoint);
|
|
75
|
+
if (!matchesCommittedBlob(endpoint, entries.get(declaration.endpoint).objectId)) {
|
|
76
|
+
gap('endpoint cannot be read as lossless committed UTF-8 text; no relationship emitted.', [block.provenance], [subject.id, object.id]);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const assertion = { subject, object, predicate: declaration.role === 'guarantee' ? 'emits' : 'consumes',
|
|
80
|
+
provenance: block.provenance, extractor: documentObligationIdentity, confidence: 1,
|
|
81
|
+
authority: 'inferred', extractionReview: 'deterministic', ...source.metadata,
|
|
82
|
+
contractObligation: { method: 'explicit-markdown', repositoryId: context.repositoryId, declaration,
|
|
83
|
+
endpointSource: { path: declaration.endpoint, digest: sha256(endpoint), commitSha: context.commitSha,
|
|
84
|
+
startLine: 1, endLine: endpoint.split('\n').length } } };
|
|
85
|
+
facts.push({ ...assertion, id: sha256(stableJson(assertion)), snapshotId: context.snapshotId });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
diagnoseObligationGroups(facts, gap);
|
|
89
|
+
return { facts: facts.sort((a, b) => a.id.localeCompare(b.id)), diagnostics: diagnostics.sort((a, b) => a.id.localeCompare(b.id)) };
|
|
90
|
+
}
|
|
91
|
+
function diagnoseObligationGroups(facts, gap) {
|
|
92
|
+
const identities = new Map();
|
|
93
|
+
for (const fact of facts) {
|
|
94
|
+
const key = stableJson([fact.provenance.path, fact.contractObligation.declaration.id]);
|
|
95
|
+
const group = identities.get(key) ?? [];
|
|
96
|
+
group.push(fact);
|
|
97
|
+
identities.set(key, group);
|
|
98
|
+
}
|
|
99
|
+
for (const group of identities.values())
|
|
100
|
+
if (group.length > 1)
|
|
101
|
+
gap('duplicate declaration ID within a source; all cited assertions retained for review.', group.map(fact => fact.provenance), [...new Set(group.flatMap(fact => [fact.subject.id, fact.object.id]))]);
|
|
102
|
+
const contracts = new Map();
|
|
103
|
+
for (const fact of facts) {
|
|
104
|
+
const group = contracts.get(fact.object.id) ?? [];
|
|
105
|
+
group.push(fact);
|
|
106
|
+
contracts.set(fact.object.id, group);
|
|
107
|
+
}
|
|
108
|
+
for (const group of contracts.values())
|
|
109
|
+
if (new Set(group.map(fact => fact.contractObligation.declaration.role)).size < 2) {
|
|
110
|
+
gap('only one side of this contract has structured obligations in the selected documents; other producers/consumers and compatibility remain unknown.', group.map(fact => fact.provenance), [group[0].object.id]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GraphFact, MappingCandidate } from '@weftlet/core';
|
|
2
|
+
/** Join real callable syntax to exact indexed name occurrences, never fuzzy labels. */
|
|
3
|
+
export declare function mappingCandidates(path: string, text: string, digest: string, commit: string, facts: readonly GraphFact[], limits: {
|
|
4
|
+
maxCandidates: number;
|
|
5
|
+
maxInputBytes: number;
|
|
6
|
+
}): {
|
|
7
|
+
candidates: (MappingCandidate & {
|
|
8
|
+
text: string;
|
|
9
|
+
})[];
|
|
10
|
+
issues: string[];
|
|
11
|
+
excluded: number;
|
|
12
|
+
observed: number;
|
|
13
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { callableName, factsAt, isAmbient, isCallable, parseCallSource, syntaxSpan } from './call-syntax.js';
|
|
3
|
+
/** Join real callable syntax to exact indexed name occurrences, never fuzzy labels. */
|
|
4
|
+
export function mappingCandidates(path, text, digest, commit, facts, limits) {
|
|
5
|
+
const parsed = parseCallSource(path, text, digest, facts);
|
|
6
|
+
const candidates = [], issues = [];
|
|
7
|
+
let excluded = 0, observed = 0;
|
|
8
|
+
const seen = new Set();
|
|
9
|
+
if (parsed.parseErrors.length)
|
|
10
|
+
return { candidates, issues: ['invalid-typescript'], excluded, observed };
|
|
11
|
+
const add = (node, definitions) => {
|
|
12
|
+
observed++;
|
|
13
|
+
const symbol = definitions[0].subject;
|
|
14
|
+
if (seen.has(symbol.id))
|
|
15
|
+
issues.push('multiple-implementation-bodies');
|
|
16
|
+
seen.add(symbol.id);
|
|
17
|
+
if (observed > limits.maxCandidates) {
|
|
18
|
+
issues.push('candidate-budget-exceeded');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const body = syntaxSpan(node, parsed.source, digest, commit);
|
|
22
|
+
const proofs = facts.filter(f => f.codeOccurrence?.role === 'definition' && f.subject.id === symbol.id);
|
|
23
|
+
// Avoid expanding many overlapping large callable bodies merely to discover a budget failure.
|
|
24
|
+
const excerpt = node.getText(parsed.source), excerptBytes = Buffer.byteLength(excerpt);
|
|
25
|
+
const tooLarge = excerptBytes > limits.maxInputBytes;
|
|
26
|
+
if (tooLarge)
|
|
27
|
+
issues.push('input-budget-exceeded');
|
|
28
|
+
candidates.push({ node: symbol, body, excerptBytes, definitions: proofs, text: tooLarge ? '' : excerpt });
|
|
29
|
+
};
|
|
30
|
+
const visit = (node) => {
|
|
31
|
+
if (isCallable(node)) {
|
|
32
|
+
const definitions = factsAt(callableName(node), parsed, commit, 'definition');
|
|
33
|
+
const ids = new Set(definitions.map(f => f.subject.id));
|
|
34
|
+
if (!node.body || isAmbient(node) || !callableName(node))
|
|
35
|
+
excluded++;
|
|
36
|
+
else if (ids.size !== 1 || definitions.some(f => f.subject.codeSymbol?.resolution !== 'indexed-definition')) {
|
|
37
|
+
issues.push('callable-without-unambiguous-indexed-definition');
|
|
38
|
+
}
|
|
39
|
+
else
|
|
40
|
+
add(node, definitions);
|
|
41
|
+
}
|
|
42
|
+
ts.forEachChild(node, visit);
|
|
43
|
+
};
|
|
44
|
+
visit(parsed.source);
|
|
45
|
+
return { candidates, issues: [...new Set(issues)], excluded, observed };
|
|
46
|
+
}
|