fix(semantic-layer): support snapshot export on Windows#531
Conversation
| - `src/keboola_agent_cli/changelog.py` | ||
| - record the Windows export fix in the next release entry. |
There was a problem hiding this comment.
🔍 Changelog entry promised in design doc is missing from the PR
The design spec's implementation map (docs/superpowers/specs/2026-07-23-issue-529-windows-semantic-layer-export-design.md:73-74) states src/keboola_agent_cli/changelog.py should record the Windows export fix, but no changelog change is present in this PR. Per CONTRIBUTING.md, make changelog-check is a release-time / local-only gate (not a per-PR CI gate), so this will not fail CI, but the PR does not fully match its own stated implementation map. Reviewer may want to confirm whether the changelog entry is intended for a follow-up version bump.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Addressed in 2c0e080. The PRD now states that this PR intentionally has no changelog edit because the repository has no unreleased section; the fix is recorded when the next release version is prepared.
padak
left a comment
There was a problem hiding this comment.
Review of #531 — fix(semantic-layer): support snapshot export on Windows
Generated by
kbagent-pr-reviewersubagent. Verdict and findings below
are advisory; the human author retains every veto. CI-coverable issues
(lint, format, tests) are confirmed viamake check, not duplicated here.
Summary
This PR fixes a genuine, reproducible Windows crash: write_snapshot_to_file() (shared by semantic-layer export and the optional semantic-layer build --output path) hardcoded os.O_NOFOLLOW in its os.open() flags expression, which raises AttributeError on Windows before the file is ever opened, since that constant does not exist in Windows' os module. The fix builds the flags incrementally with getattr(os, "O_NOFOLLOW", 0) (preserves POSIX symlink hardening, no-ops on Windows) and additionally adds getattr(os, "O_BINARY", 0) to prevent Windows CRT text-mode CRLF translation from corrupting the byte-exact JSON payload — a detail that is easy to miss and correctly reasoned through in the PR's design doc. Two new regression tests are added (a cross-platform Windows-simulation test via monkeypatch.delattr(os, "O_NOFOLLOW"), using real non-ASCII content, and a POSIX-only symlink-rejection test that did not exist before this PR), plus a new Windows CI job step that runs the focused export tests on a real windows-latest runner. The change is tightly scoped: no CLI command, flag, JSON shape, or exit code changed. Verdict: APPROVE — no blocking or non-blocking issues found; one cosmetic nit on PR-description staleness.
Verdict
- Verdict: APPROVE
- Blocking findings: 0
- Non-blocking findings: 0
- Nits: 1
Blocking findings
(none)
Non-blocking findings
(none)
Nits
[NIT-1]PR description ("Implementation map") vs. committed design doc — the GitHub PR description still listssrc/keboola_agent_cli/changelog.pyunder "Implementation map" ("record the Windows export fix in the next release entry"), but the actual committed filedocs/superpowers/specs/2026-07-23-issue-529-windows-semantic-layer-export-design.md:114-116was revised during implementation to explicitly say no changelog entry is made in this PR — and indeedchangelog.pyis not in the diff, which is the correct call (0.76.1 is already tagged/released; there's no open "unreleased" changelog key to append to, matching the precedent set by the immediately-preceding sibling PR #530). Considergh pr edit 531to sync the description with the final decision so a reviewer skimming only the GitHub body doesn't wonder whetherchangelog.pywas supposed to change.
Verification log
gh auth status→ authenticated aspadak,repo/workflowscopes ✓- Read
CONTRIBUTING.md(Checklist: Adding a New CLI Command, Plugin synchronization map, Releasing a new version) ✓ — confirmed this PR adds/removes/renames zero CLI commands, so the per-command checklist and most of the sync-map table are N/A. - Read
CLAUDE.mdconvention #17 and## All CLI Commands✓ — no command signature affected. - Read
plugins/kbagent/agents/keboola-expert.md§1 and §3 ✓ — no rule/gotcha implicated; greppedcontext.py,keboola-expert.md,gotchas.md,commands-reference.md,CLAUDE.mdforO_NOFOLLOW/0o644/atomic-write→ empty, nothing agent-facing references the changed internals. gh pr view 531 --json title,body,files,additions,deletions,baseRefName,headRefName,labels,state→ OPEN,fix(semantic-layer):prefix matches a bug fix, 4 files, +170/-13 ✓gh pr checkout 531→ failed (already used by worktree at /private/tmp/kbagent-issue-529); usedgit worktree list+gh pr view --json headRefOidto confirm that pre-existing worktree is clean and sits exactly at the PR head SHA2c0e080d1a8d52fd63907c046a1b89ba85bfe47d✓ — read PR code from there; own tree (claude/pr-531-review-f4d37a) left untouched (git status --shortclean, confirmed before and after).gh pr diff 531→ 264-line diff across.github/workflows/ci.yml, newdocs/superpowers/specs/2026-07-23-issue-529-windows-semantic-layer-export-design.md,src/keboola_agent_cli/services/_semantic_layer_internals.py,tests/test_semantic_layer_service.py✓- Layer-violation greps (typer/click in services, httpx/requests in commands, formatter/typer in clients) → all empty ✓;
git diff main...HEAD -- cli.py commands/**for new/removed@*_app.command→ empty (no command surface change) ✓ grep -rn write_snapshot_to_file src/ tests/→ confirmed exactly 2 call sites (semantic_layer_service.py:619inexport_model,:1595inbuild), both delegate identically with no divergent wrapper logic, so one root-cause fix + one shared regression test genuinely covers both callers ✓- Reproduced the reported bug directly:
python -cwithos.O_NOFOLLOWdeleted, evaluating the old (pre-fix,git show main:...) expressionos.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW→AttributeError: module 'os' has no attribute 'O_NOFOLLOW'(exact match to the issue); evaluating the newgetattr-based expression under the same simulated environment → succeeds, flags = 1537 ✓ uv run pytest tests/test_semantic_layer_service.py -k "export" -v(mirrors the new CI Windows step exactly) →5 passed(test_export_envelope_shape,test_export_writes_file_with_correct_permissions,test_export_writes_utf8_json_without_o_nofollow,test_export_rejects_existing_symlink_output_path,test_export_default_path) ✓uv sync --extra server(environment fix for a fresh worktree venv, not a code change) thenmake check→ruff checkclean,ruff format --checkclean,ty checkexit 0 (3 pre-existingwarning[unresolved-import]diagnostics unrelated to this diff — explicitly non-blocking per CONTRIBUTING.md), SKILL.md up-to-date, version in sync,check_command_sync.py→ "all 251 CLI commands registered/documented",generate_changelog.py --check→ "All 43 stable releases have changelog entries" (confirms no changelog gap),check_error_codes.pyclean, full suite → 4669 passed, 8 skipped, 132 deselected ✓python -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))"→ parses cleanly, newTest semantic-layer export on Windows (issue #529)step correctly ordered insidebuild-windowsjob, no--extra serverneeded sincetest_semantic_layer_service.py/conftest.pyimport nothing fromfastapi/server(verified via import grep) souv run pyteston the bare Windows runner env resolves fine ✓- Verified
os.O_BINARYdoes not exist on this POSIX runner (hasattr(os, "O_BINARY") == False) so the new flag is a genuine no-op here, matching the PR's own claim ✓ diffof the raw GitHub PR body against the committed design-doc file → 2 differences: trailing whitespace (cosmetic) and the changelog.py paragraph (→ NIT-1) ✓- Checked precedent: sibling PR #530 (
fix(update): make self-update safe on Windows, merged immediately prior onmain) also added adocs/superpowers/specs/...design.md, also touched zero plugin-sync surfaces and zerochangelog.py, also did not bump the version — same author pattern, already accepted ✓
Open questions for the author
(none)
Design: Make semantic-layer snapshot export portable to Windows
Issue: #529
Status: Proposed
Target:
kbagent semantic-layer exportand every shared snapshot writerProblem
write_snapshot_to_file()builds itsos.open()flags withos.O_NOFOLLOW. That flag is available on POSIX platforms but is not definedby Python's
osmodule on Windows. Evaluating the flags therefore raisesAttributeErrorbefore the output file is opened.The helper is shared by semantic-layer export and the optional build output
path, so the portability bug affects more than one command even though export
is the reported reproducer.
Goals
symlink protection.
0o644permissions contract on POSIX.regression tests.
Non-goals
ignored by Windows.
not expose a direct
O_NOFOLLOWequivalent throughos.open.Design
Build the flags incrementally from portable flags:
O_NOFOLLOWremains active and a pre-existing final-componentsymlink is rejected.
os.open()succeeds.O_BINARYis a no-op where unavailable and prevents Windows text-modenewline translation because the helper writes an already encoded byte
payload with
os.write().Keep the descriptor lifecycle in the existing
try/finally; do not replace itwith a text-mode
Path.write_text()call, which would discard the POSIXhardening and change the low-level write contract.
Update the docstring to describe conditional
O_NOFOLLOWprotection ratherthan implying that every platform provides it.
Implementation map
src/keboola_agent_cli/services/_semantic_layer_internals.pytests/test_semantic_layer_service.pyos.O_NOFOLLOWand verify exportcreates valid UTF-8 JSON;
O_NOFOLLOWexists, verify a symlink output path is rejected and itstarget remains unchanged;
.github/workflows/ci.ymlwindows-latestjob so the real Windowsosflags are exercised.src/keboola_agent_cli/changelog.pyAcceptance criteria
write_snapshot_to_file()on Windows never accessesos.O_NOFOLLOWdirectly.kbagent semantic-layer export --output <path>creates parseable UTF-8 JSONon Windows.
0o644files.O_NOFOLLOW, a pre-existing symlink at the output path isrejected and the symlink target is not modified.
test and on the existing Windows CI runner.
Validation
Run locally:
On
windows-latest, run the focused export regression against Python 3.12 andthe built project. Confirm the output contains non-ASCII data without newline
or encoding corruption.
Risks
O_NOFOLLOW. This PR preserves the strongest protection Python exposesthrough the current low-level API on each platform and documents the gap.
existing Windows dependency/build setup is reused.