Skip to content

test(sync): formal verification pilot of the sync engine (#792) - #793

Open
padak wants to merge 1 commit into
mainfrom
feat/792-formal-sync-pilot
Open

padak wants to merge 1 commit into
mainfrom
feat/792-formal-sync-pilot

Conversation

@padak

@padak padak commented Sep 26, 2026 •

Copy link
Copy Markdown
Member

Summary

Pilot for #792: formal verification of the sync engine (pull / diff / push).

  • formal/sync/tla/: TLA+ model checked with TLC (2 branches, 4 configs, ignorable component, ~153k states per run).
  • formal/sync/lean/: Lean 4 model of the pure decision functions: 35 theorems, 0 sorry, standard axioms only. False properties are kept as proven negations with concrete counterexamples.
  • tests/test_sync_formal_counterexamples.py: one test per finding, replayed against the real SyncService with a mocked client. Reproduced bugs are xfail(strict=True). A fix PR removes the marker.

The models are documentation, not a CI gate. See formal/sync/README.md for how to run them.

Findings (all reproduced against real code except K)

ID Finding Severity
A Remote delete + recreate under the same name: pull's stale sweep deletes the dir it just wrote, and the next push DELETEs the live new config (found independently by Lean and TLC) HIGH
B pull / pull --force compare only _config.yml: edits in transform.sql / code.py / _description.md are silently overwritten, with no SYNC_CONFLICT HIGH
C Remote delete + local edit: pull deletes the locally edited dir without a conflict HIGH
D sync push --branch dev (promoting main/) creates another dev copy of each prod config dev lacks, on every push HIGH
E An untracked file carrying a config id (scaffold / adopted) is compared 2-way, so push overwrites UI edits MED
F After a push aborted with ENCRYPTION_FAILED, the retry duplicates configs the aborted push already created MED
G sync push deletes remote configs without --force, although the help text says --force gates deletion (deletes are soft, to trash) MED, product decision
H A config deleted remotely by another actor is silently re-created by the next push MED
I mv of a config folder becomes remote DELETE + CREATE under a new id LOW-MED
J pull --branch dev reports untouched prod configs as "removed" LOW
K A cosmetic local edit blocks pull from applying remote changes. This is documented, conservative behavior, so it is kept as a regression guard not a bug

No production code changes in this PR. Fixes follow in separate PRs.

Test plan

  • pytest tests/test_sync_formal_counterexamples.py: 1 passed, 10 xfailed
  • ruff check / ruff format --check clean; lake build OK

Devin Review

TLA+ model (TLC) and Lean 4 model of the sync pull/diff/push decision
logic, plus one pytest per consolidated finding (A-K) replayed against
the real SyncService. Reproduced bugs are strict xfail tests; remove the
marker when the fix lands. Models are documentation, not a CI gate.

@keboola-pr-reviewer-bot keboola-pr-reviewer-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: auto_approve (risk 1/5) · profile _default

Test-and-docs-only formal-verification pilot with zero production runtime changes; safe to auto-approve.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

Devin Review

Comment on lines +26 to +29
java -XX:+UseParallelGC -cp ~/tools/tla/tla2tools.jar tlc2.TLC -workers auto -deadlock \
-dumpTrace json $N.json -metadir "states_$N" -config $N.cfg SyncEngine.tla > $N.log 2>&1 || true
rm -rf "states_$N"
grep -E "violated|No error|states generated|distinct states|Error:" $N.log | head -5

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Model-checker errors return success

When TLC fails before checking an invariant, run_one.sh discards its exit status. The final log-filter pipeline still returns zero, so verification runs can pass without checking the model.

Learn more

TLC uses a nonzero status for invariant violations as well as tool and model errors. The runner currently ignores all such statuses, then returns the status of head, which succeeds even if the log contains an error or no result. That makes a missing jar, an invalid model, or an incomplete run indistinguishable from a completed check to a caller using the script's exit status.

Example: With an invalid tla2tools.jar path, Java writes an error to the log and exits nonzero. run_one.sh prints the error but exits zero, so run_all.sh can finish successfully without running TLC.

Recommended fix: Preserve TLC's status and explicitly classify its log as a completed proof, an expected invariant counterexample, or an execution/model failure. Return nonzero for the last case, and propagate it from run_all.sh.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +172 to +173
{ [kind |-> "deleted", id |-> id, path |-> st.mn[id].path, trk |-> TRUE,
comp |-> st.mn[id].comp, base |-> st.mn[id].base] : id \in Deleted(st, b) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Deletion invariant assumes a path absent from real diffs

I2_DeleteOnlyUserRemoved checks deleted changes against userDel using their path. Real compute_changeset emits those changes with path="". The model therefore tests a richer change record than the service receives; keep the manifest path as separate ghost state if this invariant needs it.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +435 to +438
sql_client_pull("SELECT 100;") # remote changed the SQL in the meantime
w.svc.pull(alias="prod", project_root=w.root)

assert "SELECT 42" in sql_file.read_text(), (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Force-pull SQL behavior remains untested

The companion-file finding covers plain pull and --force, but the regression test calls only plain pull. Because --force has a separate conflict guard, add a case that exercises it after local and remote SQL edits.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants