@orangepro/orangepro-mcp 0.1.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/LICENSE +21 -0
- package/README.md +328 -0
- package/dist/local/agentWorkflow.js +81 -0
- package/dist/local/aiGraph/links.js +635 -0
- package/dist/local/analyze/analyzer.js +2129 -0
- package/dist/local/analyze/behaviorContracts.js +169 -0
- package/dist/local/analyze/boilerplate.js +42 -0
- package/dist/local/analyze/callGraph.js +458 -0
- package/dist/local/analyze/classify.js +219 -0
- package/dist/local/analyze/clustering.js +357 -0
- package/dist/local/analyze/confirm.js +2422 -0
- package/dist/local/analyze/coverage.js +518 -0
- package/dist/local/analyze/coverageArtifacts.js +607 -0
- package/dist/local/analyze/frameworks.js +115 -0
- package/dist/local/analyze/linkage/conventions.js +160 -0
- package/dist/local/analyze/parseCache.js +164 -0
- package/dist/local/analyze/selfAssert.js +53 -0
- package/dist/local/analyze/symbols.js +430 -0
- package/dist/local/analyze/testLayer.js +135 -0
- package/dist/local/analyze/treeSitter/engine.js +1253 -0
- package/dist/local/analyze/treeSitter/languages.js +101 -0
- package/dist/local/autoProve.js +620 -0
- package/dist/local/cli.js +1468 -0
- package/dist/local/cliArgs.js +112 -0
- package/dist/local/corpusScope.js +162 -0
- package/dist/local/enrich/csv.js +348 -0
- package/dist/local/enrich/index.js +43 -0
- package/dist/local/enrich/markdown.js +193 -0
- package/dist/local/explain/explain.js +91 -0
- package/dist/local/exportCli.js +26 -0
- package/dist/local/flows/flowWalker.js +215 -0
- package/dist/local/flows/llmFlowDiscovery.js +567 -0
- package/dist/local/freshness/changed.js +280 -0
- package/dist/local/freshness/manifest.js +35 -0
- package/dist/local/freshness/status.js +30 -0
- package/dist/local/gaps/gaps.js +114 -0
- package/dist/local/generate/buckets.js +73 -0
- package/dist/local/generate/compareJudge.js +124 -0
- package/dist/local/generate/compareReport.js +538 -0
- package/dist/local/generate/compareScore.js +105 -0
- package/dist/local/generate/deriveImports.js +91 -0
- package/dist/local/generate/generator.js +2586 -0
- package/dist/local/generate/prompt.js +144 -0
- package/dist/local/generate/promptV5.js +438 -0
- package/dist/local/generate/providers.js +400 -0
- package/dist/local/generate/runHints.js +304 -0
- package/dist/local/graph/citations.js +73 -0
- package/dist/local/graph/confirmable.js +72 -0
- package/dist/local/graph/factories.js +210 -0
- package/dist/local/graph/ontology.js +18 -0
- package/dist/local/interactive.js +53 -0
- package/dist/local/jobs/jobStore.js +80 -0
- package/dist/local/jobs/notify.js +29 -0
- package/dist/local/jobs/runner.js +75 -0
- package/dist/local/ledger.js +117 -0
- package/dist/local/localConfig.js +112 -0
- package/dist/local/mcp.js +548 -0
- package/dist/local/operations.js +1749 -0
- package/dist/local/pack/coverageReport.js +192 -0
- package/dist/local/pack/exporter.js +195 -0
- package/dist/local/pack/schema.js +128 -0
- package/dist/local/pack/summary.js +127 -0
- package/dist/local/pack/validate.js +25 -0
- package/dist/local/proofRunnability.js +366 -0
- package/dist/local/recipe/dbSqljs.js +255 -0
- package/dist/local/reprove/paths.js +13 -0
- package/dist/local/reprove/scoped.js +136 -0
- package/dist/local/resolve/barrelWalker.js +178 -0
- package/dist/local/resolve/exportIndex.js +270 -0
- package/dist/local/resolve/importGraph.js +347 -0
- package/dist/local/resolve/resolver.js +122 -0
- package/dist/local/resolve/resolverCache.js +117 -0
- package/dist/local/rtm.js +413 -0
- package/dist/local/score/coverage.js +99 -0
- package/dist/local/score/doctor.js +67 -0
- package/dist/local/score/risk.js +362 -0
- package/dist/local/score/score.js +182 -0
- package/dist/local/types.js +1 -0
- package/dist/local/util/hash.js +16 -0
- package/dist/local/util/ids.js +16 -0
- package/dist/local/util/progress.js +8 -0
- package/dist/local/util/redact.js +39 -0
- package/dist/local/util/time.js +1 -0
- package/dist/local/util/walk.js +174 -0
- package/dist/local/viz/behaviorReportData.js +367 -0
- package/dist/local/viz/behaviorReportHtml.js +664 -0
- package/dist/local/viz/d3.bundle.js +3 -0
- package/dist/local/viz/html.js +1152 -0
- package/dist/local/viz/payload.js +525 -0
- package/dist/local/workspace.js +99 -0
- package/docs/agent-workflow.md +167 -0
- package/docs/agents/claude-code.md +43 -0
- package/docs/agents/codex.md +52 -0
- package/docs/agents/cursor.md +39 -0
- package/docs/agents/opencode.md +43 -0
- package/docs/agents/vscode.md +34 -0
- package/docs/local-proof-kit.md +269 -0
- package/package.json +92 -0
- package/scripts/spikes/dynamic-proof-jest-reporter.cjs +66 -0
- package/scripts/spikes/dynamic-proof-mocha-reporter.cjs +105 -0
- package/scripts/spikes/dynamic-proof-spike.mjs +2335 -0
- package/scripts/spikes/dynamic-proof-vitest-reporter.mjs +81 -0
- package/scripts/spikes/failure-summary.mjs +29 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, resolve } from "node:path";
|
|
3
|
+
import { runConfirmer } from "../analyze/confirm.js";
|
|
4
|
+
import { isTestFile, languageOf } from "../analyze/classify.js";
|
|
5
|
+
import { classifyTestLayer } from "../analyze/testLayer.js";
|
|
6
|
+
import { extractTestNames } from "../analyze/symbols.js";
|
|
7
|
+
import { makeProofEdges, makeTestCaseNode } from "../graph/factories.js";
|
|
8
|
+
import { confirmedCoverageByLayer } from "../score/coverage.js";
|
|
9
|
+
import { hashString } from "../util/hash.js";
|
|
10
|
+
import { resolveContained, toWorkspaceRel } from "./paths.js";
|
|
11
|
+
const DETECTOR = "repo_analyzer";
|
|
12
|
+
export function tryScopedReprove(input) {
|
|
13
|
+
if (!input.testPath)
|
|
14
|
+
return null;
|
|
15
|
+
const parsed = parseSymId(input.targetSymbol);
|
|
16
|
+
if (!parsed)
|
|
17
|
+
return null;
|
|
18
|
+
const absRoot = resolve(input.root);
|
|
19
|
+
const testAbs = resolveContained(absRoot, input.testPath);
|
|
20
|
+
if (!existsSync(testAbs))
|
|
21
|
+
return null;
|
|
22
|
+
const testRel = toWorkspaceRel(absRoot, testAbs);
|
|
23
|
+
if (!isTestFile(testRel))
|
|
24
|
+
return null;
|
|
25
|
+
const testLang = languageOf(testRel);
|
|
26
|
+
if (testLang !== "typescript" && testLang !== "javascript")
|
|
27
|
+
return null;
|
|
28
|
+
if (!/\.([cm]?[tj]sx?)$/i.test(parsed.implRel))
|
|
29
|
+
return null;
|
|
30
|
+
const implAbs = resolveContained(absRoot, parsed.implRel);
|
|
31
|
+
if (!existsSync(implAbs))
|
|
32
|
+
return null;
|
|
33
|
+
const symbolsByImpl = eligibleSymbolsByImpl(input.graph);
|
|
34
|
+
const existingSymIds = new Set(input.graph.nodes.filter((n) => n.kind === "CodeSymbol").map((n) => n.external_id));
|
|
35
|
+
const candidate = { testRel, testAbs, implRel: parsed.implRel, implAbs };
|
|
36
|
+
let confirmations;
|
|
37
|
+
try {
|
|
38
|
+
confirmations = runConfirmer({ candidates: [candidate], symbolsByImpl, existingSymIds, anchorFile: absRoot }).confirmations;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const matched = confirmations.filter((c) => c.symId === input.targetSymbol);
|
|
44
|
+
if (matched.length === 0)
|
|
45
|
+
return null;
|
|
46
|
+
const next = patchGraph(input.graph, {
|
|
47
|
+
testRel,
|
|
48
|
+
testAbs,
|
|
49
|
+
targetSymbol: input.targetSymbol,
|
|
50
|
+
now: input.now
|
|
51
|
+
});
|
|
52
|
+
const newEdges = [`test:${testRel}->${input.targetSymbol}`].filter((edge) => !input.preEdges.includes(edge));
|
|
53
|
+
return newEdges.length > 0
|
|
54
|
+
? { graph: next, newEdges, reason: "Scoped confirmer produced a static COVERS edge; full re-analysis skipped." }
|
|
55
|
+
: null;
|
|
56
|
+
}
|
|
57
|
+
function patchGraph(graph, input) {
|
|
58
|
+
const content = readFileSync(input.testAbs, "utf8");
|
|
59
|
+
const testNames = extractTestNames(content);
|
|
60
|
+
const layer = classifyTestLayer(input.testRel, content);
|
|
61
|
+
const provenance = prov(graph, input.testRel, hashString(testNames.join("\n")));
|
|
62
|
+
const lastVerified = Date.parse(input.now) || Date.now();
|
|
63
|
+
const next = {
|
|
64
|
+
...graph,
|
|
65
|
+
updated_at: input.now,
|
|
66
|
+
nodes: [...graph.nodes],
|
|
67
|
+
edges: [...graph.edges],
|
|
68
|
+
...(graph.analysis ? { analysis: { ...graph.analysis } } : {})
|
|
69
|
+
};
|
|
70
|
+
const testExternalId = `test:${input.testRel}`;
|
|
71
|
+
if (!next.nodes.some((n) => n.external_id === testExternalId)) {
|
|
72
|
+
next.nodes.push(makeTestCaseNode({
|
|
73
|
+
testRel: input.testRel,
|
|
74
|
+
title: basename(input.testRel),
|
|
75
|
+
testLayer: layer.layer,
|
|
76
|
+
layerConfidence: layer.confidence,
|
|
77
|
+
layerSignals: layer.signals,
|
|
78
|
+
testNames,
|
|
79
|
+
provenance,
|
|
80
|
+
contentHash: hashString(content)
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
const edgeIds = new Set(next.edges.map((e) => e.id));
|
|
84
|
+
const proofEdges = makeProofEdges({
|
|
85
|
+
testRel: input.testRel,
|
|
86
|
+
symId: input.targetSymbol,
|
|
87
|
+
provenance: prov(graph, input.testRel, hashString(input.targetSymbol)),
|
|
88
|
+
lastVerified
|
|
89
|
+
});
|
|
90
|
+
for (const edge of proofEdges) {
|
|
91
|
+
if (!edgeIds.has(edge.id))
|
|
92
|
+
next.edges.push(edge);
|
|
93
|
+
}
|
|
94
|
+
if (next.analysis) {
|
|
95
|
+
next.analysis = {
|
|
96
|
+
...next.analysis,
|
|
97
|
+
confirmed_coverage: {
|
|
98
|
+
confirmed_pairs: (next.analysis.confirmed_coverage?.confirmed_pairs ?? 0) +
|
|
99
|
+
proofEdges.filter((e) => e.relationship_type === "COVERS").length,
|
|
100
|
+
attempted: next.analysis.confirmed_coverage?.attempted ?? 0,
|
|
101
|
+
capped_downgrades: next.analysis.confirmed_coverage?.capped_downgrades ?? 0,
|
|
102
|
+
skipped_files_budget: next.analysis.confirmed_coverage?.skipped_files_budget ?? 0
|
|
103
|
+
},
|
|
104
|
+
confirmed_by_layer: confirmedCoverageByLayer(next)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return next;
|
|
108
|
+
}
|
|
109
|
+
function eligibleSymbolsByImpl(graph) {
|
|
110
|
+
const out = new Map();
|
|
111
|
+
for (const node of graph.nodes) {
|
|
112
|
+
if (node.kind !== "CodeSymbol" || node.denominator_eligible !== true || node.properties.member_of)
|
|
113
|
+
continue;
|
|
114
|
+
const parsed = parseSymId(node.external_id);
|
|
115
|
+
if (!parsed)
|
|
116
|
+
continue;
|
|
117
|
+
const list = out.get(parsed.implRel);
|
|
118
|
+
if (list)
|
|
119
|
+
list.push(parsed.name);
|
|
120
|
+
else
|
|
121
|
+
out.set(parsed.implRel, [parsed.name]);
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
function parseSymId(symId) {
|
|
126
|
+
const m = symId.match(/^sym:(.+)#([^#]+)$/);
|
|
127
|
+
return m ? { implRel: m[1], name: m[2] } : null;
|
|
128
|
+
}
|
|
129
|
+
function prov(graph, ref, quote_hash) {
|
|
130
|
+
return {
|
|
131
|
+
source_scope_id: graph.sources[0]?.source_scope_id ?? graph.workspace.name,
|
|
132
|
+
source_ref: ref,
|
|
133
|
+
quote_hash,
|
|
134
|
+
detector: DETECTOR
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// Barrel walker (Gate 1 / G-RESOLVE, Phase 1 PR-2).
|
|
2
|
+
//
|
|
3
|
+
// Given a starting file and the name of a binding imported FROM it, follow the
|
|
4
|
+
// re-export chain (`export * from` / `export { x as y } from`) to the TERMINAL
|
|
5
|
+
// file that locally defines the binding. Only a terminal RUNTIME definition
|
|
6
|
+
// reached via RUNTIME hops is COVERS-eligible (`covered === true`); every
|
|
7
|
+
// dead-end (ambiguous star, depth/cycle exhaustion, unresolvable hop) AND every
|
|
8
|
+
// type-only outcome (a type terminal, or a hop through a `export type ... from`)
|
|
9
|
+
// is NOT covered — a false "confirmed" is worse than "inferred"
|
|
10
|
+
// (gate-specs-digest.md §resolver: type-only targets are NOT terminal impl).
|
|
11
|
+
//
|
|
12
|
+
// Resolution rules (TypeScript module semantics, parse-only):
|
|
13
|
+
// - Local definition -> terminal here. covered iff the binding kind is
|
|
14
|
+
// runtime (not "type") AND no type-only hop taken.
|
|
15
|
+
// - Explicit named re-export -> follow it; it SHADOWS star re-exports for that
|
|
16
|
+
// name (TS: explicit exports win over `export *`).
|
|
17
|
+
// A renamed alias (`export { x as y } from`) is
|
|
18
|
+
// deterministic and stays COVERS-eligible.
|
|
19
|
+
// - Single `export *` -> follow it with the SAME binding name.
|
|
20
|
+
// - 2+ `export *` -> probe each; exactly one supplying terminal is
|
|
21
|
+
// deterministic; two or more is AMBIGUOUS.
|
|
22
|
+
// - `default` via `export *` -> unresolved. TS/Node never forward a default
|
|
23
|
+
// through a star (`import d from barrel` where
|
|
24
|
+
// the barrel only `export *`s = "no default
|
|
25
|
+
// export"); only `export { x as default } from`
|
|
26
|
+
// (the named branch) forwards one.
|
|
27
|
+
// - Depth > maxDepth (5) -> depth-exceeded (downgrade).
|
|
28
|
+
// - Revisited (file, binding) -> cycle (downgrade) — barrels are often cyclic.
|
|
29
|
+
// A cycle is CONCLUSIVE for its own branch (ESM
|
|
30
|
+
// ResolveExport treats circular star paths as
|
|
31
|
+
// contributing nothing), so a sibling star may
|
|
32
|
+
// still supply the binding deterministically.
|
|
33
|
+
// - Total work > maxExpansions -> budget-exhausted (fail-closed; bounds a wide fan-out).
|
|
34
|
+
// - Hop resolves to external/null -> unresolved (downgrade).
|
|
35
|
+
// - In a 2+ star probe, a branch CUT mid-exploration (depth-exceeded or
|
|
36
|
+
// budget-exhausted) or an unexpandable star POISONS the whole collapse:
|
|
37
|
+
// we cannot prove the surviving terminal is unique, and a hidden second
|
|
38
|
+
// supplier would make the barrel ambiguous (TS2308 / ESM "conflicting star
|
|
39
|
+
// exports" = the binding is NOT delivered). Only conclusively-explored
|
|
40
|
+
// branches (unresolved = binding absent; cycle; deeper-ambiguous = excluded
|
|
41
|
+
// by ESM) may be dropped.
|
|
42
|
+
// - A `.d.ts` terminal is types-only at runtime -> terminal but NOT covered.
|
|
43
|
+
import path from "node:path";
|
|
44
|
+
import { resolveImport } from "./resolver.js";
|
|
45
|
+
import { buildExportIndex } from "./exportIndex.js";
|
|
46
|
+
const DEFAULT_MAX_DEPTH = 5;
|
|
47
|
+
const DEFAULT_MAX_EXPANSIONS = 4000;
|
|
48
|
+
/** Default resolver: only INTERNAL terminals are walkable (a hop into node_modules is a dead-end). */
|
|
49
|
+
function defaultResolve(specifier, containingFile) {
|
|
50
|
+
const r = resolveImport(specifier, containingFile);
|
|
51
|
+
if (!r.resolved || r.isExternal)
|
|
52
|
+
return null;
|
|
53
|
+
return r.resolvedFileName;
|
|
54
|
+
}
|
|
55
|
+
/** Kinds that denote a concrete RUNTIME implementation binding (COVERS-eligible). */
|
|
56
|
+
function isRuntimeKind(kind) {
|
|
57
|
+
return kind === "function" || kind === "class" || kind === "const" || kind === "default" || kind === "namespace";
|
|
58
|
+
}
|
|
59
|
+
function fail(status, depth) {
|
|
60
|
+
return { status, terminalFile: null, terminalBinding: null, terminalKind: null, depth, covered: false };
|
|
61
|
+
}
|
|
62
|
+
function terminalKey(r) {
|
|
63
|
+
return `${r.terminalFile}::${r.terminalBinding}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Follow the re-export chain from `file` for the binding `binding` to its
|
|
67
|
+
* terminal defining file. See module docs for the resolution rules.
|
|
68
|
+
*/
|
|
69
|
+
export function walkBarrel(file, binding, opts = {}) {
|
|
70
|
+
const maxDepth = opts.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
71
|
+
const resolve = opts.resolve ?? defaultResolve;
|
|
72
|
+
const loadIndex = opts.loadIndex ?? buildExportIndex;
|
|
73
|
+
const budget = { n: 0, max: opts.maxExpansions ?? DEFAULT_MAX_EXPANSIONS };
|
|
74
|
+
return walkRec(file, binding, 0, new Set(), false, maxDepth, resolve, loadIndex, budget);
|
|
75
|
+
}
|
|
76
|
+
function walkRec(currentFile, binding, depth, visited, typeTainted, maxDepth, resolve, loadIndex, budget) {
|
|
77
|
+
// Total-work budget (shared across all sibling star branches): a wide fan-out
|
|
78
|
+
// can branch combinatorially within the depth bound, so cap total nodes
|
|
79
|
+
// visited and downgrade fail-closed (never a false terminal) on exhaustion.
|
|
80
|
+
// The status is DISTINCT from "unresolved" so the multi-star collapse can tell
|
|
81
|
+
// a cut branch apart from a conclusively-absent binding (cuts must poison).
|
|
82
|
+
if (++budget.n > budget.max)
|
|
83
|
+
return fail("budget-exhausted", depth);
|
|
84
|
+
if (depth > maxDepth)
|
|
85
|
+
return fail("depth-exceeded", depth);
|
|
86
|
+
// Cycle detection is along THIS path: a per-call copy lets sibling star
|
|
87
|
+
// branches explore independently while still catching a loop back to a
|
|
88
|
+
// (file, binding) pair already open on the current chain.
|
|
89
|
+
const key = `${path.resolve(currentFile)}::${binding}`;
|
|
90
|
+
if (visited.has(key))
|
|
91
|
+
return fail("cycle", depth);
|
|
92
|
+
const pathVisited = new Set(visited);
|
|
93
|
+
pathVisited.add(key);
|
|
94
|
+
const index = loadIndex(currentFile);
|
|
95
|
+
// 1. Defined locally -> terminal. Covered only for a RUNTIME kind reached
|
|
96
|
+
// without a type-only hop, and never in a declaration file (a `.d.ts`
|
|
97
|
+
// "export declare function" is types-only at runtime — not terminal impl).
|
|
98
|
+
if (index.local.has(binding)) {
|
|
99
|
+
const kind = index.local.get(binding) ?? "unknown";
|
|
100
|
+
const isDeclarationFile = /\.d\.[mc]?ts$/i.test(index.file);
|
|
101
|
+
return {
|
|
102
|
+
status: "terminal",
|
|
103
|
+
terminalFile: index.file,
|
|
104
|
+
terminalBinding: binding,
|
|
105
|
+
terminalKind: kind,
|
|
106
|
+
depth,
|
|
107
|
+
covered: !typeTainted && !isDeclarationFile && isRuntimeKind(kind)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// 2. Explicit named re-exports shadow star re-exports for this name.
|
|
111
|
+
const named = [];
|
|
112
|
+
for (const re of index.reexports) {
|
|
113
|
+
if (re.kind === "named") {
|
|
114
|
+
for (const n of re.names) {
|
|
115
|
+
if (n.exported === binding)
|
|
116
|
+
named.push({ specifier: re.specifier, source: n.source, typeOnly: n.isTypeOnly });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (named.length >= 2)
|
|
121
|
+
return fail("ambiguous", depth); // duplicate explicit re-export
|
|
122
|
+
if (named.length === 1) {
|
|
123
|
+
const target = resolve(named[0].specifier, currentFile);
|
|
124
|
+
if (!target)
|
|
125
|
+
return fail("unresolved", depth);
|
|
126
|
+
return walkRec(target, named[0].source, depth + 1, pathVisited, typeTainted || named[0].typeOnly, maxDepth, resolve, loadIndex, budget);
|
|
127
|
+
}
|
|
128
|
+
// A `default` cannot be supplied by `export *` (TS/Node never forward a default
|
|
129
|
+
// through a star). Having exhausted explicit named re-exports above, a `default`
|
|
130
|
+
// request that reaches the star section can never be satisfied — downgrade
|
|
131
|
+
// instead of false-confirming a star target's OWN local default.
|
|
132
|
+
if (binding === "default")
|
|
133
|
+
return fail("unresolved", depth);
|
|
134
|
+
// 3. Star re-exports are the only remaining source.
|
|
135
|
+
const stars = index.reexports.filter((re) => re.kind === "star");
|
|
136
|
+
if (stars.length === 0)
|
|
137
|
+
return fail("unresolved", depth);
|
|
138
|
+
if (stars.length === 1) {
|
|
139
|
+
const target = resolve(stars[0].specifier, currentFile);
|
|
140
|
+
if (!target)
|
|
141
|
+
return fail("unresolved", depth);
|
|
142
|
+
return walkRec(target, binding, depth + 1, pathVisited, typeTainted || stars[0].isTypeOnly, maxDepth, resolve, loadIndex, budget);
|
|
143
|
+
}
|
|
144
|
+
// 4. 2+ stars: probe each branch independently. Exactly one terminal is
|
|
145
|
+
// deterministic; two or more DISTINCT terminals is ambiguous. A branch is
|
|
146
|
+
// droppable ONLY when conclusively explored: "unresolved" (binding absent),
|
|
147
|
+
// "cycle" (ESM circular star paths contribute nothing), or "ambiguous"
|
|
148
|
+
// deeper down (ESM excludes ambiguous star exports from the namespace). A
|
|
149
|
+
// CUT branch (depth-exceeded / budget-exhausted) or an unexpandable star
|
|
150
|
+
// POISONS the collapse: a hidden second supplier would make this barrel
|
|
151
|
+
// ambiguous (TS2308), so a surviving lone terminal cannot be trusted.
|
|
152
|
+
const hits = [];
|
|
153
|
+
for (const star of stars) {
|
|
154
|
+
const target = resolve(star.specifier, currentFile);
|
|
155
|
+
if (!target)
|
|
156
|
+
return fail("unresolved", depth); // a star we cannot expand -> cannot prove uniqueness
|
|
157
|
+
const sub = walkRec(target, binding, depth + 1, new Set(pathVisited), typeTainted || star.isTypeOnly, maxDepth, resolve, loadIndex, budget);
|
|
158
|
+
if (sub.status === "terminal")
|
|
159
|
+
hits.push(sub);
|
|
160
|
+
else if (sub.status === "depth-exceeded" || sub.status === "budget-exhausted")
|
|
161
|
+
return fail(sub.status, depth);
|
|
162
|
+
}
|
|
163
|
+
const distinct = new Map();
|
|
164
|
+
for (const h of hits) {
|
|
165
|
+
const k = terminalKey(h);
|
|
166
|
+
const prev = distinct.get(k);
|
|
167
|
+
// Same terminal reached via multiple stars: keep the COVERED outcome so the
|
|
168
|
+
// result is independent of star declaration order (a runtime star and a
|
|
169
|
+
// type-only star reaching the same def must deterministically be covered).
|
|
170
|
+
if (!prev || (!prev.covered && h.covered))
|
|
171
|
+
distinct.set(k, h);
|
|
172
|
+
}
|
|
173
|
+
if (distinct.size === 1)
|
|
174
|
+
return [...distinct.values()][0];
|
|
175
|
+
if (distinct.size >= 2)
|
|
176
|
+
return fail("ambiguous", depth);
|
|
177
|
+
return fail("unresolved", depth);
|
|
178
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// Per-file export index (Gate 1 / G-RESOLVE, Phase 1 PR-2).
|
|
2
|
+
//
|
|
3
|
+
// For one file, list (a) the bindings it EXPORTS whose terminal definition is
|
|
4
|
+
// the file itself, and (b) its re-export directives (`export ... from`). The
|
|
5
|
+
// barrel walker (barrelWalker.ts) consumes this to follow re-export chains to a
|
|
6
|
+
// terminal defining file.
|
|
7
|
+
//
|
|
8
|
+
// PARSE-ONLY: a single `ts.createSourceFile` pass, NO `Program`/`TypeChecker`.
|
|
9
|
+
// The confirmed-safety spike proved this parse-only path is false-confirm-safe
|
|
10
|
+
// (see private/spikes/confirmed). A re-export of an IMPORTED binding
|
|
11
|
+
// (`import { x } from "./m"; export { x }`) is synthesized into a named
|
|
12
|
+
// re-export so the walker follows it to the real definition rather than stopping
|
|
13
|
+
// at the re-exporting file. Type-only-ness is tracked per re-exported name and
|
|
14
|
+
// per local binding (kind "type") so the walker never treats a type as a
|
|
15
|
+
// COVERS-eligible runtime terminal (gate-specs-digest.md §resolver: type-only
|
|
16
|
+
// targets are NOT terminal impl).
|
|
17
|
+
import ts from "typescript";
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
const EMPTY_INDEX = (file) => ({
|
|
21
|
+
file,
|
|
22
|
+
local: new Map(),
|
|
23
|
+
reexports: [],
|
|
24
|
+
isBarrel: false
|
|
25
|
+
});
|
|
26
|
+
// Cache by `absPath + ":" + mtimeMs` so an edited file busts its own entry; a
|
|
27
|
+
// long-lived MCP process never serves a stale index after edits. Mirrors the
|
|
28
|
+
// resolver's scope cache.
|
|
29
|
+
const indexCache = new Map();
|
|
30
|
+
function cacheKey(file) {
|
|
31
|
+
let mtimeMs = 0;
|
|
32
|
+
try {
|
|
33
|
+
mtimeMs = fs.statSync(file).mtimeMs;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* unreadable file — key on mtime 0 */
|
|
37
|
+
}
|
|
38
|
+
return `${path.resolve(file)}:${mtimeMs}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build (and memoize) the export index for `file`. Returns an empty index for
|
|
42
|
+
* an unreadable file rather than throwing.
|
|
43
|
+
*/
|
|
44
|
+
export function buildExportIndex(file) {
|
|
45
|
+
const key = cacheKey(file);
|
|
46
|
+
const cached = indexCache.get(key);
|
|
47
|
+
if (cached)
|
|
48
|
+
return cached;
|
|
49
|
+
const index = computeExportIndex(file);
|
|
50
|
+
indexCache.set(key, index);
|
|
51
|
+
return index;
|
|
52
|
+
}
|
|
53
|
+
/** Clear the process-level export-index cache. Call at the START of an analyze run. */
|
|
54
|
+
export function resetExportIndexCache() {
|
|
55
|
+
indexCache.clear();
|
|
56
|
+
}
|
|
57
|
+
function modifierFlags(node) {
|
|
58
|
+
return ts.canHaveModifiers(node) ? ts.getCombinedModifierFlags(node) : ts.ModifierFlags.None;
|
|
59
|
+
}
|
|
60
|
+
/** Collect identifier names from a binding name (identifier or destructuring pattern). */
|
|
61
|
+
function bindingNames(name, out) {
|
|
62
|
+
if (ts.isIdentifier(name)) {
|
|
63
|
+
out.push(name.text);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
for (const el of name.elements) {
|
|
67
|
+
if (ts.isBindingElement(el))
|
|
68
|
+
bindingNames(el.name, out);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function moduleText(node) {
|
|
72
|
+
return node && ts.isStringLiteralLike(node) ? node.text : null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Kind for a `namespace N { ... }`: "namespace" (runtime object) only when it is
|
|
76
|
+
* INSTANTIATED. TS erases a namespace whose body holds only interfaces, type
|
|
77
|
+
* aliases, and uninstantiated nested namespaces — and any ambient (`declare`)
|
|
78
|
+
* namespace — so those must be kind "type" (never COVERS-eligible).
|
|
79
|
+
*/
|
|
80
|
+
function namespaceKind(node) {
|
|
81
|
+
if (modifierFlags(node) & ts.ModifierFlags.Ambient)
|
|
82
|
+
return "type"; // `declare namespace` is erased
|
|
83
|
+
return isInstantiatedNamespace(node) ? "namespace" : "type";
|
|
84
|
+
}
|
|
85
|
+
function isInstantiatedNamespace(node) {
|
|
86
|
+
const body = node.body;
|
|
87
|
+
if (!body)
|
|
88
|
+
return false;
|
|
89
|
+
if (ts.isModuleDeclaration(body))
|
|
90
|
+
return isInstantiatedNamespace(body); // `namespace A.B { ... }`
|
|
91
|
+
if (!ts.isModuleBlock(body))
|
|
92
|
+
return false;
|
|
93
|
+
return body.statements.some((s) => {
|
|
94
|
+
if (ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s))
|
|
95
|
+
return false;
|
|
96
|
+
if (ts.isModuleDeclaration(s))
|
|
97
|
+
return isInstantiatedNamespace(s);
|
|
98
|
+
return true; // any value declaration or executable statement instantiates
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function computeExportIndex(file) {
|
|
102
|
+
let text;
|
|
103
|
+
try {
|
|
104
|
+
text = fs.readFileSync(file, "utf8");
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return EMPTY_INDEX(path.resolve(file));
|
|
108
|
+
}
|
|
109
|
+
const abs = path.resolve(file);
|
|
110
|
+
const sourceFile = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, /*setParentNodes*/ false);
|
|
111
|
+
// Pass 1: top-level declarations (name -> kind, whether exported or not) and
|
|
112
|
+
// import bindings — both needed to resolve `export { x }` (no `from`).
|
|
113
|
+
const declared = new Map();
|
|
114
|
+
const localImports = new Map();
|
|
115
|
+
for (const stmt of sourceFile.statements) {
|
|
116
|
+
collectDeclarations(stmt, declared);
|
|
117
|
+
collectImports(stmt, localImports);
|
|
118
|
+
}
|
|
119
|
+
// Pass 2: exports + re-exports.
|
|
120
|
+
const local = new Map();
|
|
121
|
+
const reexports = [];
|
|
122
|
+
for (const stmt of sourceFile.statements) {
|
|
123
|
+
collectExports(stmt, declared, localImports, local, reexports);
|
|
124
|
+
}
|
|
125
|
+
return { file: abs, local, reexports, isBarrel: reexports.length > 0 };
|
|
126
|
+
}
|
|
127
|
+
function collectDeclarations(node, declared) {
|
|
128
|
+
// An ambient declaration (`declare function` etc., or anything in a .d.ts) is
|
|
129
|
+
// erased at emit — its binding is types-only for coverage purposes.
|
|
130
|
+
const ambient = (modifierFlags(node) & ts.ModifierFlags.Ambient) !== 0;
|
|
131
|
+
const runtime = (kind) => (ambient ? "type" : kind);
|
|
132
|
+
if (ts.isFunctionDeclaration(node) && node.name)
|
|
133
|
+
declared.set(node.name.text, runtime("function"));
|
|
134
|
+
else if (ts.isClassDeclaration(node) && node.name)
|
|
135
|
+
declared.set(node.name.text, runtime("class"));
|
|
136
|
+
else if (ts.isEnumDeclaration(node))
|
|
137
|
+
declared.set(node.name.text, runtime("const"));
|
|
138
|
+
else if (ts.isInterfaceDeclaration(node))
|
|
139
|
+
declared.set(node.name.text, "type");
|
|
140
|
+
else if (ts.isTypeAliasDeclaration(node))
|
|
141
|
+
declared.set(node.name.text, "type");
|
|
142
|
+
else if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name))
|
|
143
|
+
declared.set(node.name.text, namespaceKind(node));
|
|
144
|
+
else if (ts.isVariableStatement(node)) {
|
|
145
|
+
for (const decl of node.declarationList.declarations) {
|
|
146
|
+
const names = [];
|
|
147
|
+
bindingNames(decl.name, names);
|
|
148
|
+
for (const n of names)
|
|
149
|
+
declared.set(n, runtime("const"));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function collectImports(node, localImports) {
|
|
154
|
+
if (!ts.isImportDeclaration(node))
|
|
155
|
+
return;
|
|
156
|
+
const specifier = moduleText(node.moduleSpecifier);
|
|
157
|
+
if (specifier === null)
|
|
158
|
+
return;
|
|
159
|
+
const clause = node.importClause;
|
|
160
|
+
if (!clause)
|
|
161
|
+
return; // side-effect import binds nothing
|
|
162
|
+
const clauseTypeOnly = clause.isTypeOnly; // `import type ...`
|
|
163
|
+
if (clause.name)
|
|
164
|
+
localImports.set(clause.name.text, { specifier, source: "default", typeOnly: clauseTypeOnly });
|
|
165
|
+
const named = clause.namedBindings;
|
|
166
|
+
if (named && ts.isNamespaceImport(named)) {
|
|
167
|
+
localImports.set(named.name.text, { specifier, source: "*", typeOnly: clauseTypeOnly });
|
|
168
|
+
}
|
|
169
|
+
else if (named && ts.isNamedImports(named)) {
|
|
170
|
+
for (const el of named.elements) {
|
|
171
|
+
localImports.set(el.name.text, {
|
|
172
|
+
specifier,
|
|
173
|
+
source: (el.propertyName ?? el.name).text,
|
|
174
|
+
typeOnly: clauseTypeOnly || el.isTypeOnly // `import { type x }`
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function collectExports(node, declared, localImports, local, reexports) {
|
|
180
|
+
const flags = modifierFlags(node);
|
|
181
|
+
const isExported = (flags & ts.ModifierFlags.Export) !== 0;
|
|
182
|
+
const isDefault = (flags & ts.ModifierFlags.Default) !== 0;
|
|
183
|
+
// `export default function/class ...` exposes the binding as "default".
|
|
184
|
+
if (isExported && isDefault) {
|
|
185
|
+
local.set("default", "default");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (isExported) {
|
|
189
|
+
// `export declare ...` is ambient: erased at emit, so types-only for coverage.
|
|
190
|
+
const ambient = (flags & ts.ModifierFlags.Ambient) !== 0;
|
|
191
|
+
const runtime = (kind) => (ambient ? "type" : kind);
|
|
192
|
+
if (ts.isFunctionDeclaration(node) && node.name)
|
|
193
|
+
local.set(node.name.text, runtime("function"));
|
|
194
|
+
else if (ts.isClassDeclaration(node) && node.name)
|
|
195
|
+
local.set(node.name.text, runtime("class"));
|
|
196
|
+
else if (ts.isEnumDeclaration(node))
|
|
197
|
+
local.set(node.name.text, runtime("const"));
|
|
198
|
+
else if (ts.isInterfaceDeclaration(node))
|
|
199
|
+
local.set(node.name.text, "type");
|
|
200
|
+
else if (ts.isTypeAliasDeclaration(node))
|
|
201
|
+
local.set(node.name.text, "type");
|
|
202
|
+
else if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name))
|
|
203
|
+
local.set(node.name.text, namespaceKind(node));
|
|
204
|
+
else if (ts.isVariableStatement(node)) {
|
|
205
|
+
for (const decl of node.declarationList.declarations) {
|
|
206
|
+
const names = [];
|
|
207
|
+
bindingNames(decl.name, names);
|
|
208
|
+
for (const n of names)
|
|
209
|
+
local.set(n, runtime("const"));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// `export default <expr>` (ExportAssignment, not `export =`).
|
|
215
|
+
if (ts.isExportAssignment(node) && !node.isExportEquals) {
|
|
216
|
+
local.set("default", "default");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// `export = expr` (CommonJS) is intentionally NOT indexed (rare in modern TS-ESM).
|
|
220
|
+
if (!ts.isExportDeclaration(node))
|
|
221
|
+
return;
|
|
222
|
+
// `export * [as ns] from "./m"` / `export { ... } from "./m"`.
|
|
223
|
+
if (node.moduleSpecifier) {
|
|
224
|
+
const specifier = moduleText(node.moduleSpecifier);
|
|
225
|
+
if (specifier === null)
|
|
226
|
+
return;
|
|
227
|
+
const isTypeOnly = node.isTypeOnly;
|
|
228
|
+
const clause = node.exportClause;
|
|
229
|
+
if (!clause) {
|
|
230
|
+
reexports.push({ kind: "star", specifier, isTypeOnly });
|
|
231
|
+
}
|
|
232
|
+
else if (ts.isNamespaceExport(clause)) {
|
|
233
|
+
// `export * as ns from "./m"` binds the namespace object HERE.
|
|
234
|
+
local.set(clause.name.text, isTypeOnly ? "type" : "namespace");
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
const names = clause.elements.map((el) => ({
|
|
238
|
+
source: (el.propertyName ?? el.name).text,
|
|
239
|
+
exported: el.name.text,
|
|
240
|
+
isTypeOnly: isTypeOnly || el.isTypeOnly
|
|
241
|
+
}));
|
|
242
|
+
reexports.push({ kind: "named", specifier, isTypeOnly, names });
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
// `export { a, b as c }` (no `from`): re-export of an imported binding (follow
|
|
247
|
+
// it) OR a local/ambient binding (terminal here).
|
|
248
|
+
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
249
|
+
for (const el of node.exportClause.elements) {
|
|
250
|
+
const exported = el.name.text;
|
|
251
|
+
const localName = (el.propertyName ?? el.name).text;
|
|
252
|
+
const elementTypeOnly = node.isTypeOnly || el.isTypeOnly;
|
|
253
|
+
const imported = localImports.get(localName);
|
|
254
|
+
if (imported) {
|
|
255
|
+
// A re-export of an imported binding is type-only if the EXPORT marks it
|
|
256
|
+
// type-only OR the original IMPORT was type-only (`import type {x}; export {x}`
|
|
257
|
+
// re-exports a type, never a runtime value — must not become COVERS-eligible).
|
|
258
|
+
reexports.push({
|
|
259
|
+
kind: "named",
|
|
260
|
+
specifier: imported.specifier,
|
|
261
|
+
isTypeOnly: node.isTypeOnly || imported.typeOnly,
|
|
262
|
+
names: [{ source: imported.source, exported, isTypeOnly: elementTypeOnly || imported.typeOnly }]
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
local.set(exported, elementTypeOnly ? "type" : declared.get(localName) ?? "unknown");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|