Port Sync Playbook
July 15, 2026 · View on GitHub
Doc status: Rust port-specific (no upstream equivalent). Documents the Rust port lifecycle: parity verification, sync workflow, and port history.
How to keep flowmark-rs in sync with the Python flowmark source, maintain exact parity, and fix issues as they are discovered.
Purpose
This document is the operational guide for syncing with Python upstream, porting new features, fixing parity issues, and maintaining the test mapping. Read this before starting any sync or parity work.
For general build/test/lint instructions, see docs/development.md.
For the full port overview and release status, see
docs/port-status.md.
This document is flowmark-rs-specific (fixtures, mapping files, local scripts). For reusable process, use the canonical playbook docs first:
../repos/rust-porting-playbook/playbooks/python-to-rust-sync-release-workflow.md../repos/rust-porting-playbook/playbooks/port-checklist-update-template.md../repos/rust-porting-playbook/playbooks/auto-sync-agent-prompt-template.md
For a full manual validation run (automated tests + tryscript sanity review + docs/help equivalence checks), use: tests/qa/rust-python-parity-e2e.qa.md.
Background & Porting Methodology
flowmark-rs was built using the
rust-porting-playbook, available as a
git submodule at repos/rust-porting-playbook/. The playbook defines the methodology,
principles, and patterns that govern all work on this port.
Essential reading
Before doing any porting, parity, or maintenance work, read or load these documents:
| Document | What it covers | Priority |
|---|---|---|
| Porting Principles and Anti-Patterns | 8 non-negotiable principles for agent-driven porting, parity definition, active pursuit, test discipline, disparity handling | Must read |
| Python-to-Rust Porting Rules | Type mappings, module structure, dependency equivalences, pitfalls (regex anchoring, string indexing, comrak arena pattern) | Must read |
| Test Coverage for Porting | Test strategy, coverage targets, fixture organization, cross-validation | Must read |
| Python-to-Rust CLI Porting | argparse→clap mapping, SIGPIPE handling, exit codes, I/O parity | Read for CLI work |
| Rust General Rules | Edition 2024 changes, ownership patterns, error handling, string safety | Read for Rust coding |
| Rust CLI App Patterns | CLI project structure, logging, progress, config management | Read for CLI work |
| Rust Project Setup | Cargo.toml, CI/CD, lint config, release workflow, security auditing | Read for infra work |
Reference documents
For deeper context on the porting methodology and decision history:
| Document | What it covers |
|---|---|
| Python-to-Rust Playbook | The complete porting process from assessment through ongoing sync |
| Sync Release Workflow | Two-stage release refresh: Rust-only stabilization, then upstream sync |
| Code Review Checklist | Rust code review checklist for ports |
| Mapping Reference | Comprehensive type/project/dependency mapping tables |
| Test Coverage Playbook | Pre-port test coverage strategy and tooling |
Flowmark case study
The playbook includes a detailed case study of this project’s port at
repos/rust-porting-playbook/case-studies/flowmark/, covering library evaluation,
technical decisions, cross-validation results, comrak workarounds, and the custom
wrapping algorithm.
Key principles (summary)
The Porting Principles document defines 8 non-negotiable rules. Each was learned from an actual mistake during agent-driven porting:
- Parity definition must be crisp: never redefine scope without approval
- Agents must actively pursue parity: every gap is a blocker, not a note
- Tests must always run in CI: no orphaned test files
- Tests must never hide failures: no massaging, truncating, or weakening
- Fix the process, not the test: when tests fail, fix the code or CI
- Environment dependencies must be explicit: CI installs everything
- Ignored tests must be tracked: every
#[ignore]needs a reason and issue - Disparities must be tested before fixed: write failing test first, then fix
Repository Layout
Both upstream repos are git submodules under repos/:
repos/
├── flowmark/ # Python flowmark source (pinned to release tag)
└── rust-porting-playbook/ # Porting methodology and guidelines
The current Python version is tracked in Cargo.toml:
[package.metadata.parity]
version = "0.7.0"
Version Convention
Each release documents which Python version it targets, and dev builds include commits-ahead and git hash metadata:
flowmark 0.2.5-dev.
+g (Rust port of flowmark-py 0.7.0; base v0.2.5)
The Rust version follows its own semver independently. The port note indicates which Python version’s behavior is fully covered.
Initial Setup
First thing every session: initialize submodules. Most sync work fails early without them and the failure modes are confusing (missing
repos/flowmarkpaths, empty fixtures, “playbook not found”).
git submodule update --init --recursive
Checking for Upstream Updates
To check whether the Python upstream has a newer release than the current parity target, run:
BASELINE=$(grep -A1 '\[package.metadata.parity\]' Cargo.toml | grep version | sed 's/.*"\(.*\)"/\1/')
git -C repos/flowmark fetch --tags >/dev/null 2>&1
LATEST=$(git -C repos/flowmark tag -l 'v[0-9]*' | sort -V | tail -1)
echo "Current parity target: v${BASELINE}"
echo "Latest upstream tag: ${LATEST}"
If LATEST is greater than v${BASELINE}, run a Mode B sync (below).
Otherwise, no sync work is needed.
Sync Process (Mode B: Upstream baseline change)
Use this section when updating from one published Python release to a newer one (for
example, v0.6.5 -> v0.7.0).
Canonical checklist translation for flowmark-rs
The reusable playbook checklist uses generic names. For this repo, use these concrete equivalents:
| Canonical checklist item | flowmark-rs equivalent |
|---|---|
scripts/sync-from-python.sh | Update repos/flowmark submodule tag + copy tests/testdocs fixtures |
scripts/validate-parity.sh | cargo test --all-features, FLOWMARK_PARITY_PYTHON=1 cargo test --test test_parity_cross_binary, scripts/corpus-parity-check.sh |
[package.metadata.python_source] | [package.metadata.parity] in Cargo.toml |
docs/version-history.md / docs/python-sync-log.md | docs/sync-artifacts/*.md, README.md “Last sync”, docs/port-status.md |
test-fixtures/ | tests/testdocs/ and tests/parity/ |
End-to-end runbook
-
Declare baseline and target
BASELINE=$(grep -A1 '\[package.metadata.parity\]' Cargo.toml | grep version | sed 's/.*"\(.*\)"/\1/') TARGET="0.X.Y" echo "Syncing Python baseline v${BASELINE} -> v${TARGET}" -
Create a diff artifact (required before coding)
mkdir -p docs/sync-artifacts ARTIFACT="docs/sync-artifacts/$(date +%F)-sync-v${BASELINE}-to-v${TARGET}.md" -
Update Python submodule and summarize upstream changes
cd repos/flowmark git fetch --tags git checkout "v${TARGET}" git log --oneline "v${BASELINE}..v${TARGET}" git diff --name-status "v${BASELINE}..v${TARGET}" cd ../.. -
Sync fixtures, the runtime skill mirror, and generated README inputs
cp repos/flowmark/tests/testdocs/testdoc.orig.md tests/testdocs/ cp repos/flowmark/tests/testdocs/testdoc.expected.*.md tests/testdocs/ python3 scripts/sync_skill_mirror.py ./scripts/generate_rust_readme.pyThe generator pulls
last_sync_datefromdocs/port-status.mdandparity_versionfromCargo.toml, so update those before running it. It also verifies thatsrc/skills/is byte-identical to the complete upstream skill bundle, except for the Rust-onlymod.rsimplementation. The Rust repository does not publish a separate rootskills/flowmark/bundle. -
Triage mapping gaps early (temp manifest)
cd python TMP_PYTHON=$(mktemp) uv run flowmark-dev discover-python --local-path ../repos/flowmark --output "$TMP_PYTHON" uv run flowmark-dev check-mapping \ --python-yaml "$TMP_PYTHON" \ --rust-yaml ../admin/port-coverage-mapping/rust-tests.yaml \ --mapping-yaml ../admin/port-coverage-mapping/test-mapping.yaml || true cd ..This gives an immediate list of new/changed upstream tests to port or classify.
-
Port behavior and tests, then refresh checked-in mapping files
cd python uv run flowmark-dev discover-python --local-path ../repos/flowmark uv run flowmark-dev discover-rust uv run flowmark-dev init-mapping uv run flowmark-dev check-mapping cd ..For each
missingmapping entry:- add/update Rust tests and set
status: mapped, or - set
status: excludedwith explicitnotes:rationale.
- add/update Rust tests and set
-
Update all parity version references
Cargo.toml[package.metadata.parity].version.github/workflows/ci.ymlFLOWMARK_PY_VERSIONpython/src/flowmark_dev_tools/cli.pyDEFAULT_REFREADME.mdlast sync linedocs/port-status.mdparity target/status textpython/tests/test_smoke.pyexpected discovery counts when they change
-
Run full validation gates
cargo fmt --all -- --check cargo build --locked --all-features cargo clippy --locked --all-targets --all-features -- -D warnings cargo test --locked --all-features FLOWMARK_PARITY_PYTHON=1 cargo test --locked --test test_parity_cross_binary cd python uv run pytest tests/test_smoke.py -q uv run flowmark-dev check-mapping cd .. ./scripts/generate-parity-golden.sh cargo build --release ./scripts/corpus-parity-check.sh -
Commit and release
git add repos/flowmark src/skills/ Cargo.toml .github/workflows/ci.yml README.md docs/port-status.md tests/testdocs/ tests/parity/ python/ admin/port-coverage-mapping/ git commit -m "sync: update Python source to v${TARGET}"Then follow
docs/publishing.mdto cut the Rust release.
Sync gotchas (lessons folded back from past syncs)
Recurring traps this runbook’s steps don’t otherwise surface.
Recorded as playbook observations in
repos/rust-porting-playbook/case-studies/flowmark/flowmark-sync-observations-v0.7.2.md.
- Re-run the doc generators after the submodule bump.
scripts/generate_rust_readme.pyembeds upstream shared-doc text; when upstream rewrites that text or adds a__FLOWMARK_VERSION__-style placeholder, the generator fails (anchored-string or leftover-placeholder). It needs Python 3.14 (uv run --python 3.14 --script), so it only fails in the “README generation sync” CI job unless you run it locally. - Line endings:
.gitattributesenforces LF. The binary embedsSKILL.mdviainclude_str!; without LF enforcement a Windows checkout (CRLF) breaks\n-anchored assertions — silently, Linux-green. Keep.gitattributes(* text=auto eol=lf). - Public-API change vs.
the published crate. If the sync changes the public Rust API, check the published
baseline (
cargo search flowmark) and runcargo semver-checksearly. A breaking change against an already-published version needs a maintainer call: bump the version, or allow the specific lint inCargo.toml([package.metadata.cargo-semver-checks.lints]). - Smoke counts change every sync.
python/tests/test_smoke.pyasserts hard-coded python/rust/mapping counts; update them afterdiscover-python/discover-rust. - Corpus gate needs the external corpus.
scripts/corpus-parity-check.shreadsattic/test-docs, which is not checked in. When absent, substitute a repo-Markdown spot-check (Rust vs Python overdocs/**, default/semantic/auto) and say so. - Port new golden suites for new CLI surfaces. When upstream adds tryscript
scenarios (e.g. for a new flag), port them; prefer asserting installed content +
location, not just
test -fexistence. - Record observations at the end. Per the playbook’s Codified Auto-Sync Process
(step 9) and closure loop, record categorized observations and a
_meta/playbook-improvement-log.mdentry — do not end the sync without them.
Test Mapping and Parity Verification
The admin/port-coverage-mapping/ directory contains a CI-enforced system that tracks provenance between every Python test and its Rust counterpart. This ensures that when Python upstream adds or changes tests, we know exactly which Rust tests correspond and whether any are missing.
See the full spec for design rationale.
How it works
Three YAML files form the mapping system:
| File | Role | Generated by |
|---|---|---|
admin/port-coverage-mapping/python-tests.yaml | All Python test functions (347 entries from v0.7.0; 323 mapped + 24 excluded) | flowmark-dev discover-python |
admin/port-coverage-mapping/rust-tests.yaml | All Rust test functions (408+ entries) | flowmark-dev discover-rust |
admin/port-coverage-mapping/test-mapping.yaml | Hand-maintained Python→Rust mapping with status | Manual / agent edits |
Discovery procedure
Python test discovery (flowmark-dev discover-python):
- AST-parses the Python flowmark repo at the pinned release tag
- Extracts every
test_*function, classifies by type (unit, integration, golden, infrastructure) - Writes
python-tests.yamlwith idempotent merge, hand-added entries survive re-generation
Rust test discovery (flowmark-dev discover-rust):
- Runs
cargo test -- --list --format terse(compiler-authoritative) - Resolves file paths and line numbers
- Writes
rust-tests.yamlwith idempotent merge
Mapping maintenance
test-mapping.yaml maps each Python test to one or more Rust tests.
Each entry has a status:
| Status | Meaning |
|---|---|
mapped | Python test has verified Rust counterpart(s) |
excluded | Intentionally not ported (Python-specific infrastructure) |
partial | Rust test covers subset of Python test behavior |
missing | No Rust equivalent yet (CI will fail) |
A single Python test can map to multiple Rust tests (1:N mapping).
For example, test_ellipses maps to 10 separate Rust test functions.
CI enforcement
The check-mapping job in .github/workflows/ci.yml runs after all Rust tests pass.
It:
- Runs 13 smoke tests (
pytest tests/test_smoke.py) that verify YAML round-trip stability, deterministic ordering, discovery counts, and mapping completeness - Runs
flowmark-dev check-mappingwhich validates:- Every Python test has a mapping entry
- Every mapped Rust function exists in
rust-tests.yaml - No entries have
status: missing
- Fails the build if any check fails
Updating after a Python upstream release
When Python flowmark releases a new version with new or changed tests:
# 1. Update the Python submodule to the new tag
cd repos/flowmark && git checkout v0.X.Y && cd ../..
# 2. Re-discover Python tests (new tests appear in python-tests.yaml)
cd python
uv run flowmark-dev discover-python --local-path ../repos/flowmark
# 3. Re-discover Rust tests (after implementing new test counterparts)
uv run flowmark-dev discover-rust
# 4. Initialize mapping for any new entries (adds them as status: missing)
uv run flowmark-dev init-mapping
# 5. Check mapping, will fail until all new tests are mapped
uv run flowmark-dev check-mapping
cd ..
For each new missing entry, either:
- Port the corresponding test to Rust and set
status: mapped - Set
status: excludedwith anotes:field explaining why
CI will not pass until every Python test has a non-missing status.
Test Fixture Files
| File | Role | Tracked |
|---|---|---|
tests/testdocs/testdoc.orig.md | Input fixture (from Python) | Yes |
tests/testdocs/testdoc.expected.*.md | Expected golden outputs (from Python) | Yes |
tests/testdocs/testdoc.actual.*.md | Actual outputs (generated by cargo test) | No (gitignored) |
Updating the Playbook
The porting playbook itself is also a submodule. To update it or push improvements back upstream:
cd repos/rust-porting-playbook
git checkout main
git pull
# Make edits, commit, push
cd ../..
git add repos/rust-porting-playbook
git commit -m "sync: update porting playbook"
Markdown Formatting
All docs are auto-formatted with flowmark. Run locally:
FLOWMARK_PY_VERSION=$(grep -A1 '\[package.metadata.parity\]' Cargo.toml | grep version | sed 's/.*"\(.*\)"/\1/')
uvx "flowmark@${FLOWMARK_PY_VERSION}" --auto --extend-exclude "tests/" --extend-exclude "attic/" --extend-exclude ".claude/" --extend-exclude "python/" .
git checkout -- README.md
This also runs in CI as a non-blocking check.