Skip to content

fix(sync): converge generated files in one run and release them when their tool is off - #341

Merged
JacksonFergusonDev merged 3 commits into
mainfrom
feat/retract-generated-files
Sep 25, 2026
Merged

JacksonFergusonDev merged 3 commits into
mainfrom
feat/retract-generated-files

Conversation

@JacksonFergusonDev

@JacksonFergusonDev JacksonFergusonDev commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Summary & Motivation

This PR fixes two leftover problems in how generated text files (justfile, Dockerfile) are reconciled. Both live in Reconciliation._write_generated.

  1. A generated file catches up one sync late. When a region inside the justfile stopped being declared, the generated writer skipped the whole file for that run and left the region step, which runs later, to retract the region. If the justfile's generated text also changed in that run, the change only landed on the next sync. So sync --check failed once in CI right after a real sync. feat(templates): typed template options that gate content through requires #338 noted this as a known limitation.
  2. A generated file outlives its tool. Turning just or Docker off made _write_justfile / _write_docker_artifacts return early, so justfile and Dockerfile stayed on disk and in the lock forever. This is the same hole feat(sync): retract structured documents nothing declares any more #340 closed for structured documents.

Planning & Key Decisions

The generated writer retracts omitted regions itself. It no longer skips the file. It runs the region retraction (append_marker_blocks with no payloads and the omitted regions' baselines) over the local text first, then merges:

  • Unedited omitted region: removed from the file and cut from the whole-file baseline.
  • Region the user already deleted: forgotten.
  • Region settled in this run: taken or kept.

Region ownership leaves the record, so the region step later finds nothing left to do, and the file converges in one run.

A region kept with local sits out the merge. It's the user's now, so it must not be owned text in the baseline. But if it stays in the local text during the diff3 merge, it sits right after the last generated line. When that line changes, the adjacent hunks overlap and create a spurious conflict; the first version of this change hit exactly that in a test. So the writer detaches the kept regions before the merge and appends them afterwards, the way a new region is appended.

Only a pending decision holds the file. While an edited omitted region has no resolution yet, the writer leaves the file alone and the region step reports the retracted conflict, as before. The file regenerates in the run that settles it. Regenerating around an open decision would have meant writing the region into the result while also reporting it. A target where Protostar owns only regions (the regions policy) is still left to the region step.

Cutting a region from the baseline doesn't compare it. The whole-file baseline and a region's own baseline can drift apart: a region can update on its own while the whole-file merge conflicts. So removing a region from the text baseline is now appends.cut_regions, which removes by marker. It no longer runs the three-way region merge against a baseline that might not match. The region step's existing baseline cut uses it too.

Undeclared generated files are released the way seeds are, at file level. EnvironmentManifest.generated_files() lists what a tool generates this run; target_files now builds on it. Reconciliation._release_undeclared_generated takes each owned text-policy record that no tool generates and no declared region targets:

  • Text matches the whole-file baseline: the file is deleted.
  • Already deleted: the record is forgotten.
  • Edited: a retracted conflict for the whole file, where local keeps it as the user's and desired deletes it.

Unlike #340's structured documents, generated text has no units to keep apart. A justfile reduced to hunks around the user's edits would be a fragment, so the decision stays whole.

A file that still receives a declared region stays declared. If a template appends to the justfile without requires = "just", the generated text stays while that region is declared, matching #340's "declared while a producer contributes to it". Retracting only the generated text and turning the file into a regions-only file would have been a policy change of its own.

Architectural Invariants

  • A generated file converges in the run that drops a region. _write_generated settles omitted regions before it merges. Only an unsettled edited region defers regeneration.
  • A region Protostar lets go of is never owned text. It is cut from the whole-file baseline by marker, and a kept one is merged as the user's content.
  • A generated file nothing declares is released whole. An owned text record outside generated_files() and the declared region targets is deleted when unedited, forgotten when gone, and otherwise one file-level retracted decision.

Testing

  • test_generated_execution.py:
    • a dropped region plus a changed generated text converge in one run, and the next run touches nothing
    • an edited omitted region holds the file and reports one retracted conflict with no edits
    • settling it with local or desired regenerates in that run, with the kept region appended and later runs asking nothing
    • Docker switched off deletes the Dockerfile
    • a justfile still receiving a region keeps its generated text
  • test_lifecycle.py, with just switched off:
    • an unedited justfile is deleted
    • one already deleted is forgotten
    • an edited one is retracted, and local or desired keeps it as the user's or deletes it
  • The apply_generated harness now declares the path it generates, because it writes through a patched writer without turning a tool on.
  • The full suite, strict docs build, and snapshot checks pass on the pre-push hooks.

When a region inside a generated file (such as a template's justfile
recipes) stopped being declared, the generated writer skipped the whole
file for that run and left the region step to retract it, so a changed
generated text only landed one sync later and sync --check failed once
right after a real sync.

The writer now retracts omitted regions itself before merging: unedited
ones are removed and cut from the whole-file baseline, deleted ones are
forgotten, and settled ones are taken or kept. A region kept with its
edit is the user's, so it sits out the three-way merge and is appended
after it. Only a region still waiting for its decision holds the file.
Turning just or Docker off left justfile and Dockerfile behind, owned

forever: their writers return early when the tool is off, the same hole
#340 closed for structured documents.

An owned generated file that no tool generates and no declared region
targets is now released the way a seed is: deleted when its text matches
the whole-file baseline, forgotten when already deleted, and otherwise a
retracted conflict for the whole file, whose local choice keeps it as the
user's and desired deletes it.
@JacksonFergusonDev
JacksonFergusonDev merged commit 5dee666 into main Sep 25, 2026
28 checks passed
@JacksonFergusonDev
JacksonFergusonDev deleted the feat/retract-generated-files branch September 25, 2026 22:59
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