🌳
pt0/serverF/opencodeMcpF/violBaselineAI.mts
1import { execSync } from 'child_process'
2import ts from 'typescript'
4import { ptDir } from '../ptDirF.mts'
7export type BaselineContentMap = Map<string, string | null> // absPath -> HEAD content (null = not in HEAD, i.e. new file)
9export const loadHeadContents = (ptPathsA: string[]): BaselineContentMap => {
10 const map: BaselineContentMap = new Map()
11 for (const ptPath of ptPathsA) {
12 let content: string | null = null
13 try { content = execSync(`git show HEAD:${ptPath}`, {cwd: ptDir, encoding: 'utf8', stdio: 'pipe'}) } catch { content = null } // new file: no HEAD blob
14 map.set(ptPathToAbsPath(ptPath as any), content)
15 }
16 return map
19export const normalizeViolKey = (viol: string) => viol.replace(/^(\S+?):\d+:\d+( - )/, '$1$2')
21export const partitionRatchetViols = (viols: string[], ratchetDirs: string[], excludes: string[]) => {
22 const ratchetViols: string[] = [], lenientViols: string[] = []
23 for (const viol of viols) {
24 const ptPath = viol.match(/^(\S+?)(?:\s|:\d)/)?.[1]
25 const isRatchet = !!ptPath && ratchetDirs.some(d => ptPath.startsWith(d)) && !excludes.some(d => ptPath.startsWith(d))
26 ;(isRatchet ? ratchetViols : lenientViols).push(viol)
27 }
28 return {ratchetViols, lenientViols}
31export const subtractBaselineViols = (currentViols: string[], baselineViols: string[]) => {
32 const remaining = new Map<string, number>()
33 for (const viol of baselineViols) {
34 const key = normalizeViolKey(viol)
35 remaining.set(key, (remaining.get(key) ?? 0) + 1)
36 }
37 const newViols: string[] = []
38 let preExistingCount = 0
39 for (const viol of currentViols) {
40 const key = normalizeViolKey(viol)
41 const count = remaining.get(key) ?? 0
42 if (count > 0) {
43 remaining.set(key, count - 1)
44 preExistingCount++
45 continue
46 }
47 newViols.push(viol)
48 }
49 return {newViols, preExistingCount}
52export const createHeadOverlayHost = (options: ts.CompilerOptions, baselineContent: BaselineContentMap): ts.CompilerHost => {
53 const host = ts.createCompilerHost(options)
54 const origReadFile = host.readFile
55 const origFileExists = host.fileExists
56 host.readFile = (fileName: string) => baselineContent.has(fileName) ? baselineContent.get(fileName) ?? undefined : origReadFile(fileName)
57 host.fileExists = (fileName: string) => baselineContent.has(fileName) ? baselineContent.get(fileName) !== null : origFileExists(fileName)
58 return host
61export const createBaselineProgram = (
62 roots: string[],
63 options: ts.CompilerOptions,
64 editedAbsSet: Set<string>,
65 oldProgram?: ts.Program,
66): ts.Program => {
67 const baselineContent = loadHeadContents([...editedAbsSet].map(p => toPtRelPath(p)))
68 return ts.createProgram(roots, options, createHeadOverlayHost(options, baselineContent), oldProgram)
71const diagKey = (diag: ts.Diagnostic): string => {
72 const msg = ts.flattenDiagnosticMessageText(diag.messageText, '\n')
73 return `${diag.code}:${diag.start ?? 0}:${msg}`
76export const getBaselineDiagKeys = (
77 baselineProgram: ts.Program,
78 importerAbsPaths: string[],
79 surfaceCodes: Set<number>,
80): Set<string> => {
81 const keys = new Set<string>()
82 for (const absPath of importerAbsPaths) {
83 const sourceFile = baselineProgram.getSourceFile(absPath)
84 if (!sourceFile) continue
85 const diagnostics = [
86 ...baselineProgram.getSyntacticDiagnostics(sourceFile),
87 ...baselineProgram.getSemanticDiagnostics(sourceFile),
88 ]
89 for (const diag of diagnostics) {
90 if (diag.category !== ts.DiagnosticCategory.Error) continue
91 if (!surfaceCodes.has(diag.code)) continue
92 if (diag.file && diag.file.fileName !== absPath) continue
93 keys.add(diagKey(diag))
94 }
95 }
96 mcpDebugLog(`baseline-diag-keys: ${keys.size} pre-existing importer errors filtered`)
97 return keys
100export const isPreExistingImporterViol = (
101 diag: ts.Diagnostic,
102 baselineDiagKeys: Set<string>,
103): boolean => baselineDiagKeys.has(diagKey(diag))