Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,9 @@ kbagent config new --component-id ID [--name NAME] [--project NAME] [--output-di
# the scaffold had no ID and always landed in the default branch tree, so the next
# `sync push` created a DUPLICATE (34-config incident). With --configuration, the local file
# mirrors the pushed encrypted body -- placeholders would overwrite the remote on next push.
# The scaffold also records _keboola.base_config_hash (#792 E), the created config as the API
# returned it, so `sync diff` compares it 3-way and push never overwrites an edit made in the UI.
# Version gate in gotchas.md.

# sync: GitOps -- configs as local files. init/pull/push/diff are filesystem-local (no serve REST surface).
kbagent sync init --project ALIAS [--directory DIR] [--git-branching] [--adopt-existing] [--with-workspaces]
Expand All @@ -1041,6 +1044,13 @@ kbagent sync push --project ALIAS [--all-projects] [--dry-run] [--force] [--allo
# under skipped_deletions (+ skipped_deletions_reason), also in --dry-run, whose summary.deleted counts only
# what push would delete. A config/row deleted on the remote since the last pull diffs as remote_deleted and
# is never re-created (it lands in skipped). Version gate in gotchas.md.
# sync diff/push/pull and adopted files (#792 E): an untracked _config.yml whose _keboola.config_id
# exists on the target has no baseline unless it records _keboola.base_config_hash; without one a
# difference is a conflict (push skips it), two files with one id are both conflicts, and pull keeps
# such a file with local changes (skipped) -- only --theirs overwrites it. Version gate in gotchas.md.
# sync push encryption (#792 F): every #-secret is encrypted before the first write, so an
# ENCRYPTION_FAILED stops push before anything reaches the remote (the message ends with "Nothing
# was pushed.") and a retry creates nothing twice. Version gate in gotchas.md.
# sync push workspace delete (CLI-25): in a `syncWorkspaces` tree, a `push --force` that deletes a shared SQL workspace also deletes its SQL editor sessions (every user's, push branch) and their backend workspaces, which `config restore` does not bring back; `push --dry-run --force` lists them (warnings[] workspace_sessions), a plain push lists the workspace under skipped_deletions and touches no session. `--force` is destructive-class (FLAG_ESCALATIONS `sync.push --force`), so `--deny-destructive` / a cli:destructive deny blocks it while a plain push stays write-class. Version gate for this entry lives in gotchas.md.
# Partial failures change the exit code (#745): commands that collect per-item failures
# and keep going exit 1 (was 0) when at least one item failed:
Expand Down
16 changes: 8 additions & 8 deletions formal/sync/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ explores traces of up to 5 actions. BFS returns the shortest counterexample.
| I5 pull keeps local work | violated: a remote delete plus a local edit ends with plain or `--force` pull deleting the edited directory silently |
| I6 push then diff is clean | violated on `--branch` promote (finding D, since fixed in the engine; the TLA model is unchanged). Holds on production only (30,334 states) |
| I8 manifest matches disk | violated: the stale sweep, and a promote write-back that records a `devt/` entry for a file in `main/` |
| I9 an aborted push is atomic | violated (strong reading): the changes before the failing one reached the API and the manifest was never saved |
| I11 no lost remote update | violated: an adopted file with a config id is diffed 2-way, so push reverts a UI edit |
| I9 an aborted push is atomic | violated (strong reading): the changes before the failing one reached the API and the manifest was never saved. Fixed in the code (F: push encrypts before its first write); the model is unchanged |
| I11 no lost remote update | violated: an adopted file with a config id is diffed 2-way, so push reverts a UI edit. Fixed in the code (E); the model is unchanged |
| I11b no silent resurrect | violated: a remote delete followed by any push re-creates the config, even with no local edit (S2). Fixed in the code (H); the model is unchanged |
| I12 a pull resolves REMOTE MODIFIED | violated: after a cosmetic edit, plain pull skips the file forever and `--force` raises a conflict (S3) |

Expand All @@ -103,8 +103,8 @@ the regression test for each in `tests/test_sync_formal_counterexamples.py`.
| B | Pull compares only `_config.yml`: local edits in `transform.sql`/`code.py`/`_description.md` are silently overwritten when the remote changed, no SYNC_CONFLICT | Lean F6 | HIGH -- **FIXED** (pull's local-modification check now covers every `pull_extra_hashes` companion file) | `test_b_pull_never_overwrites_local_sql_edit` (unmarked regression guard) + `tests/test_sync_pull_companion_files.py` |
| C | Remote delete + local edit: pull (plain/`--force`) deletes the locally-edited directory with no conflict | Lean F7, TLA I5 | HIGH -- **fixed** (#792 A/C PR) | `test_c_pull_never_deletes_locally_edited_dir_on_remote_delete` |
| D | `sync push --branch dev` (promote) creates another dev copy of a prod-only config on every push. **Fixed** (fix/792-dev-promote-duplicates): on the promote path the target-branch entry shadows the production entry for the same `main/` dir (`sync/branch_scope.py::_promoted_paths`) | TLA I6/I1 | HIGH | `test_d_promote_push_is_idempotent`, `test_d_promote_push_diff_is_clean_and_edits_update_the_dev_copy` (regression guards) |
| E | An untracked file carrying a config id (`config new --push --output-dir` scaffold / adopted orphan) is diffed 2-way: push overwrites a UI edit made after the scaffold was written | TLA I11 | MED | `test_e_adopted_scaffold_push_does_not_overwrite_remote_edit` |
| F | A push aborted by `ENCRYPTION_FAILED` leaves the manifest unsaved; the retry duplicates the change(s) the aborted push already applied | TLA I1b (model trace only; replayed live for this pilot) | MED | `test_f_aborted_push_does_not_duplicate_already_created_config` |
| E | An untracked file carrying a config id (`config new --push --output-dir` scaffold / adopted orphan) is diffed 2-way: push overwrites a UI edit made after the scaffold was written | TLA I11, Lean F4/F5 | MED -- **fixed** (without a baseline a difference is a `conflict`; the scaffold records `_keboola.base_config_hash`; pull keeps such a file) | `test_e_adopted_scaffold_push_does_not_overwrite_remote_edit` + `tests/test_sync_adopted_files.py` |
| F | A push aborted by `ENCRYPTION_FAILED` leaves the manifest unsaved; the retry duplicates the change(s) the aborted push already applied | TLA I1b (model trace only; replayed live for this pilot) | MED -- **fixed** (push encrypts every secret before its first write, so the abort writes nothing) | `test_f_aborted_push_does_not_duplicate_already_created_config` + `tests/test_sync_push_encrypt_first.py` |
| G | `sync push` deletes remote configs with no `--force`; the CLI help text says `--force` gates deletion (soft delete to trash since 0.89.0, restorable) | Spec S1, Lean F1, TLA I2b | MED -- **fixed** (push deletes only with `--force`) | `test_g_push_without_force_does_not_delete_remote_config` |
| H | A config deleted remotely by another actor is silently re-created by the next push, no warning | Spec S2, Lean F2, TLA I11b | MED -- **fixed** (diff reports `remote_deleted`, push skips it) | `test_h_push_does_not_silently_resurrect_deleted_config` |
| I | Moving a config's directory by hand (`mv`/`git mv`) is seen as remote DELETE + CREATE under a new id | Lean F3 | LOW-MED | `test_i_moving_config_dir_is_not_delete_plus_create` |
Expand All @@ -120,11 +120,11 @@ and is deleted only by `--theirs`. That is what the I2 (sweep half), I5 and I8
TLC/Lean rerun still reports them until the model's `Pull` is updated to match.
Their tests are ordinary regression guards now.

E, F and I reproduce on current code and are `xfail(strict=True)` (D, G and H
I reproduces on current code and is `xfail(strict=True)` (D, E, F, G and H
are fixed) -- flipping to a hard failure the moment a fix lands is the point:
delete the `xfail` marker to adopt the fix. F's test now reaches the aborted
delete the `xfail` marker to adopt the fix. F's test reaches the aborted
create through a promote push, because push no longer re-creates the
remote-deleted config its first version used. J reproduces and
is `xfail`. K is deliberate, documented behavior, so it is an ordinary
remote-deleted config its first version used; since the F fix the aborted
push creates nothing. J reproduces and is `xfail`. K is deliberate, documented behavior, so it is an ordinary
(unmarked) regression guard instead. B is fixed: its marker was removed
and the test is now an ordinary regression guard.
7 changes: 6 additions & 1 deletion plugins/kbagent/agents/keboola-expert.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,12 @@ its absence is NOT a promise the entry is version-independent (see §1 Rule 6).
branch's tree -- `sync pull` to re-target, never push. Since 0.96.1 (#792)
`sync push` deletes only with `--force` (else `skipped_deletions`), and a
`- REMOTE DELETED` diff line = deleted on the remote, push never re-creates
it: `sync pull`, or `config restore` to keep it. `sync status`
it: `sync pull`, or `config restore` to keep it. Since vNEXT (#792) a
`CONFLICT` on a file that carries a config id but has no manifest entry (a
lost manifest, a `config new --push --output-dir` scaffold of an older
version) means "no baseline": keep the edit aside, `sync pull --theirs`,
apply the edit again, push. An `ENCRYPTION_FAILED` push wrote nothing: fix
the cause and run the same push again. `sync status`
is local-only -- audit real drift with `sync diff`. On <= 0.90.1 a
`~ REMOTE MODIFIED ... codes changed` on a config nobody touched is usually
PHANTOM (issue #686: push stamped the baseline from disk); fixed in 0.91.0 --
Expand Down
Loading
Loading