feat(vba-extractor): UTF-8 BOM strip and Windows-1252 fallback for VBA files (closes #53) - #71
Merged
Conversation
…A files (closes #53) Defense in depth: dysflow normally guarantees UTF-8-no-BOM for VBA exports, but codegraph-vba can be pointed at exports from older dysflow runs, other tools, or hand-copied modules that are still CP1252 on disk. Today every file is read as UTF-8 unconditionally; a CP1252 file with accented identifiers (ubiquitous in Spanish-language Access code: `Sub ActualizarSituación`, enum member `Sí`, module headers `' MÓDULO:`) decodes to replacement characters, `\p{L}` regexes stop matching, and the symbol silently disappears from the graph. A BOM-carrying UTF-8 file additionally corrupts the first line (breaks `Attribute VB_Name` detection → wrong module name). Changes: * `src/extraction/vba-source.ts` (NEW) — small encoding helper: - `isVbaFamilyFile(filePath)` — true for `.bas/.cls/.frm/.dsr/ .form.txt/.report.txt/.sql` (case-insensitive). - `readVbaSource(filePath, opts?)` — reads the file as `Buffer`, strips a leading UTF-8 BOM if present, tries `TextDecoder('utf-8', { fatal: true })`, and falls back to `TextDecoder('windows-1252')` on decode failure (cheap sniff: one fatal decode attempt; the fallback only fires when UTF-8 actually fails). Returns `{ text, bomStripped }`. Optional `readFile` DI for testing and optional `onFallback` callback for a low-severity warning surface. - `stripUtf8Bom(text)` — string-level BOM strip as a defensive post-process for callers that already hold a string. * `src/extraction/index.ts` — wires the helper into the file-read pipeline via two thin helpers, `readForExtraction` (async, used at the batch + retry + comment-strip + single-file sites) and `readForExtractionSync` (sync, used at the `sync()` reconcile + fast-path + full-scan sites). Each calls `isVbaFamilyFile` exactly once and branches: VBA-family files go through `readVbaSource`, everything else stays on the existing byte-identical `readFile(path, 'utf-8')` path. 7 read sites rewired (1412, 1524, 1568, 1688, 1983, 2067, 2112). The 3 detect-time hashing sites are included deliberately so a BOM-carrying `.bas` does not report as perpetually "modified" against its BOM-stripped store-state hash. * `__tests__/vba-source.test.ts` (NEW) — 8 atoms covering the BOM strip, the CP1252 fallback path, the `onFallback` callback wiring, the empty-buffer edge case, the UTF-8 happy path, and the VBA-family truth table. Uses the helper's `readFile` DI — no temp files required for unit tests. Scope discipline (per Issue #53 spec): only VBA-family files get the BOM strip + CP1252 fallback. Tree-sitter-routed languages (TS, JS, Python, Go, Rust, etc.) keep the existing `readFile(path, 'utf-8')` path byte-identical. Acceptance criterion "valid UTF-8 path byte- identical to today (no behavior change, no perf cliff)" holds. Validation: * `pnpm exec vitest run __tests__/vba-source.test.ts` — 8 passed in 403 ms * `pnpm exec vitest run __tests__/extraction-vba.test.ts __tests__/extraction-vba-control-modeling.test.ts __tests__/extraction-vba-form.test.ts __tests__/extraction-vba-enums-consts.test.ts __tests__/extraction-vba-realfixtures.test.ts __tests__/extraction-vba-roadmap-25-26.test.ts` — 236 passed in 5.03 s, zero regressions * `pnpm run build` — tsc clean, no TypeScript errors
ardelperal
added a commit
that referenced
this pull request
Jul 4, 2026
…mts` config (#72) PR #70 added `vitest.config.ts` with `testTimeout: 30_000`, but vitest 3.x prefers the existing ESM `vitest.config.mts` when both are present in the same project root — so the `.ts` was a silent no-op and the windows CI jobs continued to fail at the default 5000ms timeout (workflow runs `28702911905` and `28703027415` on PR #71 both flaked at `__tests__/db-perf.test.ts:116` and `index-command.test.ts:97` with the same 5000ms default). Fix: add `testTimeout: 30_000` to the actively-loaded `vitest.config.mts` (the existing file with maxWorkers/pool/execArgv already in place for the Node >=25 dev-machine safety). No other changes — the `.ts` sibling from PR #70 is left in place for reference but does not affect test execution; a future housekeeping PR can delete it. Verification: * `pnpm exec vitest run __tests__/index-command.test.ts __tests__/db-perf.test.ts` → 16 passed locally. * The flake pattern (5s timeout crossing on CI Windows) is closed by the global cap; per-test explicit timeouts (`it(name, fn, 15_000)`) still take precedence. User rule upheld: "todo lo que se integre a main ha de estar verde".
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #53.
What
Defense in depth for VBA-family source decoding. dysflow normally
guarantees UTF-8-no-BOM on export, but codegraph-vba can be pointed at
exports from older dysflow runs, hand-copied modules, or other tools
that still write CP1252 (ANSI) — the Access
SaveAsText/ VBIDEExportdefault. Today every file is read as UTF-8 unconditionally, soa CP1252 file with accented identifiers (ubiquitous in Spanish-
language Access code:
Sub ActualizarSituación, enum memberSí,module headers
' MÓDULO:) decodes to replacement characters,\p{L}regexes stop matching, and the symbol silently disappearsfrom the graph. A BOM-carrying UTF-8 file additionally corrupts the
first line (breaks
Attribute VB_Namedetection → wrong modulename).
Why
A Dysflow-managed Access project where every author wrote
Sub ActualizarNombre()would have its second-most-frequent identifiersilently invisible to the graph because of a one-byte encoding
mismatch. This fix makes the read path encoding-robust for VBA-family
files only (
.bas/.cls/.frm/.dsr/.form.txt/.report.txt/.sql).Diff
src/extraction/vba-source.ts(NEW)src/extraction/index.ts(wire-up)__tests__/vba-source.test.ts(NEW)Source change is well under the 400-line review budget.
Design
src/extraction/vba-source.ts(NEW)A small focused helper module exporting three functions:
isVbaFamilyFile(filePath)— case-insensitive extension match.readVbaSource(filePath, opts?)— reads the file asBuffer,strips a leading UTF-8 BOM if present, tries
TextDecoder('utf-8', { fatal: true }), and falls back toTextDecoder('windows-1252')on decode failure. Returns
{ text, bomStripped }. OptionalreadFileDI for testing + optionalonFallbackcallback so theCLI can surface a low-severity warning when the fallback fires.
The UTF-8 sniff is
O(fileSize)but only runs the decoder once onthe happy path. CP1252 fallback only fires when UTF-8 actually
throws — valid UTF-8 (dysflow, modern editors, anything written
today) is byte-identical to
readFileSync(path, 'utf-8')with theBOM difference only.
stripUtf8Bom(text)— defensive string-level BOM strip forcallers that already hold a string. (Most callers go through
readVbaSourceand never need it.)src/extraction/index.ts(wire-up)Two thin helpers (
readForExtractionasync +readForExtractionSyncsync) replace each
fsp.readFile(fullPath, 'utf-8')/fs.readFileSync(fullPath, 'utf-8')call site. Each callsisVbaFamilyFileexactly once and branches: VBA-family files gothrough
readVbaSource; everything else stays on the existingbyte-identical
readFile(path, 'utf-8')path. 7 read sitesrewired: 1412 (batch parallel reader), 1524 (WASM-retry reader),
1568 (comment-strip retry reader), 1688 (single-file index read),
1983 (sync reconcile hash), 2067 (git fast-path change detect
hash), 2112 (full-scan fallback change detect hash).
The 3 detect-time hashing sites are included deliberately: the
detector hashes the read content, and the storage stores the
BOM-stripped text — if the BOM strip didn't happen at read time
the hash would always diverge and the file would report as
perpetually "modified".
Surrounding
try/catch,stat, and error-attribution code is leftintact. Three non-extraction reads (
.gitignorebuffer,.gitgitdir file, framework-detection manifest probe) are NOT touched —
they're not VBA source.
__tests__/vba-source.test.ts(NEW)8 atoms using the helper's
readFileDI for in-memoryBufferfixtures:
textmatches utf-8 decode,bomStripped === false, no fallback callback fires.textdoes NOT start with\uFEFF,bomStripped === true.'ó'(0xF3) — falls back to Windows-1252,textends with'ó'.{ text: '', bomStripped: false }.Attribute VB_Name = "MiClase"(Spanish-accented Mi Clase) — BOM-stripped resolution works.onFallbackcallback fires when CP1252 fallback reaches it and is NOT fired on the UTF-8 happy path.isVbaFamilyFiletrue cases:.bas,.cls,.form.txt,.report.txt,.sql.isVbaFamilyFilefalse cases:.ts,.js,.json,.md,.clsss,MyModule.txt.Scope discipline
Per Issue #53 spec, only VBA-family files get the BOM strip +
CP1252 fallback. Tree-sitter-routed languages (TS, JS, Python,
Go, Rust, etc.) keep the existing
readFile(path, 'utf-8')pathbyte-identical. Acceptance criterion "valid UTF-8 path byte-
identical to today (no behavior change, no perf cliff)" holds.
Validation
pnpm exec vitest run __tests__/vba-source.test.ts— 8 passed in 403 mspnpm run build— tsc clean, no TS errorsOut of scope (intentional)
e.g.
0x80..0x9Fdiffer slightly between CP1252 and CP1254Turkish). CP1252 is the documented default for Access
SaveAsText/ VBIDE
Export; that's what we fall back to.CP1252 fallback covers 99% of Access exports. Per-byte-detection
of these encodings is out of scope for this P3 issue.
__tests__/index-command.test.tsand__tests__/db-perf.test.tsis fixed separately in PR fix(tests): bump e2e index-command test timeout for Windows CI runners #70 (vitest.config.ts with
testTimeout: 30_000); that fix lands onrelease-origin/mainbefore this PR's target HEAD and applies automatically.
Not done
n/a — issue complete in this PR.