@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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -0
  3. package/dist/local/agentWorkflow.js +81 -0
  4. package/dist/local/aiGraph/links.js +635 -0
  5. package/dist/local/analyze/analyzer.js +2129 -0
  6. package/dist/local/analyze/behaviorContracts.js +169 -0
  7. package/dist/local/analyze/boilerplate.js +42 -0
  8. package/dist/local/analyze/callGraph.js +458 -0
  9. package/dist/local/analyze/classify.js +219 -0
  10. package/dist/local/analyze/clustering.js +357 -0
  11. package/dist/local/analyze/confirm.js +2422 -0
  12. package/dist/local/analyze/coverage.js +518 -0
  13. package/dist/local/analyze/coverageArtifacts.js +607 -0
  14. package/dist/local/analyze/frameworks.js +115 -0
  15. package/dist/local/analyze/linkage/conventions.js +160 -0
  16. package/dist/local/analyze/parseCache.js +164 -0
  17. package/dist/local/analyze/selfAssert.js +53 -0
  18. package/dist/local/analyze/symbols.js +430 -0
  19. package/dist/local/analyze/testLayer.js +135 -0
  20. package/dist/local/analyze/treeSitter/engine.js +1253 -0
  21. package/dist/local/analyze/treeSitter/languages.js +101 -0
  22. package/dist/local/autoProve.js +620 -0
  23. package/dist/local/cli.js +1468 -0
  24. package/dist/local/cliArgs.js +112 -0
  25. package/dist/local/corpusScope.js +162 -0
  26. package/dist/local/enrich/csv.js +348 -0
  27. package/dist/local/enrich/index.js +43 -0
  28. package/dist/local/enrich/markdown.js +193 -0
  29. package/dist/local/explain/explain.js +91 -0
  30. package/dist/local/exportCli.js +26 -0
  31. package/dist/local/flows/flowWalker.js +215 -0
  32. package/dist/local/flows/llmFlowDiscovery.js +567 -0
  33. package/dist/local/freshness/changed.js +280 -0
  34. package/dist/local/freshness/manifest.js +35 -0
  35. package/dist/local/freshness/status.js +30 -0
  36. package/dist/local/gaps/gaps.js +114 -0
  37. package/dist/local/generate/buckets.js +73 -0
  38. package/dist/local/generate/compareJudge.js +124 -0
  39. package/dist/local/generate/compareReport.js +538 -0
  40. package/dist/local/generate/compareScore.js +105 -0
  41. package/dist/local/generate/deriveImports.js +91 -0
  42. package/dist/local/generate/generator.js +2586 -0
  43. package/dist/local/generate/prompt.js +144 -0
  44. package/dist/local/generate/promptV5.js +438 -0
  45. package/dist/local/generate/providers.js +400 -0
  46. package/dist/local/generate/runHints.js +304 -0
  47. package/dist/local/graph/citations.js +73 -0
  48. package/dist/local/graph/confirmable.js +72 -0
  49. package/dist/local/graph/factories.js +210 -0
  50. package/dist/local/graph/ontology.js +18 -0
  51. package/dist/local/interactive.js +53 -0
  52. package/dist/local/jobs/jobStore.js +80 -0
  53. package/dist/local/jobs/notify.js +29 -0
  54. package/dist/local/jobs/runner.js +75 -0
  55. package/dist/local/ledger.js +117 -0
  56. package/dist/local/localConfig.js +112 -0
  57. package/dist/local/mcp.js +548 -0
  58. package/dist/local/operations.js +1749 -0
  59. package/dist/local/pack/coverageReport.js +192 -0
  60. package/dist/local/pack/exporter.js +195 -0
  61. package/dist/local/pack/schema.js +128 -0
  62. package/dist/local/pack/summary.js +127 -0
  63. package/dist/local/pack/validate.js +25 -0
  64. package/dist/local/proofRunnability.js +366 -0
  65. package/dist/local/recipe/dbSqljs.js +255 -0
  66. package/dist/local/reprove/paths.js +13 -0
  67. package/dist/local/reprove/scoped.js +136 -0
  68. package/dist/local/resolve/barrelWalker.js +178 -0
  69. package/dist/local/resolve/exportIndex.js +270 -0
  70. package/dist/local/resolve/importGraph.js +347 -0
  71. package/dist/local/resolve/resolver.js +122 -0
  72. package/dist/local/resolve/resolverCache.js +117 -0
  73. package/dist/local/rtm.js +413 -0
  74. package/dist/local/score/coverage.js +99 -0
  75. package/dist/local/score/doctor.js +67 -0
  76. package/dist/local/score/risk.js +362 -0
  77. package/dist/local/score/score.js +182 -0
  78. package/dist/local/types.js +1 -0
  79. package/dist/local/util/hash.js +16 -0
  80. package/dist/local/util/ids.js +16 -0
  81. package/dist/local/util/progress.js +8 -0
  82. package/dist/local/util/redact.js +39 -0
  83. package/dist/local/util/time.js +1 -0
  84. package/dist/local/util/walk.js +174 -0
  85. package/dist/local/viz/behaviorReportData.js +367 -0
  86. package/dist/local/viz/behaviorReportHtml.js +664 -0
  87. package/dist/local/viz/d3.bundle.js +3 -0
  88. package/dist/local/viz/html.js +1152 -0
  89. package/dist/local/viz/payload.js +525 -0
  90. package/dist/local/workspace.js +99 -0
  91. package/docs/agent-workflow.md +167 -0
  92. package/docs/agents/claude-code.md +43 -0
  93. package/docs/agents/codex.md +52 -0
  94. package/docs/agents/cursor.md +39 -0
  95. package/docs/agents/opencode.md +43 -0
  96. package/docs/agents/vscode.md +34 -0
  97. package/docs/local-proof-kit.md +269 -0
  98. package/package.json +92 -0
  99. package/scripts/spikes/dynamic-proof-jest-reporter.cjs +66 -0
  100. package/scripts/spikes/dynamic-proof-mocha-reporter.cjs +105 -0
  101. package/scripts/spikes/dynamic-proof-spike.mjs +2335 -0
  102. package/scripts/spikes/dynamic-proof-vitest-reporter.mjs +81 -0
  103. 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
+ }