@luckychen1993/pm-mcp 0.1.1

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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +304 -0
  3. package/dist/acceptance-evaluator.js +330 -0
  4. package/dist/acceptance-model.js +440 -0
  5. package/dist/acceptance-report.js +335 -0
  6. package/dist/acceptance-tools.js +318 -0
  7. package/dist/audit-tools.js +61 -0
  8. package/dist/audit.js +364 -0
  9. package/dist/budget.js +71 -0
  10. package/dist/dashboard.js +241 -0
  11. package/dist/governance-audit.js +126 -0
  12. package/dist/governance-model.js +306 -0
  13. package/dist/governance-tools.js +240 -0
  14. package/dist/health.js +58 -0
  15. package/dist/index-db.js +97 -0
  16. package/dist/index-store.js +381 -0
  17. package/dist/index.js +210 -0
  18. package/dist/init.js +40 -0
  19. package/dist/knowledge-tools.js +204 -0
  20. package/dist/language-adapters.js +450 -0
  21. package/dist/language-dependencies.js +351 -0
  22. package/dist/license.js +261 -0
  23. package/dist/paths.js +51 -0
  24. package/dist/polyglot-ast.js +329 -0
  25. package/dist/portfolio.js +349 -0
  26. package/dist/project-fingerprint.js +83 -0
  27. package/dist/project-tools.js +217 -0
  28. package/dist/quality-store.js +77 -0
  29. package/dist/registry.js +38 -0
  30. package/dist/roadmap.js +96 -0
  31. package/dist/scan.js +271 -0
  32. package/dist/search.js +312 -0
  33. package/dist/security.js +269 -0
  34. package/dist/semantic-evidence-store.js +133 -0
  35. package/dist/semantic-evidence.js +100 -0
  36. package/dist/semantic-graph-algorithms.js +86 -0
  37. package/dist/semantic-graph.js +390 -0
  38. package/dist/semantic-parsers.js +311 -0
  39. package/dist/store.js +185 -0
  40. package/dist/task-tools.js +198 -0
  41. package/dist/tool-base.js +32 -0
  42. package/dist/tools.js +21 -0
  43. package/dist/types.js +208 -0
  44. package/dist/typescript-semantic.js +282 -0
  45. package/package.json +70 -0
@@ -0,0 +1,390 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { isTestFile, listFiles } from "./scan.js";
4
+ import { SOURCE_LANGUAGE, UNKNOWN_SOURCE_EXTS, candidates, contractKind, normalizedRoot, analyzeSource, relPath, } from "./semantic-parsers.js";
5
+ import { SemanticEvidenceDocumentSchema, assuranceRank, verifySemanticEvidence, } from "./semantic-evidence.js";
6
+ import { computeImpactAnalysis, findModuleCycles } from "./semantic-graph-algorithms.js";
7
+ /** Assign by the longest matching module root; ties are deterministic by module id. */
8
+ export function moduleForFile(rel, modules) {
9
+ const target = relPath(rel);
10
+ const matches = modules.flatMap((module) => module.roots.map((root) => ({ module: module.id, root: normalizedRoot(root) })))
11
+ .filter(({ root }) => root === "" || target === root || target.startsWith(`${root}/`))
12
+ .sort((a, b) => b.root.length - a.root.length || a.module.localeCompare(b.module));
13
+ return matches[0]?.module ?? null;
14
+ }
15
+ function moduleFromSpecifier(specifier, modules) {
16
+ const value = specifier.toLowerCase().replace(/\\|::|\./g, "/");
17
+ const ranked = modules.flatMap((module) => [module.id, module.name ?? "", ...module.roots]
18
+ .filter(Boolean).map((token) => ({ id: module.id, token: normalizedRoot(token).toLowerCase() })))
19
+ .filter(({ token }) => token && (value === token || value.startsWith(`${token}/`) || value.endsWith(`/${token}`) || value.includes(`/${token}/`)))
20
+ .sort((a, b) => b.token.length - a.token.length || a.id.localeCompare(b.id));
21
+ return ranked[0]?.id ?? null;
22
+ }
23
+ function analyzerMeetsPolicy(analyzers, minimum) {
24
+ const hasAst = analyzers.some((analyzer) => analyzer.assurance === "ast");
25
+ const hasRuntime = analyzers.some((analyzer) => analyzer.assurance === "runtime");
26
+ if (minimum === "runtime")
27
+ return hasAst && hasRuntime;
28
+ if (minimum === "ast")
29
+ return hasAst;
30
+ return analyzers.length > 0;
31
+ }
32
+ function emptySemanticFile(rel, module, owners, language, parser, status, diagnostic) {
33
+ return { path: rel, module, owners, language, parser, confidence: 0, status, analyzers: [], diagnostics: diagnostic ? [diagnostic] : [], exports: [] };
34
+ }
35
+ function referencesFromEvidence(document) {
36
+ return document.references.map((reference, index) => ({
37
+ kind: reference.kind,
38
+ specifier: reference.specifier,
39
+ line: reference.line,
40
+ parser: document.analyzer.id,
41
+ confidence: reference.confidence,
42
+ forceLocal: reference.force_local,
43
+ offset: index,
44
+ symbol: reference.symbol,
45
+ }));
46
+ }
47
+ function exportsFromEvidence(document) {
48
+ return document.exports.map((item) => ({
49
+ name: item.name,
50
+ line: item.line,
51
+ isTypeOnly: item.is_type_only,
52
+ sourceSpecifier: item.source_specifier,
53
+ parser: document.analyzer.id,
54
+ confidence: item.confidence,
55
+ }));
56
+ }
57
+ export function buildSemanticGraph(root, governance, options = {}) {
58
+ const abs = path.resolve(root);
59
+ const modules = governance.modules;
60
+ const moduleMap = new Map(modules.map((m) => [m.id, m]));
61
+ const requested = options.files?.map(relPath) ?? listFiles(abs, { content: false }).map((f) => f.rel);
62
+ const paths = [...new Set(requested)]
63
+ .filter((p) => p && p !== ".." && !p.startsWith("../"))
64
+ .filter((p) => options.includeTests === true || !isTestFile(p))
65
+ .sort();
66
+ const maxBytes = options.maxFileBytes ?? 2 * 1024 * 1024;
67
+ const minimumAssurance = options.minimumSemanticAssurance ?? governance.policies?.minimum_semantic_assurance ?? "heuristic";
68
+ const failOnFallback = options.failOnSemanticFallback ?? governance.policies?.fail_on_semantic_fallback ?? false;
69
+ const evidenceByFile = new Map();
70
+ const evidenceErrors = [];
71
+ for (const [index, input] of (options.semanticEvidence ?? []).entries()) {
72
+ const parsed = SemanticEvidenceDocumentSchema.safeParse(input);
73
+ if (!parsed.success) {
74
+ evidenceErrors.push(`evidence[${index}] invalid: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "document"} ${issue.message}`).join("; ")}`);
75
+ continue;
76
+ }
77
+ const evidenceFile = relPath(parsed.data.file);
78
+ if (!evidenceFile || evidenceFile === ".." || evidenceFile.startsWith("../") || path.posix.isAbsolute(evidenceFile)) {
79
+ evidenceErrors.push(`evidence[${index}] unsafe file path: ${parsed.data.file}`);
80
+ continue;
81
+ }
82
+ evidenceByFile.set(evidenceFile, [...(evidenceByFile.get(evidenceFile) ?? []), parsed.data]);
83
+ }
84
+ const files = [];
85
+ const rawByFile = new Map();
86
+ const contracts = [];
87
+ const semanticViolations = evidenceErrors.map((error) => ({ type: "semantic-evidence-invalid", from: "evidence", to: "schema", evidence: [error] }));
88
+ const analysisLimitations = new Set();
89
+ for (const rel of paths) {
90
+ const ext = path.posix.extname(rel).slice(1).toLowerCase();
91
+ const known = SOURCE_LANGUAGE[ext];
92
+ const module = moduleForFile(rel, modules);
93
+ const owners = [...(module ? moduleMap.get(module)?.owners ?? [] : [])];
94
+ let stat;
95
+ try {
96
+ stat = fs.statSync(path.join(abs, ...rel.split("/")));
97
+ }
98
+ catch {
99
+ files.push(emptySemanticFile(rel, module, owners, known?.[0] ?? "unknown", known?.[1] ?? "none", "unreadable", "stat failed"));
100
+ continue;
101
+ }
102
+ if (stat.size > maxBytes) {
103
+ files.push(emptySemanticFile(rel, module, owners, known?.[0] ?? "unknown", known?.[1] ?? "none", "oversize", `${stat.size} bytes exceeds ${maxBytes}`));
104
+ continue;
105
+ }
106
+ let content;
107
+ try {
108
+ content = fs.readFileSync(path.join(abs, ...rel.split("/")), "utf8");
109
+ }
110
+ catch {
111
+ files.push(emptySemanticFile(rel, module, owners, known?.[0] ?? "unknown", known?.[1] ?? "none", "unreadable", "UTF-8 read failed"));
112
+ continue;
113
+ }
114
+ const contract = contractKind(rel, content);
115
+ const verifiedDocuments = [];
116
+ const diagnostics = [];
117
+ for (const candidate of evidenceByFile.get(rel) ?? []) {
118
+ const verified = verifySemanticEvidence(candidate, rel, content);
119
+ if (!verified.ok || !verified.document) {
120
+ const errors = verified.errors.map((error) => `${rel}: ${error}`);
121
+ diagnostics.push(...errors);
122
+ evidenceErrors.push(...errors);
123
+ semanticViolations.push({ type: "semantic-evidence-invalid", from: rel, to: candidate.analyzer.id, evidence: errors });
124
+ continue;
125
+ }
126
+ verifiedDocuments.push(verified.document);
127
+ }
128
+ const evidenceLanguage = verifiedDocuments[0]?.language;
129
+ const language = known?.[0] ?? evidenceLanguage ?? (contract === "openapi" ? "openapi" : "unknown");
130
+ for (const document of verifiedDocuments) {
131
+ if (document.language !== language) {
132
+ const error = `${rel}: evidence language ${document.language} does not match ${language}`;
133
+ diagnostics.push(error);
134
+ evidenceErrors.push(error);
135
+ semanticViolations.push({ type: "semantic-evidence-invalid", from: rel, to: document.analyzer.id, evidence: [error] });
136
+ }
137
+ }
138
+ const validDocuments = verifiedDocuments.filter((document) => document.language === language);
139
+ const hasNativeStaticEvidence = validDocuments.some((document) => document.analyzer.assurance === "ast" && document.analyzer.capabilities.includes("imports"));
140
+ const configuredParser = known?.[1] ?? (contract ? `regex:${contract}-contract-v1` : "none");
141
+ const parseResults = [];
142
+ if (configuredParser !== "none" && ((language === "typescript" || language === "javascript") || !hasNativeStaticEvidence)) {
143
+ parseResults.push(analyzeSource(rel, language, configuredParser, content));
144
+ }
145
+ const analyzers = [
146
+ ...parseResults.map((result) => result.analyzer),
147
+ ...validDocuments.map((document) => document.analyzer),
148
+ ];
149
+ const analyzerById = new Map(analyzers.map((analyzer) => [analyzer.id, analyzer]));
150
+ const candidateReferences = [
151
+ ...parseResults.flatMap((result) => result.references),
152
+ ...validDocuments.flatMap(referencesFromEvidence),
153
+ ];
154
+ const referenceMap = new Map();
155
+ for (const reference of candidateReferences) {
156
+ const key = `${reference.kind}|${reference.line}|${reference.specifier}|${reference.symbol ?? ""}`;
157
+ const previous = referenceMap.get(key);
158
+ const currentRank = assuranceRank(analyzerById.get(reference.parser)?.assurance ?? "heuristic");
159
+ const previousRank = previous ? assuranceRank(analyzerById.get(previous.parser)?.assurance ?? "heuristic") : -1;
160
+ if (!previous || currentRank > previousRank || (currentRank === previousRank && reference.confidence > previous.confidence))
161
+ referenceMap.set(key, reference);
162
+ }
163
+ const candidateExports = [
164
+ ...parseResults.flatMap((result) => result.exports),
165
+ ...validDocuments.flatMap(exportsFromEvidence),
166
+ ];
167
+ const exportMap = new Map();
168
+ for (const item of candidateExports) {
169
+ const key = `${item.name}|${item.line}|${item.sourceSpecifier ?? ""}`;
170
+ const previous = exportMap.get(key);
171
+ const currentRank = assuranceRank(analyzerById.get(item.parser)?.assurance ?? "heuristic");
172
+ const previousRank = previous ? assuranceRank(analyzerById.get(previous.parser)?.assurance ?? "heuristic") : -1;
173
+ if (!previous || currentRank > previousRank || (currentRank === previousRank && item.confidence > previous.confidence))
174
+ exportMap.set(key, item);
175
+ }
176
+ for (const result of parseResults) {
177
+ diagnostics.push(...result.diagnostics);
178
+ for (const limitation of result.limitations)
179
+ analysisLimitations.add(limitation);
180
+ }
181
+ const primary = [...analyzers].sort((a, b) => assuranceRank(b.assurance) - assuranceRank(a.assurance) || a.id.localeCompare(b.id))[0];
182
+ const parser = primary?.id ?? configuredParser;
183
+ const confidence = primary?.assurance === "runtime" ? 1 : primary?.assurance === "ast" ? 0.99 : primary ? known?.[2] ?? 0.8 : 0;
184
+ const incomplete = parseResults.some((result) => result.status === "partial") || validDocuments.some((document) => document.status === "partial");
185
+ const policyFailures = [];
186
+ if (parser !== "none" && !analyzerMeetsPolicy(analyzers, minimumAssurance)) {
187
+ policyFailures.push(`requires ${minimumAssurance}; analyzers provide ${[...new Set(analyzers.map((analyzer) => analyzer.assurance))].join("+") || "none"}`);
188
+ }
189
+ if (failOnFallback && analyzers.some((analyzer) => analyzer.family === "regex-fallback"))
190
+ policyFailures.push("regex fallback is forbidden");
191
+ if ((minimumAssurance === "ast" || minimumAssurance === "runtime") && incomplete)
192
+ policyFailures.push("semantic evidence is partial");
193
+ if (policyFailures.length > 0) {
194
+ semanticViolations.push({ type: "semantic-evidence-below-policy", from: rel, to: minimumAssurance, evidence: policyFailures.map((failure) => `${rel}: ${failure}`) });
195
+ }
196
+ const status = parser === "none" ? "unknown" : policyFailures.length > 0 ? "rejected" : incomplete ? "partial" : "parsed";
197
+ const fileExports = [...exportMap.values()].sort((a, b) => a.line - b.line || a.name.localeCompare(b.name));
198
+ files.push({ path: rel, module, owners, language, parser, confidence, status, analyzers, diagnostics, exports: fileExports });
199
+ if (contract)
200
+ contracts.push({ file: rel, kind: contract, module, confidence: contract === "openapi" ? 0.9 : 0.96 });
201
+ if (parser !== "none" && status !== "rejected")
202
+ rawByFile.set(rel, [...referenceMap.values()]);
203
+ }
204
+ const fileSet = new Set(files.filter((f) => f.status !== "unreadable").map((f) => f.path));
205
+ const contractByBase = new Map();
206
+ for (const contract of contracts)
207
+ contractByBase.set(path.posix.basename(contract.file), [...(contractByBase.get(path.posix.basename(contract.file)) ?? []), contract.file]);
208
+ const interfaceEvidence = (fromModule, toModule, to, specifier, kind) => {
209
+ if (!fromModule || !toModule)
210
+ return null;
211
+ const published = new Set(moduleMap.get(toModule)?.public_interfaces ?? []);
212
+ for (const item of governance.interfaces ?? []) {
213
+ if (!published.has(item.id) || item.provider !== toModule || !item.consumers.includes(fromModule))
214
+ continue;
215
+ const contracts = item.contract_files.map(relPath);
216
+ const exactContract = to !== null && contracts.includes(to);
217
+ const explicit = (kind === "contract" || kind === "grpc" || kind === "http") &&
218
+ (specifier === item.id || specifier === `interface:${item.id}`);
219
+ if (exactContract || explicit)
220
+ return item.id;
221
+ }
222
+ return null;
223
+ };
224
+ const fileEdges = [];
225
+ for (const [from, refs] of rawByFile) {
226
+ const fromModule = moduleForFile(from, modules);
227
+ const owner = fromModule ? moduleMap.get(fromModule) : undefined;
228
+ for (const ref of refs) {
229
+ let to = null;
230
+ let toModule = null;
231
+ let resolution = "external";
232
+ if ((ref.kind === "http" && /^[a-z]+:\/\//i.test(ref.specifier)) || /^(?:inline GraphQL|C ABI)$/.test(ref.specifier))
233
+ resolution = ref.specifier === "inline GraphQL" || ref.specifier === "C ABI" ? "signal" : "external";
234
+ else {
235
+ to = candidates(from, ref.specifier, files.find((f) => f.path === from)?.language ?? "unknown", owner?.roots).find((p) => fileSet.has(p)) ?? null;
236
+ if (!to && ref.kind === "contract") {
237
+ const sameBase = contractByBase.get(path.posix.basename(ref.specifier));
238
+ if (sameBase?.length === 1)
239
+ to = sameBase[0];
240
+ }
241
+ if (to) {
242
+ toModule = moduleForFile(to, modules);
243
+ resolution = "file";
244
+ }
245
+ else {
246
+ toModule = moduleFromSpecifier(ref.specifier, modules);
247
+ if (toModule)
248
+ resolution = "module";
249
+ else if (ref.forceLocal || ref.specifier.startsWith(".") || ref.specifier.startsWith("/"))
250
+ resolution = "unresolved";
251
+ else if (ref.kind === "grpc" || ref.kind === "ffi")
252
+ resolution = "signal";
253
+ }
254
+ }
255
+ const interfaceId = interfaceEvidence(fromModule, toModule, to, ref.specifier, ref.kind);
256
+ fileEdges.push({ from, fromModule, to, toModule, specifier: ref.specifier, kind: ref.kind, line: ref.line, parser: ref.parser, confidence: ref.confidence, symbol: ref.symbol, resolution, interfaceId });
257
+ }
258
+ }
259
+ fileEdges.sort((a, b) => a.from.localeCompare(b.from) || a.line - b.line || a.kind.localeCompare(b.kind) || a.specifier.localeCompare(b.specifier));
260
+ const grouped = new Map();
261
+ for (const edge of fileEdges)
262
+ if (edge.fromModule && edge.toModule && edge.fromModule !== edge.toModule) {
263
+ const key = `${edge.fromModule}\0${edge.toModule}`;
264
+ grouped.set(key, [...(grouped.get(key) ?? []), edge]);
265
+ }
266
+ const interfaceDeclared = (from, to, evidence) => (governance.interfaces ?? []).some((i) => i.provider === to && i.consumers.includes(from) && evidence.some((e) => e.interfaceId === i.id));
267
+ const moduleEdges = [...grouped].map(([key, evidence]) => {
268
+ const [from, to] = key.split("\0");
269
+ const spec = moduleMap.get(from);
270
+ return { from, to, kinds: [...new Set(evidence.map((e) => e.kind))].sort(), files: [...new Set(evidence.map((e) => e.from))].sort(), confidence: Math.max(...evidence.map((e) => e.confidence)), declared: (spec.depends_on ?? []).includes(to) || interfaceDeclared(from, to, evidence) };
271
+ }).sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
272
+ const violations = [...semanticViolations];
273
+ for (const edge of moduleEdges) {
274
+ const spec = moduleMap.get(edge.from);
275
+ const evidence = edge.files;
276
+ if ((spec.denied_dependencies ?? []).some((x) => x === "*" || x === edge.to))
277
+ violations.push({ type: "denied-dependency", from: edge.from, to: edge.to, evidence });
278
+ if ((spec.allowed_dependencies?.length ?? 0) > 0 && !(spec.allowed_dependencies ?? []).some((x) => x === "*" || x === edge.to))
279
+ violations.push({ type: "not-allowed-dependency", from: edge.from, to: edge.to, evidence });
280
+ if ((governance.policies?.enforce_declared_dependencies ?? true) && !edge.declared)
281
+ violations.push({ type: "undeclared-dependency", from: edge.from, to: edge.to, evidence });
282
+ }
283
+ const unresolved = fileEdges.filter((e) => e.resolution === "unresolved");
284
+ const resolvedInternal = fileEdges.filter((e) => e.resolution === "file" || e.resolution === "module").length;
285
+ const internalTotal = resolvedInternal + unresolved.length;
286
+ const unknownFiles = files.filter((f) => f.status === "unknown").map((f) => f.path);
287
+ const skippedFiles = files.filter((f) => f.status === "oversize" || f.status === "unreadable").map((f) => f.path);
288
+ const contractFiles = new Set(contracts.map((c) => c.file));
289
+ const sourceCandidates = files.filter((file) => {
290
+ const ext = path.posix.extname(file.path).slice(1).toLowerCase();
291
+ return SOURCE_LANGUAGE[ext] !== undefined || UNKNOWN_SOURCE_EXTS.has(ext) || contractFiles.has(file.path) || file.analyzers.length > 0;
292
+ });
293
+ const sourceCandidateFiles = sourceCandidates.length;
294
+ const sourceCoveragePct = sourceCandidateFiles === 0 ? 100 : Math.round((sourceCandidates.filter((f) => f.status === "parsed").length / sourceCandidateFiles) * 100);
295
+ const astFiles = sourceCandidates.filter((file) => file.analyzers.some((analyzer) => analyzer.assurance === "ast")).length;
296
+ const runtimeFiles = sourceCandidates.filter((file) => file.analyzers.some((analyzer) => analyzer.assurance === "runtime")).length;
297
+ const heuristicFiles = sourceCandidates.filter((file) => file.analyzers.some((analyzer) => analyzer.assurance === "heuristic")).length;
298
+ const assuredFiles = sourceCandidates.filter((file) => file.status === "parsed" && analyzerMeetsPolicy(file.analyzers, minimumAssurance)).length;
299
+ const semanticAssurancePct = sourceCandidateFiles === 0 ? 100 : Math.round((assuredFiles / sourceCandidateFiles) * 100);
300
+ if (governance.policies?.enforce_public_interfaces ?? true) {
301
+ for (const edge of fileEdges) {
302
+ if (!edge.to || !edge.fromModule || !edge.toModule || edge.fromModule === edge.toModule)
303
+ continue;
304
+ const target = moduleMap.get(edge.toModule);
305
+ if ((target?.public_interfaces?.length ?? 0) === 0 || edge.interfaceId !== null)
306
+ continue;
307
+ violations.push({
308
+ type: "private-interface",
309
+ from: edge.fromModule,
310
+ to: edge.toModule,
311
+ evidence: [`${edge.from}:${edge.line} -> ${edge.to} (${edge.kind}); target publishes ${target.public_interfaces.join(", ")}`],
312
+ });
313
+ }
314
+ }
315
+ if (governance.policies?.enforce_ownership ?? false) {
316
+ for (const file of sourceCandidates) {
317
+ if (file.module !== null && file.owners.length > 0)
318
+ continue;
319
+ violations.push({
320
+ type: "unowned-file",
321
+ from: file.path,
322
+ to: file.module ?? "(no-module)",
323
+ evidence: [file.module === null ? `${file.path} matches no module root` : `${file.path} belongs to ${file.module}, whose owners list is empty`],
324
+ });
325
+ }
326
+ }
327
+ if (governance.policies?.fail_on_unresolved ?? false) {
328
+ for (const edge of unresolved) {
329
+ violations.push({ type: "unresolved-reference", from: edge.fromModule ?? edge.from, to: edge.specifier, evidence: [`${edge.from}:${edge.line} unresolved ${edge.kind} reference ${edge.specifier}`] });
330
+ }
331
+ }
332
+ const minimumCoverage = governance.policies?.minimum_coverage_pct;
333
+ if (minimumCoverage !== undefined && sourceCoveragePct < minimumCoverage) {
334
+ violations.push({ type: "coverage-below-policy", from: "project", to: `${sourceCoveragePct}%`, evidence: [`source coverage ${sourceCoveragePct}% (${sourceCandidates.filter((f) => f.status === "parsed").length}/${sourceCandidateFiles}) is below policy ${minimumCoverage}%`] });
335
+ }
336
+ analysisLimitations.add("Static AST evidence does not prove runtime execution; reflection, generated code, and data-driven dispatch require separately hash-bound runtime evidence.");
337
+ const analyzerGroups = new Map();
338
+ for (const file of files)
339
+ for (const analyzer of file.analyzers) {
340
+ const current = analyzerGroups.get(analyzer.id);
341
+ if (current)
342
+ current.files += 1;
343
+ else
344
+ analyzerGroups.set(analyzer.id, { ...analyzer, capabilities: [...analyzer.capabilities], files: 1 });
345
+ }
346
+ const assurances = new Set(files.flatMap((file) => file.analyzers.map((analyzer) => analyzer.assurance)));
347
+ const analysisConfidence = assurances.size > 1
348
+ ? "mixed"
349
+ : assurances.has("runtime")
350
+ ? "runtime"
351
+ : assurances.has("ast")
352
+ ? "ast"
353
+ : "heuristic";
354
+ const exportedSymbols = files.flatMap((file) => file.exports.map((item) => ({ ...item, file: file.path, module: file.module })));
355
+ return {
356
+ analysis: {
357
+ parser: "hybrid-semantic-v2",
358
+ confidence: analysisConfidence,
359
+ analyzers: [...analyzerGroups.values()].sort((a, b) => a.id.localeCompare(b.id)),
360
+ fallbackFiles: sourceCandidates.filter((file) => file.analyzers.some((analyzer) => analyzer.family === "regex-fallback")).map((file) => file.path),
361
+ rejectedFiles: files.filter((file) => file.status === "rejected").map((file) => file.path),
362
+ evidenceErrors,
363
+ limitations: [...analysisLimitations],
364
+ },
365
+ files, fileEdges, moduleEdges, exports: exportedSymbols, contracts, unresolved, cycles: findModuleCycles(moduleEdges), violations,
366
+ coverage: {
367
+ totalFiles: files.length,
368
+ sourceCandidateFiles,
369
+ sourceCoveragePct,
370
+ parsedFiles: sourceCandidates.filter((f) => f.status === "parsed").length,
371
+ recognizedFiles: files.filter((f) => f.language !== "unknown").length,
372
+ ownedFiles: files.filter((f) => f.module !== null).length,
373
+ unknownFiles,
374
+ skippedFiles,
375
+ references: fileEdges.length,
376
+ resolvedInternal,
377
+ unresolvedInternal: unresolved.length,
378
+ externalOrSignal: fileEdges.filter((e) => e.resolution === "external" || e.resolution === "signal").length,
379
+ resolutionPct: internalTotal === 0 ? 100 : Math.round((resolvedInternal / internalTotal) * 100),
380
+ astFiles,
381
+ runtimeFiles,
382
+ heuristicFiles,
383
+ semanticAssurancePct,
384
+ },
385
+ };
386
+ }
387
+ /** Reverse closure over file targets and module-only contract/FFI edges. */
388
+ export function impactAnalysis(graph, changedFiles) {
389
+ return computeImpactAnalysis(graph, changedFiles);
390
+ }