@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,39 @@
|
|
|
1
|
+
export interface ScipOccurrence {
|
|
2
|
+
readonly range: readonly number[];
|
|
3
|
+
readonly symbol: string;
|
|
4
|
+
readonly symbol_roles: number;
|
|
5
|
+
readonly enclosing_range?: readonly number[];
|
|
6
|
+
}
|
|
7
|
+
export interface ScipDocument {
|
|
8
|
+
readonly relative_path: string;
|
|
9
|
+
readonly occurrences: readonly ScipOccurrence[];
|
|
10
|
+
readonly symbols: readonly ScipSymbolInformation[];
|
|
11
|
+
}
|
|
12
|
+
interface ScipSymbolInformation {
|
|
13
|
+
documentation: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface ScipIndex {
|
|
16
|
+
readonly documents: readonly ScipDocument[];
|
|
17
|
+
readonly external_symbols: readonly ScipSymbolInformation[];
|
|
18
|
+
readonly metadata?: {
|
|
19
|
+
project_root: string;
|
|
20
|
+
};
|
|
21
|
+
serializeBinary(): Uint8Array;
|
|
22
|
+
}
|
|
23
|
+
interface ScipRuntime {
|
|
24
|
+
readonly scip: {
|
|
25
|
+
readonly Index: {
|
|
26
|
+
deserializeBinary(bytes: Uint8Array): ScipIndex;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export declare function loadScipRuntime(): ScipRuntime;
|
|
31
|
+
export interface ScipIndexResult {
|
|
32
|
+
readonly path: string;
|
|
33
|
+
readonly digest: string;
|
|
34
|
+
readonly mode: 'full' | 'package';
|
|
35
|
+
readonly version: string;
|
|
36
|
+
/** Exact document paths observed in the normalized index, including zero-occurrence documents. */
|
|
37
|
+
readonly indexedPaths?: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Modified for standalone extraction; see docs/extraction-manifest.json.
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
export function loadScipRuntime() {
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const main = require.resolve('@sourcegraph/scip-typescript');
|
|
7
|
+
return require(join(dirname(main), 'scip.js'));
|
|
8
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ExtractionContext, type FactExtractor, type GraphFact } from '@weftlet/core';
|
|
2
|
+
import type { GitRepository } from '../repository/git.js';
|
|
3
|
+
import type { ScipIndexResult } from './scip-runtime.js';
|
|
4
|
+
export declare const scipSymbolExtractorIdentity: {
|
|
5
|
+
readonly id: "scip-symbol-extractor";
|
|
6
|
+
readonly version: "2.1.0";
|
|
7
|
+
};
|
|
8
|
+
export declare class ScipSymbolExtractor implements FactExtractor {
|
|
9
|
+
private readonly repository;
|
|
10
|
+
private readonly index;
|
|
11
|
+
readonly identity: {
|
|
12
|
+
readonly id: "scip-symbol-extractor";
|
|
13
|
+
readonly version: "2.1.0";
|
|
14
|
+
};
|
|
15
|
+
constructor(repository: GitRepository, index: ScipIndexResult);
|
|
16
|
+
extract(context: ExtractionContext): Promise<readonly GraphFact[]>;
|
|
17
|
+
extractWithDiagnostics(context: ExtractionContext): Promise<{
|
|
18
|
+
facts: GraphFact[];
|
|
19
|
+
diagnostics: import("@weftlet/core").GraphDiagnostic[];
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Modified for standalone extraction; see docs/extraction-manifest.json.
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { repositoryFileNode } from '@weftlet/core';
|
|
4
|
+
import { loadScipRuntime } from './scip-runtime.js';
|
|
5
|
+
import { extractCodeOccurrences } from './scip-occurrences.js';
|
|
6
|
+
import { extractStaticCalls } from './static-calls.js';
|
|
7
|
+
import { sha256, stableJson } from '../infrastructure/util.js';
|
|
8
|
+
export const scipSymbolExtractorIdentity = { id: 'scip-symbol-extractor', version: '2.1.0' };
|
|
9
|
+
export class ScipSymbolExtractor {
|
|
10
|
+
repository;
|
|
11
|
+
index;
|
|
12
|
+
identity = scipSymbolExtractorIdentity;
|
|
13
|
+
constructor(repository, index) {
|
|
14
|
+
this.repository = repository;
|
|
15
|
+
this.index = index;
|
|
16
|
+
}
|
|
17
|
+
async extract(context) {
|
|
18
|
+
return (await this.extractWithDiagnostics(context)).facts;
|
|
19
|
+
}
|
|
20
|
+
async extractWithDiagnostics(context) {
|
|
21
|
+
const parsed = loadScipRuntime().scip.Index.deserializeBinary(await readFile(this.index.path));
|
|
22
|
+
const symbols = new Map();
|
|
23
|
+
const sources = new Map();
|
|
24
|
+
for (const document of parsed.documents) {
|
|
25
|
+
const path = document.relative_path.replace(/\\/g, '/');
|
|
26
|
+
const content = await this.repository.readFileAt(context.commitSha, path);
|
|
27
|
+
sources.set(path, content);
|
|
28
|
+
const digest = sha256(content);
|
|
29
|
+
const component = repositoryFileNode(path);
|
|
30
|
+
for (const occurrence of document.occurrences) {
|
|
31
|
+
if (!occurrence.symbol || occurrence.symbol.startsWith('local '))
|
|
32
|
+
continue;
|
|
33
|
+
const definition = (occurrence.symbol_roles & 1) === 1;
|
|
34
|
+
const sites = symbols.get(occurrence.symbol) ?? {
|
|
35
|
+
definitions: new Map(),
|
|
36
|
+
references: new Map(),
|
|
37
|
+
};
|
|
38
|
+
symbols.set(occurrence.symbol, sites);
|
|
39
|
+
const target = definition ? sites.definitions : sites.references;
|
|
40
|
+
if (target.has(path))
|
|
41
|
+
continue;
|
|
42
|
+
target.set(path, {
|
|
43
|
+
path,
|
|
44
|
+
line: (occurrence.range[0] ?? 0) + 1,
|
|
45
|
+
digest,
|
|
46
|
+
component,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const facts = [];
|
|
51
|
+
for (const [symbolText, sites] of [...symbols.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
52
|
+
const definitionPaths = new Set(sites.definitions.keys());
|
|
53
|
+
const downstream = [...sites.references.values()].filter((site) => !definitionPaths.has(site.path));
|
|
54
|
+
if (sites.definitions.size === 0 || downstream.length === 0)
|
|
55
|
+
continue;
|
|
56
|
+
const symbol = {
|
|
57
|
+
id: `symbol:scip:${sha256(symbolText)}`,
|
|
58
|
+
kind: 'Contract',
|
|
59
|
+
label: symbolText,
|
|
60
|
+
attributes: { scipIndexDigest: this.index.digest },
|
|
61
|
+
};
|
|
62
|
+
const relationships = [
|
|
63
|
+
...[...sites.definitions.values()].map((site) => ({ site, definition: true })),
|
|
64
|
+
...downstream.map((site) => ({ site, definition: false })),
|
|
65
|
+
];
|
|
66
|
+
for (const { site, definition } of relationships) {
|
|
67
|
+
const relation = definition ? 'definition' : 'reference';
|
|
68
|
+
const provenance = {
|
|
69
|
+
path: site.path,
|
|
70
|
+
startLine: site.line,
|
|
71
|
+
endLine: site.line,
|
|
72
|
+
digest: site.digest,
|
|
73
|
+
commitSha: context.commitSha,
|
|
74
|
+
};
|
|
75
|
+
facts.push({
|
|
76
|
+
id: sha256(stableJson({ relation, path: site.path, symbol: symbolText, provenance })),
|
|
77
|
+
subject: site.component,
|
|
78
|
+
predicate: definition ? 'contains' : 'consumes',
|
|
79
|
+
object: symbol,
|
|
80
|
+
provenance,
|
|
81
|
+
extractor: this.identity,
|
|
82
|
+
confidence: 1,
|
|
83
|
+
snapshotId: context.snapshotId,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const occurrences = extractCodeOccurrences(parsed, sources, context, this.index.digest, this.identity);
|
|
88
|
+
const calls = extractStaticCalls(sources, occurrences, context);
|
|
89
|
+
return { facts: [...facts, ...occurrences, ...calls.facts], diagnostics: calls.diagnostics };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -14,7 +14,7 @@ export interface ExtractedAssertion {
|
|
|
14
14
|
readonly kind: GraphNodeKind;
|
|
15
15
|
readonly label: string;
|
|
16
16
|
};
|
|
17
|
-
readonly predicate: GraphPredicate
|
|
17
|
+
readonly predicate: Exclude<GraphPredicate, 'calls'>;
|
|
18
18
|
readonly object: {
|
|
19
19
|
readonly id: string;
|
|
20
20
|
readonly kind: GraphNodeKind;
|
|
@@ -7,6 +7,9 @@ export { semanticCommandEnvironment } from './codex-auth.js';
|
|
|
7
7
|
import { resolveCodexInvocation } from './codex-command.js';
|
|
8
8
|
import { runSemanticProcess, SemanticProviderError } from "./semantic-process.js";
|
|
9
9
|
export { SemanticProviderError } from "./semantic-process.js";
|
|
10
|
+
import { graphPredicates } from "@weftlet/core";
|
|
11
|
+
// Calls require indexed syntax/proofs. Keep the prose schema and its cache identity unchanged.
|
|
12
|
+
const predicates = graphPredicates.filter(predicate => predicate !== 'calls');
|
|
10
13
|
import { sha256, stableJson } from "../infrastructure/util.js";
|
|
11
14
|
import { canonicalizeRepositoryDocumentNode } from './repository-nodes.js';
|
|
12
15
|
import { semanticArtifactKey, semanticArtifactOutputDigest, semanticProviderIdentity } from './semantic-identity.js';
|
|
@@ -79,6 +82,8 @@ function renderOml(key, assertions) {
|
|
|
79
82
|
const instances = [...nodes.entries()]
|
|
80
83
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
81
84
|
.map(([id, node]) => {
|
|
85
|
+
if (node.kind === 'CodeSymbol')
|
|
86
|
+
throw new Error('Code symbols require deterministic code extraction');
|
|
82
87
|
const local = localNames.get(id);
|
|
83
88
|
const properties = [
|
|
84
89
|
` cg:hasIdentifier "${omlEscape(node.id)}"`,
|
|
@@ -119,23 +124,6 @@ const kinds = [
|
|
|
119
124
|
'PersistenceModel',
|
|
120
125
|
'Scope',
|
|
121
126
|
];
|
|
122
|
-
const predicates = [
|
|
123
|
-
'realizes',
|
|
124
|
-
'implementedBy',
|
|
125
|
-
'consumes',
|
|
126
|
-
'emits',
|
|
127
|
-
'persistsTo',
|
|
128
|
-
'governedBy',
|
|
129
|
-
'definedBy',
|
|
130
|
-
'verifiedBy',
|
|
131
|
-
'operationallyOwnedBy',
|
|
132
|
-
'decisionAuthority',
|
|
133
|
-
'routesThrough',
|
|
134
|
-
'authorizedBy',
|
|
135
|
-
'sourcedFrom',
|
|
136
|
-
'references',
|
|
137
|
-
'contains',
|
|
138
|
-
];
|
|
139
127
|
export function semanticOutputSchema(lineCount) {
|
|
140
128
|
const node = {
|
|
141
129
|
type: 'object',
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ExtractionContext, type GraphDiagnostic, type GraphFact } from '@weftlet/core';
|
|
2
|
+
export declare const staticCallIdentity: {
|
|
3
|
+
readonly id: "typescript-call-extractor";
|
|
4
|
+
readonly version: "1.0.0";
|
|
5
|
+
};
|
|
6
|
+
/** Parse only the exact committed documents already indexed by SCIP. No second resolver or name guessing. */
|
|
7
|
+
export declare function extractStaticCalls(sources: ReadonlyMap<string, string>, codeFacts: readonly GraphFact[], context: ExtractionContext): {
|
|
8
|
+
facts: GraphFact[];
|
|
9
|
+
diagnostics: GraphDiagnostic[];
|
|
10
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { repositoryFileNode } from '@weftlet/core';
|
|
3
|
+
import { sha256, stableJson } from '../infrastructure/util.js';
|
|
4
|
+
import { calleeToken, callableName, factsAt, isAmbient, isCallable, parseCallSource, syntaxSpan } from './call-syntax.js';
|
|
5
|
+
export const staticCallIdentity = { id: 'typescript-call-extractor', version: '1.0.0' };
|
|
6
|
+
const proof = (fact) => ({ factId: fact.id, provenance: fact.provenance });
|
|
7
|
+
function targetResolution(syntax, defs, kinds) {
|
|
8
|
+
if (!defs.length)
|
|
9
|
+
return 'external';
|
|
10
|
+
if (syntax === 'construct' && kinds?.has('class'))
|
|
11
|
+
return 'indexed-class';
|
|
12
|
+
if (kinds?.has('body'))
|
|
13
|
+
return 'indexed-body';
|
|
14
|
+
if (kinds?.has('declaration'))
|
|
15
|
+
return 'declaration-only';
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
/** Parse only the exact committed documents already indexed by SCIP. No second resolver or name guessing. */
|
|
19
|
+
export function extractStaticCalls(sources, codeFacts, context) {
|
|
20
|
+
const parsed = [], kinds = new Map();
|
|
21
|
+
const definitions = new Map(), local = new Map();
|
|
22
|
+
for (const fact of codeFacts.filter(f => f.codeOccurrence)) {
|
|
23
|
+
const group = local.get(fact.provenance.path) ?? [];
|
|
24
|
+
group.push(fact);
|
|
25
|
+
local.set(fact.provenance.path, group);
|
|
26
|
+
if (fact.codeOccurrence.role === 'definition') {
|
|
27
|
+
const defs = definitions.get(fact.subject.id) ?? [];
|
|
28
|
+
defs.push(fact);
|
|
29
|
+
definitions.set(fact.subject.id, defs);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const [path, text] of sources) {
|
|
33
|
+
const digest = sha256(text), facts = local.get(path) ?? [];
|
|
34
|
+
if (facts.some(f => f.provenance.commitSha !== context.commitSha || f.provenance.digest !== digest)) {
|
|
35
|
+
throw new Error('Call analysis requires matching committed SCIP source bytes');
|
|
36
|
+
}
|
|
37
|
+
const file = parseCallSource(path, text, digest, facts);
|
|
38
|
+
parsed.push(file);
|
|
39
|
+
if (file.parseErrors.length)
|
|
40
|
+
continue;
|
|
41
|
+
const classify = (node) => {
|
|
42
|
+
const name = isCallable(node) ? callableName(node) : ts.isClassDeclaration(node) || ts.isClassExpression(node) ? node.name :
|
|
43
|
+
ts.isMethodSignature(node) ? node.name : undefined;
|
|
44
|
+
const kind = isCallable(node) ? node.body && !isAmbient(node) ? 'body' : 'declaration' :
|
|
45
|
+
ts.isClassDeclaration(node) || ts.isClassExpression(node) ? isAmbient(node) ? 'declaration' : 'class' : ts.isMethodSignature(node) ? 'declaration' : undefined;
|
|
46
|
+
if (kind)
|
|
47
|
+
for (const fact of factsAt(name, file, context.commitSha, 'definition')) {
|
|
48
|
+
const values = kinds.get(fact.subject.id) ?? new Set();
|
|
49
|
+
values.add(kind);
|
|
50
|
+
kinds.set(fact.subject.id, values);
|
|
51
|
+
}
|
|
52
|
+
ts.forEachChild(node, classify);
|
|
53
|
+
};
|
|
54
|
+
classify(file.source);
|
|
55
|
+
}
|
|
56
|
+
const facts = [], diagnostics = [];
|
|
57
|
+
for (const file of parsed) {
|
|
58
|
+
const physical = repositoryFileNode(file.source.fileName);
|
|
59
|
+
const gap = (node, caller, reason, targets = []) => {
|
|
60
|
+
const record = { kind: 'implementationGaps', reason: `Static call: ${reason}`,
|
|
61
|
+
provenance: [syntaxSpan(node, file.source, file.digest, context.commitSha)],
|
|
62
|
+
relatedNodeIds: [...new Set([physical.id, caller.node.id, ...targets.map(n => n.id)])].sort(), evidenceScope: 'source' };
|
|
63
|
+
diagnostics.push({ ...record, id: sha256(stableJson({ ...record, extractor: staticCallIdentity })) });
|
|
64
|
+
};
|
|
65
|
+
if (file.parseErrors.length) {
|
|
66
|
+
gap(file.source, { node: physical, definitions: [] }, 'TypeScript syntax errors prevent call analysis in this indexed document.');
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const emit = (node, caller) => {
|
|
70
|
+
const token = calleeToken(node.expression), selected = factsAt(token, file, context.commitSha, 'reference');
|
|
71
|
+
const targets = [...new Map(selected.map(f => [f.object.id, f.object])).values()];
|
|
72
|
+
if (caller.unresolved) {
|
|
73
|
+
gap(node, caller, 'Enclosing anonymous callable or initializer has no exact indexed callable identity.', targets);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (targets.length !== 1) {
|
|
77
|
+
gap(node, caller, 'Callee expression has no single exact indexed target; dynamic dispatch is unresolved.', targets);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const target = targets[0], defs = definitions.get(target.id) ?? [], targetKinds = kinds.get(target.id);
|
|
81
|
+
const syntax = ts.isNewExpression(node) ? 'construct' : 'call';
|
|
82
|
+
const resolution = targetResolution(syntax, defs, targetKinds);
|
|
83
|
+
if (!resolution) {
|
|
84
|
+
gap(node, caller, 'Indexed value is not a known callable body or declaration; value flow, callbacks and dispatch are unresolved.', [target]);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const call = { schemaVersion: 1, method: 'typescript-ast+scip', syntax,
|
|
88
|
+
dispatch: syntax === 'construct' ? 'construct' : token && (ts.isPropertyAccessExpression(token.parent) || ts.isElementAccessExpression(token.parent)) ? 'member' : 'lexical',
|
|
89
|
+
optional: Boolean(node.flags & ts.NodeFlags.OptionalChain), caller: caller.definitions.length ? 'indexed-callable' : 'file-scope',
|
|
90
|
+
target: resolution, runtime: 'not-observed', callerDefinitions: caller.definitions.map(proof),
|
|
91
|
+
targetOccurrences: selected.map(proof), targetDefinitions: defs.map(proof) };
|
|
92
|
+
const fact = { subject: caller.node, predicate: 'calls', object: target,
|
|
93
|
+
provenance: syntaxSpan(node, file.source, file.digest, context.commitSha), codeCall: call,
|
|
94
|
+
extractor: staticCallIdentity, authority: 'implemented', extractionReview: 'deterministic', confidence: 1 };
|
|
95
|
+
facts.push({ ...fact, id: sha256(stableJson(fact)), snapshotId: context.snapshotId });
|
|
96
|
+
if (resolution === 'external' || resolution === 'declaration-only')
|
|
97
|
+
gap(node, caller, 'The selected declaration has no indexed callable body; runtime implementation remains unknown.', [target]);
|
|
98
|
+
};
|
|
99
|
+
const walk = (node, caller) => {
|
|
100
|
+
// Ambient declarations describe types; decorators/default syntax there is not executed.
|
|
101
|
+
if (ts.canHaveModifiers(node) && ts.getModifiers(node)?.some(modifier => modifier.kind === ts.SyntaxKind.DeclareKeyword))
|
|
102
|
+
return;
|
|
103
|
+
if (isCallable(node)) {
|
|
104
|
+
const defs = factsAt(callableName(node), file, context.commitSha, 'definition');
|
|
105
|
+
const ids = new Set(defs.map(f => f.subject.id));
|
|
106
|
+
const inner = ids.size === 1 ? { node: defs[0].subject, definitions: defs }
|
|
107
|
+
: { node: caller.node, definitions: [], unresolved: true };
|
|
108
|
+
// Decorators/computed names execute in the outer context, not in the function body.
|
|
109
|
+
ts.forEachChild(node, child => {
|
|
110
|
+
if (ts.isParameter(child))
|
|
111
|
+
ts.forEachChild(child, part => walk(part, part === child.initializer ? inner : caller));
|
|
112
|
+
else
|
|
113
|
+
walk(child, child === node.body ? inner : caller);
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (ts.isPropertyDeclaration(node) && node.initializer) {
|
|
118
|
+
ts.forEachChild(node, child => walk(child, child === node.initializer ? { node: caller.node, definitions: [], unresolved: true } : caller));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (ts.isClassStaticBlockDeclaration(node)) {
|
|
122
|
+
ts.forEachChild(node, child => walk(child, { node: caller.node, definitions: [], unresolved: true }));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (ts.isCallExpression(node) || ts.isNewExpression(node))
|
|
126
|
+
emit(node, caller);
|
|
127
|
+
ts.forEachChild(node, child => walk(child, caller));
|
|
128
|
+
};
|
|
129
|
+
if (!file.source.isDeclarationFile)
|
|
130
|
+
walk(file.source, { node: physical, definitions: [] });
|
|
131
|
+
}
|
|
132
|
+
return { facts: facts.sort((a, b) => a.id.localeCompare(b.id)), diagnostics: diagnostics.sort((a, b) => a.id.localeCompare(b.id)) };
|
|
133
|
+
}
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import type { ContextGraphConfig } from "../configuration/config.js";
|
|
2
2
|
import { type ExtractionContext, type FactExtractor, type GraphFact } from "@weftlet/core";
|
|
3
3
|
import { GitRepository } from "../repository/git.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
readonly digest: string;
|
|
7
|
-
readonly mode: 'full' | 'package';
|
|
8
|
-
readonly version: string;
|
|
9
|
-
}
|
|
4
|
+
import type { ScipIndexResult } from './scip-runtime.js';
|
|
5
|
+
export type { ScipIndexResult } from './scip-runtime.js';
|
|
10
6
|
export interface StaticIndexer {
|
|
11
7
|
index(context: ExtractionContext): Promise<ScipIndexResult>;
|
|
12
8
|
}
|
|
@@ -17,20 +13,7 @@ export declare class ScipTypeScriptIndexer implements StaticIndexer {
|
|
|
17
13
|
index(context: ExtractionContext): Promise<ScipIndexResult>;
|
|
18
14
|
}
|
|
19
15
|
export declare function normalizeScipCheckout(bytes: Uint8Array, checkoutRoot: string): Uint8Array;
|
|
20
|
-
export
|
|
21
|
-
readonly id: "scip-symbol-extractor";
|
|
22
|
-
readonly version: "1.1.0";
|
|
23
|
-
};
|
|
24
|
-
export declare class ScipSymbolExtractor implements FactExtractor {
|
|
25
|
-
private readonly repository;
|
|
26
|
-
private readonly index;
|
|
27
|
-
readonly identity: {
|
|
28
|
-
readonly id: "scip-symbol-extractor";
|
|
29
|
-
readonly version: "1.1.0";
|
|
30
|
-
};
|
|
31
|
-
constructor(repository: GitRepository, index: ScipIndexResult);
|
|
32
|
-
extract(context: ExtractionContext): Promise<readonly GraphFact[]>;
|
|
33
|
-
}
|
|
16
|
+
export { ScipSymbolExtractor, scipSymbolExtractorIdentity } from './scip-symbols.js';
|
|
34
17
|
export declare const repositoryFileExtractorIdentity: {
|
|
35
18
|
readonly id: "repository-file-extractor";
|
|
36
19
|
readonly version: "2.0.0";
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// Modified for standalone extraction; see docs/extraction-manifest.json.
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
2
|
import { withCommittedTree } from "../repository/committed-tree.js";
|
|
4
|
-
import { copyFile, mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { copyFile, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
5
4
|
import { createRequire } from 'node:module';
|
|
6
|
-
import {
|
|
5
|
+
import { join } from 'node:path';
|
|
7
6
|
import { pathToFileURL } from 'node:url';
|
|
8
7
|
import { repositoryFileNode } from "@weftlet/core";
|
|
9
8
|
import { GitRepository } from "../repository/git.js";
|
|
@@ -13,6 +12,8 @@ import { moduleRequestNode } from './module-request.js';
|
|
|
13
12
|
import { matchesRepositoryGlob } from "../repository/glob.js";
|
|
14
13
|
import { runProcess } from "../infrastructure/process.js";
|
|
15
14
|
import { sha256, stableJson } from "../infrastructure/util.js";
|
|
15
|
+
import { loadScipRuntime } from './scip-runtime.js';
|
|
16
|
+
import { physicalArtifactKey, temporaryArtifactPath } from '../infrastructure/artifact-paths.js';
|
|
16
17
|
export class ScipTypeScriptIndexer {
|
|
17
18
|
config;
|
|
18
19
|
storageRoot;
|
|
@@ -26,9 +27,9 @@ export class ScipTypeScriptIndexer {
|
|
|
26
27
|
assertSourceProjectInvocation(this.config.command, this.config.args);
|
|
27
28
|
}
|
|
28
29
|
const require = createRequire(import.meta.url);
|
|
29
|
-
const destination = join(this.storageRoot, 'scip', `${context.snapshotId}.scip`);
|
|
30
|
+
const destination = join(this.storageRoot, 'scip', `${physicalArtifactKey(context.snapshotId)}.scip`);
|
|
30
31
|
await mkdir(join(this.storageRoot, 'scip'), { recursive: true });
|
|
31
|
-
const temporary =
|
|
32
|
+
const temporary = temporaryArtifactPath(destination);
|
|
32
33
|
const command = this.config.command === 'bundled-scip-typescript' ? process.execPath : this.config.command;
|
|
33
34
|
const args = this.config.command === 'bundled-scip-typescript'
|
|
34
35
|
? [
|
|
@@ -38,36 +39,40 @@ export class ScipTypeScriptIndexer {
|
|
|
38
39
|
temporary,
|
|
39
40
|
]
|
|
40
41
|
: this.config.args;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
...
|
|
45
|
-
|
|
42
|
+
try {
|
|
43
|
+
await withCommittedTree(context.repositoryRoot, context.commitSha, async (worktreeRoot) => {
|
|
44
|
+
const project = this.config.sourceProject ? await prepareScipSourceProject(worktreeRoot, this.config.sourceProject, await new GitRepository(context.repositoryRoot).listAllFilesAt(context.commitSha)) : undefined;
|
|
45
|
+
await runProcess(command, project ? [...args, project] : args, worktreeRoot, {
|
|
46
|
+
...process.env,
|
|
47
|
+
NODE_OPTIONS: process.env.NODE_OPTIONS ?? '--max-old-space-size=8192',
|
|
48
|
+
});
|
|
49
|
+
const source = this.config.command === 'bundled-scip-typescript'
|
|
50
|
+
? temporary
|
|
51
|
+
: join(worktreeRoot, 'index.scip');
|
|
52
|
+
await stat(source);
|
|
53
|
+
if (source !== temporary)
|
|
54
|
+
await copyFile(source, temporary);
|
|
55
|
+
// SCIP embeds this disposable checkout in metadata and inferred-type hover text.
|
|
56
|
+
// Store the normalized artifact itself so its digest still authenticates its bytes.
|
|
57
|
+
await writeFile(temporary, normalizeScipCheckout(await readFile(temporary), worktreeRoot));
|
|
46
58
|
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
await
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
await writeFile(temporary, normalizeScipCheckout(await readFile(temporary), worktreeRoot));
|
|
56
|
-
});
|
|
57
|
-
await rename(temporary, destination);
|
|
59
|
+
await rename(temporary, destination);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
await rm(temporary, { force: true });
|
|
63
|
+
}
|
|
64
|
+
const bytes = await readFile(destination);
|
|
65
|
+
const indexedPaths = [...new Set(loadScipRuntime().scip.Index.deserializeBinary(bytes).documents
|
|
66
|
+
.map(document => document.relative_path.replaceAll('\\', '/')))].sort();
|
|
58
67
|
return {
|
|
59
68
|
path: destination,
|
|
60
|
-
digest: sha256(
|
|
69
|
+
digest: sha256(bytes),
|
|
61
70
|
mode: 'full',
|
|
62
71
|
version: this.config.version,
|
|
72
|
+
indexedPaths,
|
|
63
73
|
};
|
|
64
74
|
}
|
|
65
75
|
}
|
|
66
|
-
function loadScipRuntime() {
|
|
67
|
-
const require = createRequire(import.meta.url);
|
|
68
|
-
const main = require.resolve('@sourcegraph/scip-typescript');
|
|
69
|
-
return require(join(dirname(main), 'scip.js'));
|
|
70
|
-
}
|
|
71
76
|
export function normalizeScipCheckout(bytes, checkoutRoot) {
|
|
72
77
|
const runtime = loadScipRuntime();
|
|
73
78
|
const roots = [pathToFileURL(checkoutRoot).href, checkoutRoot.replace(/\\/g, '/'), checkoutRoot];
|
|
@@ -91,83 +96,7 @@ export function normalizeScipCheckout(bytes, checkoutRoot) {
|
|
|
91
96
|
}
|
|
92
97
|
return index.serializeBinary();
|
|
93
98
|
}
|
|
94
|
-
export
|
|
95
|
-
export class ScipSymbolExtractor {
|
|
96
|
-
repository;
|
|
97
|
-
index;
|
|
98
|
-
identity = scipSymbolExtractorIdentity;
|
|
99
|
-
constructor(repository, index) {
|
|
100
|
-
this.repository = repository;
|
|
101
|
-
this.index = index;
|
|
102
|
-
}
|
|
103
|
-
async extract(context) {
|
|
104
|
-
const parsed = loadScipRuntime().scip.Index.deserializeBinary(await readFile(this.index.path));
|
|
105
|
-
const symbols = new Map();
|
|
106
|
-
for (const document of parsed.documents) {
|
|
107
|
-
const path = document.relative_path.replace(/\\/g, '/');
|
|
108
|
-
const content = await this.repository.readFileAt(context.commitSha, path);
|
|
109
|
-
const digest = sha256(content);
|
|
110
|
-
const component = repositoryFileNode(path);
|
|
111
|
-
for (const occurrence of document.occurrences) {
|
|
112
|
-
if (!occurrence.symbol || occurrence.symbol.startsWith('local '))
|
|
113
|
-
continue;
|
|
114
|
-
const definition = (occurrence.symbol_roles & 1) === 1;
|
|
115
|
-
const sites = symbols.get(occurrence.symbol) ?? {
|
|
116
|
-
definitions: new Map(),
|
|
117
|
-
references: new Map(),
|
|
118
|
-
};
|
|
119
|
-
symbols.set(occurrence.symbol, sites);
|
|
120
|
-
const target = definition ? sites.definitions : sites.references;
|
|
121
|
-
if (target.has(path))
|
|
122
|
-
continue;
|
|
123
|
-
target.set(path, {
|
|
124
|
-
path,
|
|
125
|
-
line: (occurrence.range[0] ?? 0) + 1,
|
|
126
|
-
digest,
|
|
127
|
-
component,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
const facts = [];
|
|
132
|
-
for (const [symbolText, sites] of [...symbols.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
133
|
-
const definitionPaths = new Set(sites.definitions.keys());
|
|
134
|
-
const downstream = [...sites.references.values()].filter((site) => !definitionPaths.has(site.path));
|
|
135
|
-
if (sites.definitions.size === 0 || downstream.length === 0)
|
|
136
|
-
continue;
|
|
137
|
-
const symbol = {
|
|
138
|
-
id: `symbol:scip:${sha256(symbolText)}`,
|
|
139
|
-
kind: 'Contract',
|
|
140
|
-
label: symbolText,
|
|
141
|
-
attributes: { scipIndexDigest: this.index.digest },
|
|
142
|
-
};
|
|
143
|
-
const relationships = [
|
|
144
|
-
...[...sites.definitions.values()].map((site) => ({ site, definition: true })),
|
|
145
|
-
...downstream.map((site) => ({ site, definition: false })),
|
|
146
|
-
];
|
|
147
|
-
for (const { site, definition } of relationships) {
|
|
148
|
-
const relation = definition ? 'definition' : 'reference';
|
|
149
|
-
const provenance = {
|
|
150
|
-
path: site.path,
|
|
151
|
-
startLine: site.line,
|
|
152
|
-
endLine: site.line,
|
|
153
|
-
digest: site.digest,
|
|
154
|
-
commitSha: context.commitSha,
|
|
155
|
-
};
|
|
156
|
-
facts.push({
|
|
157
|
-
id: sha256(stableJson({ relation, path: site.path, symbol: symbolText, provenance })),
|
|
158
|
-
subject: site.component,
|
|
159
|
-
predicate: definition ? 'contains' : 'consumes',
|
|
160
|
-
object: symbol,
|
|
161
|
-
provenance,
|
|
162
|
-
extractor: this.identity,
|
|
163
|
-
confidence: 1,
|
|
164
|
-
snapshotId: context.snapshotId,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return facts;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
99
|
+
export { ScipSymbolExtractor, scipSymbolExtractorIdentity } from './scip-symbols.js';
|
|
171
100
|
export const repositoryFileExtractorIdentity = { id: 'repository-file-extractor', version: '2.0.0' };
|
|
172
101
|
export class RepositoryFileExtractor {
|
|
173
102
|
repository;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DocumentCodeBinding, GraphFact, GraphNode } from '@weftlet/core';
|
|
2
|
+
type Selector = DocumentCodeBinding['selector'];
|
|
3
|
+
export type SymbolResolution = {
|
|
4
|
+
readonly node: GraphNode;
|
|
5
|
+
readonly selected: readonly GraphFact[];
|
|
6
|
+
readonly definitions: readonly GraphFact[];
|
|
7
|
+
} | {
|
|
8
|
+
readonly problem: string;
|
|
9
|
+
readonly candidates: readonly string[];
|
|
10
|
+
};
|
|
11
|
+
export declare function resolveSymbolSelector(selector: Selector, facts: readonly GraphFact[], sourceText: string): SymbolResolution;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { stableJson } from '../infrastructure/util.js';
|
|
2
|
+
function occurrenceNode(fact) {
|
|
3
|
+
return fact.codeOccurrence?.role === 'definition' ? fact.subject : fact.object;
|
|
4
|
+
}
|
|
5
|
+
function textAt(span, text) {
|
|
6
|
+
const lines = text.split('\n');
|
|
7
|
+
if (!span.startColumn || !span.endColumn || span.columnEncoding !== 'utf-16' || span.startLine !== span.endLine)
|
|
8
|
+
return undefined;
|
|
9
|
+
return lines[span.startLine - 1]?.slice(span.startColumn - 1, span.endColumn - 1);
|
|
10
|
+
}
|
|
11
|
+
/** Qualification follows indexed lexical parents only; absent or conflicting parents are never invented. */
|
|
12
|
+
function qualifiedName(fact, localDefinitions, seen = new Set()) {
|
|
13
|
+
const node = occurrenceNode(fact), parentId = fact.codeOccurrence?.enclosingSymbolId;
|
|
14
|
+
if (seen.has(node.id) || seen.size >= 64)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (!parentId)
|
|
17
|
+
return node.label;
|
|
18
|
+
const parents = localDefinitions.get(parentId) ?? [];
|
|
19
|
+
if (!parents.length)
|
|
20
|
+
return undefined;
|
|
21
|
+
const named = parents.filter(candidate => candidate.codeOccurrence?.locationKind !== 'zero-width-anchor');
|
|
22
|
+
if (!named.length)
|
|
23
|
+
return node.label;
|
|
24
|
+
const names = new Set(named.map(parent => qualifiedName(parent, localDefinitions, new Set([...seen, node.id]))));
|
|
25
|
+
return names.size === 1 && !names.has(undefined) ? `${[...names][0]}.${node.label}` : undefined;
|
|
26
|
+
}
|
|
27
|
+
export function resolveSymbolSelector(selector, facts, sourceText) {
|
|
28
|
+
const local = facts.filter(fact => fact.codeOccurrence && fact.provenance.path === selector.path);
|
|
29
|
+
const definitions = local.filter(fact => fact.codeOccurrence.role === 'definition' && fact.codeOccurrence.locationKind === 'name');
|
|
30
|
+
const parents = new Map();
|
|
31
|
+
for (const fact of local.filter(item => item.codeOccurrence.role === 'definition')) {
|
|
32
|
+
const entries = parents.get(fact.subject.id) ?? [];
|
|
33
|
+
entries.push(fact);
|
|
34
|
+
parents.set(fact.subject.id, entries);
|
|
35
|
+
}
|
|
36
|
+
const selected = selector.line !== undefined
|
|
37
|
+
? local.filter(fact => fact.provenance.startLine === selector.line && fact.provenance.startColumn === selector.column &&
|
|
38
|
+
(occurrenceNode(fact).id === selector.symbol || textAt(fact.provenance, sourceText) === selector.symbol))
|
|
39
|
+
: definitions.filter(fact => fact.subject.id === selector.symbol || fact.subject.label === selector.symbol ||
|
|
40
|
+
qualifiedName(fact, parents) === selector.symbol);
|
|
41
|
+
const identities = [...new Set(selected.map(fact => occurrenceNode(fact).id))].sort();
|
|
42
|
+
if (identities.length !== 1)
|
|
43
|
+
return { problem: identities.length ? 'Ambiguous indexed symbol; use a qualified name or exact occurrence.'
|
|
44
|
+
: 'No indexed symbol matches this exact selector; no name, path, alias or line was guessed.', candidates: identities };
|
|
45
|
+
const target = occurrenceNode(selected[0]);
|
|
46
|
+
const resolved = facts.filter(fact => fact.codeOccurrence?.role === 'definition' && fact.subject.id === target.id);
|
|
47
|
+
if (!resolved.length)
|
|
48
|
+
return { problem: 'The occurrence has no indexed definition; implementation is unresolved in this saved scope.', candidates: identities };
|
|
49
|
+
const sorted = (items) => [...items].sort((a, b) => stableJson(a.provenance).localeCompare(stableJson(b.provenance)) || a.id.localeCompare(b.id));
|
|
50
|
+
return { node: target, selected: sorted(selected), definitions: sorted(resolved) };
|
|
51
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CanonicalDocumentLink } from './canonical-document-links.js';
|
|
2
|
+
import type { DocumentCodeBinding } from '@weftlet/core';
|
|
3
|
+
export interface SymbolLink {
|
|
4
|
+
readonly link: CanonicalDocumentLink;
|
|
5
|
+
readonly selector?: DocumentCodeBinding['selector'];
|
|
6
|
+
readonly subjectId?: string;
|
|
7
|
+
readonly predicate: 'references' | 'implementedBy' | 'verifiedBy';
|
|
8
|
+
readonly problem?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Only the explicit weftlet-symbol fragment opts a link in; ordinary headings keep their existing meaning. */
|
|
11
|
+
export declare function documentSymbolLinks(sourcePath: string, content: string): readonly SymbolLink[];
|