Skip to content

Fix windows prune - #48

Merged
dbose merged 2 commits into
mainfrom
fix-windows-prune
Sep 28, 2026
Merged

dbose merged 2 commits into
mainfrom
fix-windows-prune

Conversation

@dbose

@dbose dbose commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Fix critical Windows data-loss bugs and harden the codebase for portability

Summary

mdl import erwin reported "imported 52 entities" on Windows but left the model directory empty — a silent data-loss bug. Investigating it revealed a whole class of POSIX-only assumptions that break on Windows. This PR fixes the originating bug and then does a full audit-driven hardening pass across Python and TypeScript.

None of these were caught by the 942-test suite or the Docker check — both run on POSIX (forward-slash) filesystems, so the entire bug class is invisible to them. New regression tests simulate the Windows path separator (PureWindowsPath) so they catch it on any OS.

CRITICAL — data loss / editing unusable on Windows

  • Import deleted every file it just wrote (mdl_reverse.writer._prune_stale). _prune_stale compared each on-disk path built with str(relative_to()) (backslash on Windows) against a forward-slash "just wrote" set. On Windows nothing matched, so every freshly-written entity was pruned as "stale" and the emptied dirs removed. The summary line prints the in-memory count, masking it. Affects all importers (erwin, reverse, OSI).
  • The same bug in mdl_core.state.py — GenerationState.save() deleted the entire .mdl/state store (the 3-way-merge base) on every save on Windows, silently corrupting generation state.
  • CRLF made the canvas/LSP read-only on Windows. dir_fingerprint summed raw byte size; a Git-for-Windows (autocrlf) CRLF checkout differs from the LF size the client holds, firing StaleModelError / HTTP 409 on every edit. Now hashes newline-normalised content, and the generated .gitattributes pins eol=lf on tool-owned files.
  • dbt model-name collisions silently overwrote each other (emit-dbt). Two entities mapping to the same name (or differing only in case — the same file on a case-insensitive FS) clobbered one another's .sql and broke ref(). Now raises loudly.

HIGH — wrong behavior on Windows

  • One shared hardened filename slug (new mdl_core.fsnames): strips Windows-illegal chars, trailing dot/space, and reserved device names (CON/PRN/NUL/COM1-9/LPT1-9), with case-fold collision dedup. Replaces three weaker _slug copies; the command engine's could inject a path separator or crash open() on a user-typed name.
  • ~9 subprocess(text=True) calls (git/gh/dbt) now pass encoding="utf-8", errors="replace" — Windows decoded them as cp1252, garbling a non-ASCII git identity (e.g. "José" → wrong branch names and audit attribution), PR titles, and dbt output.
  • driftDiagnostics.ts compared document.fileName (backslash) against the CLI report's POSIX path with endsWith, so drift quick-fixes silently disappeared on Windows. Normalised to forward slashes.

LOW

Added encoding to remaining plain write_text calls; chatParticipant/extension use path.join / path.sep.

Tests

New: test_writer_prune_windows, test_state_prune_windows, test_fingerprint_newlines (LF/CRLF produce identical fingerprints), test_fsnames (illegal chars, reserved names, case dedup), test_model_name_collision. 942 pytest passed, ruff clean, all 6 extension gates green, Docker clean-install green.

Notes

  • What was already correct (not touched): most file I/O already had encoding="utf-8", subprocess uses argv lists (no shell=True), temp/home/env via tempfile/Path.home(), _force_utf8_output for stdout, and the extension's resolve.ts (finds mdl.exe on locked-down Windows).
  • Follow-up: a real windows-latest CI job is the durable guard against this class (deferred). The PureWindowsPath simulation tests are the interim.

CLI modelith-dbt bump to 0.6.12.

On Windows, mdl import erwin reported 'imported 52 entities' but left the
model dir EMPTY — a silent data-loss bug affecting every importer (erwin,
reverse, OSI all go through write_model).

Root cause: _prune_stale deletes .yaml files under the owned dirs that this
write did not produce, comparing each on-disk path against the
set. It built the comparison key with str(path.relative_to(root)), which
stringifies with the OS separator — BACKSLASH on Windows — while
holds forward-slash keys from dump()'s f-strings. So on Windows every
just-written file (logical\entities\e.yaml) failed the  check
(logical/entities/e.yaml) and was immediately unlinked, then the emptied
dirs were rmdir'd. macOS/Linux use '/' on both sides, so it never triggered
— which is why 939 tests and the Linux Docker check missed it.

Fix: compare with as_posix() so the separator is normalised on every OS, and
keep dump()'s recorded rel POSIX-style too.

Regression test simulates the Windows separator directly (PureWindowsPath),
so this class is caught on any OS. A real windows-latest CI job is a
follow-up.

CLI 0.6.12.
A full audit after the import data-loss bug found the whole class of POSIX-only
assumptions that break on Windows. This fixes them.

CRITICAL (data loss / unusable on Windows):
- core/state.py: the SAME prune-separator bug as the writer. save() compared
  str(relative_to()) (backslash) against forward-slash _shard_rel keys, so every shard
  was deleted on every save, wiping the 3-way-merge base. Fixed with as_posix().
- core/commands.py dir_fingerprint: summed raw byte st_size, so a CRLF checkout differed
  from the LF size the client held, firing StaleModelError on every edit (canvas/LSP
  read-only on Windows). Now hashes newline-normalised content. Plus cli/collab.py's
  generated .gitattributes pins eol=lf on model YAML, .mdl/lock.yaml, decisions, state.
- emit-dbt/emitter.py: two entities mapping to the same dbt model name (or case-only
  differing, the same file on a case-insensitive FS) silently overwrote each other's .sql
  and broke ref(). Now raises loudly.

HIGH:
- One shared hardened filename slug (new core/fsnames.py) reused by writer and commands:
  strips Windows-illegal chars, trailing dot/space, reserved device names
  (CON/NUL/COMn/LPTn), with case-fold collision dedup. Replaces three weaker _slug copies.
- ~9 subprocess(text=True) calls (git/gh/dbt) now pass encoding=utf-8, errors=replace:
  Windows decoded output as cp1252, garbling non-ASCII branches, PR titles, git identity.
- vscode/driftDiagnostics.ts: document.fileName (backslash) vs the CLI report's POSIX
  i.file with endsWith made drift quick-fixes silently vanish on Windows. Normalised.

LOW: encoding on the remaining plain write_text; chatParticipant/extension use
path.join / path.sep.

Tests: fsnames, state prune, CRLF-insensitive fingerprint, dbt name collision. 942 pytest,
ruff clean, all extension gates, Docker clean-install green. Windows CI is a deferred
follow-up (the whole class is invisible to a POSIX-only test matrix).
@dbose
dbose merged commit 72961da into main Sep 28, 2026
2 checks passed
dbose added a commit that referenced this pull request Sep 28, 2026
PR #48 fixed driftDiagnostics.ts (drift quick-fixes silently vanished on
Windows because document.fileName's backslash path was compared against the
CLI report's POSIX path), plus path.join/path.sep in chatParticipant and
extension, but left the extension version at 0.3.16. Bump so the shipped fix
carries a version.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant