Skip to content

Document forge's migration/stability contract with grid's duplicated in-tree copy #3

Description

@jordigilh

Summary

Forge's full ~16K-line source tree is duplicated verbatim inside praxis-proxy/grid's in-tree forge/ Cargo workspace member. That copy is deliberately kept in place — per grid#30 ("Extract Forge into the standalone praxis-proxy/forge repository"), the recommended delivery plan explicitly defers removal: "Remove the in-tree crate only after all consumers pass parity validation" (step 7, "In-tree Forge deprecation and removal"). grid will file its own tracking issue for that removal/convergence side of the work, since they hold the copy being retired. This issue is the forge-side half: while the duplicate exists, what does forge itself commit to (stability, versioning, compatibility) so grid can eventually take it as a real crate/binary dependency instead of a copy — and what keeps the two copies from silently drifting apart in the meantime?

As of today (11 days after the extraction commit, 9abe12b, 2026-08-07), divergence is already accruing:

  • One intentional, documented divergence: this repo's own README.md "How Forge relates to other Praxis repositories" section lists the changes made since extraction, including "Added fsync to kubeconfig and template-file atomic writes" — a real, deliberate behavior difference from grid's copy.
  • One undocumented divergence found while verifying this issue (see Evidence) — a stray .clone() that only exists in forge's copy because grid's workspace lint config picked up clippy::redundant_clone (grid PR #55, 2026-08-13) after the fork point, and forge's own Cargo.toml [lints.clippy] table was never updated to match. Nothing failed CI on either side — the two lint configs just quietly stopped matching.

Neither repository has an automated check that would catch the next non-formatting divergence (a bug fix, a lint-config change, a behavior change) landing in only one copy.

Evidence

Diff methodology: fetched fresh upstream/main for both repos (praxis-proxy/forge@9abe12b and praxis-proxy/grid@2fdaa26), then reformatted both copies of each file with identical rustfmt --edition 2024 settings (no repo-specific config) before diffing, to separate real code drift from max_width line-wrap noise (this repo has no rustfmt.toml, so rustfmt defaults to max_width = 100; grid's root rustfmt.toml sets max_width = 120).

  • Repo-wide size check: wc -l src/**/*.rs on this repo's upstream/main totals 16,590 lines — matches the "~16K lines" scope of the duplication.
  • Two largest files, diffed after common reformatting:
    • src/config/validate.rs (2,241 lines) vs. grid's forge/src/config/validate.rs: zero differences — fully identical logic, formatting-only drift as expected.
    • src/stack/engine.rs (1,824 lines) vs. grid's forge/src/stack/engine.rs: 3 real (non-formatting) diff hunks:
      1. #[expect(clippy::disallowed_methods, reason = "forge is synchronous; capture polling has no async runtime")] — present only in grid's copy. This is a downstream consequence of the already-documented clippy.toml divergence (grid's clippy.toml bans std::thread::sleep; this repo's clippy.toml intentionally does not, per this repo's own README note feat: add OTel observability benchmark demo #2) — expected, not a new finding.
      2. src/stack/engine.rs (function around line 559, write_bytes_atomic-style helper): this repo's copy does File::createwrite_allsync_alldrop → rename; grid's copy does a plain std::fs::write(&tmp, bytes) with no fsync. This is the README's documented "Added fsync to ... template-file atomic writes" divergence — confirmed here at the code level.
      3. read_corefile (src/stack/engine.rs, forge line 714): Ok(output.stdout.clone()) in this repo vs. Ok(output.stdout) in grid's copy — an undocumented divergence. Root cause: grid's root Cargo.toml [workspace.lints.clippy] added redundant_clone = "deny" in grid#55 (2026-08-13, six days after the forge extraction); this repo's own Cargo.toml [lints.clippy] table has never picked up that lint, so the redundant clone was never flagged here.

Risk

  • The two copies have already silently diverged on more than the one documented, intentional change — a lint-config drift produced a real (if harmless) code-level difference within days, with no CI signal on either side.
  • Nothing prevents the same pattern from happening with an actual bug fix or security-relevant change: a fix landed only in forge won't reach grid's in-tree copy (and vice versa) until someone notices by hand.
  • The intended end state (grid depends on forge as a real crate/binary instead of vendoring a copy) has no forge-side contract yet: no documented stability/versioning guarantees, no explicit "this is the compatibility surface grid can rely on" statement.

Suggested fix

This repo's half of the two-sided migration (grid#30 owns the removal side):

  1. Document a stability/versioning contract in this repo (e.g. a COMPATIBILITY.md or a section in README.md) covering what grid (and other consumers) can rely on staying stable across releases: the forge.yaml schema version policy (already versioned as forge.praxis.dev/v1alpha1), CLI subcommand/flag stability, exit-code/output-format (--output json) stability, and state-file (state.json) layout stability.
  2. Cross-reference grid#30 from that document (and from this issue) so the duplication is discoverable as a known, tracked, two-sided migration rather than something a future contributor stumbles onto and assumes is an accident.
  3. Consider lint-config parity as an explicit, tracked follow-up: either sync [lints.clippy] in this repo's Cargo.toml against grid's [workspace.lints.clippy] periodically, or document that they're allowed to diverge (this repo is a standalone sync CLI; grid's workspace lints target async code) and accept the resulting code-level drift as a known cost.
  4. Once a tagged release and stability contract exist, grid can replace its in-tree copy with a real dependency on a pinned forge release/tag — closing the loop described in grid#30's "Recommended Delivery" step 7.

Severity: Medium

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions