Skip to content

Moteur de conflits : accord mesuré avec le merge humain (lots 1/C/E/F/G/D) - #170

Merged
devlint merged 38 commits into
mainfrom
feat/conflict-engine-accuracy
Aug 31, 2026
Merged

Moteur de conflits : accord mesuré avec le merge humain (lots 1/C/E/F/G/D)#170
devlint merged 38 commits into
mainfrom
feat/conflict-engine-accuracy

Conversation

@devlint

@devlint devlint commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Quoi

Le moteur de résolution passe de "résout ce qui est trivial" à "est d'accord avec ce que les humains ont réellement committé" — mesuré, pas affirmé.

Résultat mesuré (corpus v1, v3.8.0 → cette branche) : accord global 41,7 % → nettement au-dessus ; laravel 24,3 → 83,3 %, prettier 25,3 → 49,6 %. Baseline corpus v2 : 59,24 % (391/660), détail par dépôt dans benchmark/results/.

Par lot

  • Lot 1 (A+B+D intérim) : contrat du classifieur appliqué (aucun hunk complex appliqué en silence), invariants de format avec rétractation, déclin par défaut des fichiers générés (resolveGeneratedFiles, raison actionnable).
  • Lot C — merge context : le moteur sait quelle opération est en cours (MergeContext {operation, targetSide} — déclaré, jamais re-dérivé). Règle affinée par son propre gate : target ne gagne que sur les paires de versions non ordonnables ; l'ordonnable reste newest-wins.
  • Lot E : merge key-wise des fragments de manifest JSON (arbitrage de ranges à opérateur identique, ancrage des insertions).
  • Lot F (core+CLI) : conventions du dépôt mesurées sur son propre historique (gitwand conventions) — planchers d'évidence (≥5 échantillons, ≥80 %), précédence explicite > convention > défaut, provenance dans chaque raison. Surface desktop différée par le gate (circularité corpus).
  • Lot G : gate CI (.github/workflows/benchmark-gate.yml) — toute PR touchant le moteur est comparée à la baseline (chute d'accord > 1,5 pt global ou > 5 pts/dépôt = rouge). Vérifié dans les deux sens.
  • Corpus v2 re-épinglé sur historique de merges mesuré (+symfony, git/git, bootstrap ; sélection documentée).
  • Lot D — regenerate tier (complet, plan + suivi) : pour les fichiers générés déclinés (lockfiles), le moteur émet désormais un RegenerationPlan pur (aucune exécution côté core) ; le CLI l'exécute dans un git worktree jetable, sandboxé, opt-in (--regenerate / .gitwandrc), avec suppression des scripts d'installation non-négociable et allowlist d'env (jamais de secrets/tokens transmis à l'installeur). MCP expose la même info en lecture seule (aucun outil MCP n'exécute quoi que ce soit). Un follow-up a corrigé un vrai bug (le worktree se semait depuis HEAD seul, invisible aux fichiers theirs-only) et mesuré l'accord réel après correction, sur 3 tentatives (les 2 premières ont elles-mêmes révélé des bugs du harnais de mesure, corrigés en cours de route) :
    5/13 = 38,5 % d'accord sur prettier/prettier (yarn-berry), n=13, mesure conclusive (p ≈ 1,2×10⁻³ contre la barre des 80 %). Verdict : objectif (≥80 %) non atteint → le tier reste opt-in CLI, la surface desktop n'est pas justifiée par cette preuve. Détail complet, historique des 3 tentatives et limites connues dans benchmark/README.md (§ "Regenerate-tier replay").

Notes

  • Tests d'intégration git : env hermétique + timeout 30 s par test (XProtect macOS).
  • Spec : docs/superpowers/specs/2026-08-26-conflict-engine-accuracy.md.
  • Plans : docs/superpowers/plans/2026-08-26-regenerate-tier.md et docs/superpowers/plans/2026-08-27-regenerate-tier-followup.md.

Vérification

pnpm -r build && pnpm -r test verts en local ; gate CI actif sur cette PR même.

Laurent Guitton and others added 14 commits August 26, 2026 13:07
…ecline on generated files

Lot 1 of docs/superpowers/specs/2026-08-26-conflict-engine-accuracy.md — the
three changes that stop the engine being confidently wrong, measured against
what teams actually committed (benchmark/).

A — classifier contract. A complex hunk resolved by a format-aware resolver is
reclassified `format_semantic` (new ConflictType), keeping the classifier's
dimensions and boosters and adding the resolver's, then submitted to the same
confidence threshold and policy gates as every pattern (semantic merges combine
both sides — policies that exclude non_overlapping exclude them too, like the
imports resolver already did). A non-complex hunk applied via a format resolver
keeps its type but its confidence now records the semantic validation instead
of silently bypassing the threshold. No file can come back fully resolved while
reporting `complex` hunks — on laravel that was 116 of 325 files, applied with
no confidence score and no trace.

B — format invariants. validateMergedContent now checks what syntax cannot see:
a changelog with two Unreleased sections or duplicated version headings, a JSON
object with duplicate keys (scanner is string-safe and per-object; YAML dups
were already caught by the yaml parser). Violations retract every auto-applied
hunk in the file, same mechanism as the v2.4 parse-tree retraction.

D (interim) — generated files decline by default. Their committed version is a
tool's output, not a merge of two texts: "accept theirs" and semantic lockfile
merges both diverged from what teams shipped in ~100% of measured cases. The
engine now declines with an actionable reason (resolve the source, re-run the
installer/build); only patterns that fabricate nothing (same_change,
one_side_change, delete_no_change, whitespace_only) still apply. The historical
behaviour sits behind a new option, resolveGeneratedFiles (default false), and
generated_file moves from the "trivial" tier to "unresolved".

Measured before → after on the pinned corpus (files resolved end-to-end,
byte-identical to the human merge):

  laravel/framework   325 files 24.3%  →  216 files 36.6%
  prettier/prettier   179 files 25.3%  →  100 files 45.0%
  vuejs/core          226 files 92.5%  →  220 files 95.0%
  expressjs/express    47 files 59.6%  →   49 files 59.2%

Coverage drops by design — those resolutions were ones the product's own
contract said it should not have made. The remaining laravel disagreements are
now Application.php version scalars and composer.json — exactly the merge-
context problem (direction-aware value_only_change), which is lot C.

Tests: 1078 passing. Lockfile/value_only suites exercise their resolvers behind
the opt-in; corpus fixtures F11/F13/F14 now expect decline; the golden-funnel
snapshot was regenerated on purpose (complex 20 → 14, format_semantic 6,
autoResolved 31 → 28 on the fixture corpus).

Not in this lot: desktop/CLI surfacing of resolveGeneratedFiles (settings +
5 locales), and website/reference/config.md documentation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQoTe6RE4JpoQknCASS3hu
The engine-level default (generated files decline) needs a switch at the right
scope. Whether a team regenerates or merges its lockfiles is a repository
convention, not an app preference — so the opt-in lives in .gitwandrc
(resolveGeneratedFiles: true, parsed strictly as a boolean), flows to the
desktop through the existing rc loading in useGitWand, and gets a CLI flag
(--resolve-generated) for hooks and CI. No SettingsPanel toggle on purpose.

reference/config.md gains a Generated Files section: what is detected, why the
default declines (with the benchmark link), which fabricate-nothing patterns
still apply, and both opt-in forms.

cli: 34 tests green; core tsc + desktop vue-tsc clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQoTe6RE4JpoQknCASS3hu
The largest measured source of disagreement left after lot 1 is
value_only_change guessing 'newer semver wins' on back-merges (laravel: ~112
wrong resolutions; the human answer is always the target branch's value). The
engine can't know that without knowing what merge it is in — this plan adds an
optional MergeContext to GitWandOptions, detection helpers on the callers'
side (CLI/MCP read .git state, the desktop already knows its operation), the
target-wins rule for version-like scalars, and demotes the context-less version
guess from auto-apply to propose.

Task 5 is the gate: the benchmark re-run must show agreement improving on at
least two repos before the desktop wiring ships.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQoTe6RE4JpoQknCASS3hu
…what merge it is in (lot C)

New optional GitWandOptions.mergeContext ({ operation, targetSide, oursRef?,
theirsRef? }), plain data, detected by the callers and echoed in traces — the
core stays a pure function.

The rule, refined once by the benchmark itself:
- An UNORDERABLE version pair ('13.x-dev' vs '12.54.1' — the file's version
  identity) resolves to the target side when context is present, and is
  proposed instead of applied when it is not. The old path fell back to
  prefer-theirs: a coin flip measured wrong ~3 times out of 4.
- ORDERABLE semver pairs keep "newest wins" even with context. The first
  version of this rule sent them to the target too, and agreement regressed on
  prettier (45.0 → 39.0), vue and express — teams do take the newer dependency
  brought by the source branch. The gate in the plan (task 5) caught it before
  anything shipped.

Measured, same pinned corpus, files byte-identical to the human merge:

  laravel/framework   36.6%  →  81.9%   (the Application.php class of failures)
  prettier/prettier   45.0%  →  45.0%
  expressjs/express   59.2%  →  59.2%
  vuejs/core          95.0%  →  90.0%*

  * denominator artefact, not a regression: a per-file flip scan found ZERO
    files where the previous engine agreed and this one doesn't. Fixing the
    version hunk pulls previously-excluded files into the comparable set,
    where other hunks disagree — all 11 in one merge, dominated by a
    workspace:* migration done by hand during the merge.

Detection: detectMergeContext() reads .git state (MERGE_HEAD, rebase-merge/
rebase-apply + head-name, CHERRY_PICK_HEAD, REVERT_HEAD), covers linked
worktrees, returns null when nothing is in progress. Implemented in the CLI
(tested on real temp repos: merge, rebase, cherry-pick, worktree, clean, non-
repo) and duplicated in the MCP on purpose — mcp must not depend on cli, and
core stays Node-free. targetSide is declared by the caller in all cases, so
the engine never re-derives the rebase ours/theirs inversion; rebase-merge/
head-name is the branch being REBASED (theirs), which the desktop mapping now
gets right against its own misleading field name.

Wired: CLI resolve (flag-free, plus a verbose context line), the three MCP
resolve/preview sites, and the desktop batch loader via the existing
git_repo_state command. Tests: core 1084, cli 40, mcp 12, desktop 1058 across
4 shards — all green.

benchmark/README gains the three-state impact table and the refinement story;
plan tasks 1–5 checked, task 6 (corpus fixtures, golden funnel, site docs,
changelog) remains.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQoTe6RE4JpoQknCASS3hu
Corpus gains two context-dependent fixtures — F47, the laravel back-merge shape
resolved to the target when context is present, and F48, the same conflict
without context, pinned as proposed-never-applied. Golden funnel regenerated
for the two new fixtures (46 → 48, autoResolved 28 → 29).

reference/config.md documents mergeContext (auto-detected by all three
frontends; API consumers can pass it), and the CHANGELOG's Unreleased section
tells the whole accuracy arc: format_semantic, invariants, generated files
declining, merge context, and the benchmark that drove — and once corrected —
those rules.

Core suite: 1086 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQoTe6RE4JpoQknCASS3hu
Real package.json / composer.json conflicts are fragments — a few
'"key": value,' lines mid-object — so the full-document JSON resolver never
fired on them and the textual engine merged them line by line: exactly the
wrong granularity, measured at 48-67% agreement.

New resolvers/json-fragment.ts, hooked as the fallback of the dispatcher's
JSON branch: parse each side as simple entries (anything else — nested
multi-line values, blank lines, duplicate keys — declines rather than
guesses), then three-way merge by key. One-sided changes and deletions
resolve; both-sides-different gets a single bounded arbitration: two version
constraints on the SAME operator ('^7.23.0' vs '^7.23.3') resolve to the
newer — what teams ship, per the corpus. Operator changes and workspace:*
migrations are human decisions and fall through. Output preserves each
winning line's original formatting, keeps the fragment's trailing-comma
convention (declines when the two sides disagree on it), and sorts keys only
when both sides were already sorted — npm's own convention.

First change that raises BOTH metrics at once, on all four repos:

                       agreement          files resolved end-to-end
  laravel/framework    81.9% → 83.3%      216 → 245
  prettier/prettier    45.0% → 49.6%      100 → 117
  expressjs/express    59.2% → 61.3%       49 →  62
  vuejs/core           90.0% → 90.1%      220 → 222

Core suite: 1095 green (9 new tests, including the vue @babel/parser shape,
the workspace:* decline, and the no-duplicate-key guarantee that lot 1's
invariant would otherwise retract).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQoTe6RE4JpoQknCASS3hu
… numbers

The accuracy work squatted v3.9/v3.10/v3.11 in code comments while the ROADMAP
reserves those for Live Repo, preview-to-apply and Stacked Branches. Comments
now say 'accuracy lot 1/C/E' (27 files, mechanical), and the ROADMAP gains a
'Next release — Engine Accuracy' section: what shipped on the branch, the
measured table, and the follow-ups (lot F conventions-from-history, lot G
CI gate, lot D full regeneration, corpus re-pin) each flagged as wanting its
own plan.

Two cross-references added where the roadmap already converged on this work
without knowing it: v3.10's confidence threshold (only meaningful now that
format resolvers carry a real confidence) and v4.0's feedback loop (lot F is
its active, measured form — they should share one store).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQoTe6RE4JpoQknCASS3hu
The rebase test hung to vitest's 5s timeout on a real macOS setup while
passing in a clean VM — the temp repos were inheriting the host's global git
config, so a global core.hooksPath (husky), a GPG key waiting for a
passphrase, or a configured editor could stall 'git rebase' silently.

Test git calls now run with GIT_CONFIG_GLOBAL/SYSTEM pointed at /dev/null,
prompts and editors disabled, a local hooksPath override, and a hard 10s
execFileSync timeout so any future hang fails loud instead of timing out
quietly. Production detectMergeContext is untouched — it only runs read-only
commands and should respect the user's real config.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQoTe6RE4JpoQknCASS3hu
… from its own history

The engine's rules are calibrated on a public corpus, and lot C proved
conventions differ per repo. This plan derives them per repository by
replaying its own merge history under candidate rules and scoring against
what the team actually committed — verdicts gated on sample floors, stored
per-clone in .git/gitwand/, always losing to an explicit .gitwandrc, and
carrying provenance into every trace they influence.

Task 5 is the gate again: split-half validation on the benchmark corpus
(derive on the first half of merges, measure on the second) before any
desktop surface ships.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQoTe6RE4JpoQknCASS3hu
…F, core+CLI)

A convention is no longer an assumption: `gitwand conventions` replays the
repo's own merge history (git merge-tree, read-only, capped at 200 merges),
scores candidate rules against what the team actually committed, and emits
verdicts only above evidence floors (>=5 samples, >=80% agreement). Results
live in .git/gitwand/conventions.json — per clone, never committed — and an
explicit .gitwandrc ALWAYS beats a derived convention.

Core: conventions/{types,derive}.ts (pure — observations in, verdicts out,
no git/fs/clock), GitWandOptions.conventions, and three consumptions:
generatedFiles verdict "merge" re-enables auto-resolution when the caller
expressed no choice; "regenerate" keeps the decline; changelog "tool-rebuilt"
declines markdown unions on changelog files. Every influenced resolution
carries provenance in its reason ("convention mesurée sur 16 merges, 100%").
pathPolicies are derived and REPORTED as a suggested .gitwandrc
patternOverrides snippet, never silently applied (v1).

CLI: conventions-runner inside commands/conventions.ts (derive / --show /
--clear / --max-merges / --json), git >= 2.38 guard with a clear error
(merge-tree --write-tree), hermetic-git tests on fabricated histories that
skip cleanly on older git (the dev VM runs 2.34; validated for real on 2.43
in a container: regenerate/tool-rebuilt/prefer-theirs all measured at 100%).

Task-5 gate verdict — recorded in benchmark/README and the plan: split-half
on the corpus is FLAT everywhere (zero regressions, zero gains) because every
derived verdict confirms the engine defaults... which were calibrated on this
very corpus. Circularity, not absence of value: the layer pays off as
provenance, and on repos that diverge from defaults (pinned by unit tests).
Desktop surface deferred per the gate until the corpus re-pin includes
divergent-convention repos.

Tests: core 1109 (14 new), cli 40 + 4 gated (44 on modern git) — green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQoTe6RE4JpoQknCASS3hu
The rebase-detection and conventions-derivation tests spawn 30-40 git
subprocesses each. On macOS every exec goes through XProtect/Gatekeeper
(~100-300ms per process), so vitest's default 5s test timeout is structurally
too short there — the suites run in under a second on Linux and timed out on
a real Mac. The hermetic-env fix from c25fd10 was necessary (global config
must never leak into temp repos) but not sufficient.

Each integration test now carries an explicit 30s budget, distinct from the
hard 10s per-git-call timeout that still catches genuine hangs loudly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQoTe6RE4JpoQknCASS3hu
Corpus v2 — selection is now itself measured. Candidates were probed with
rev-list --merges plus a merge-tree conflict-rate sample over their 60 most
recent merges: kubernetes, rails and godot rejected at 0/60 (merge queues);
symfony (back-merge culture, composer.json in half its conflicted merges),
git/git (integration branches, maintainer-resolved conflicts — the best human
ground truth there is) and bootstrap (adversarial _variables.scss family) come
in. cargo/django dropped for having nothing to replay, and vue dropped DESPITE
being the 92-95% showcase — keeping it would have been flattering rather than
informative.

Current-engine baseline on v2, committed as the CI reference
(results/v3.8.0-corpus2-baseline.json): 1927 merges replayed, 634 conflicted,
5675 hunks — 59.2% of end-to-end-resolved files byte-identical to the human
merge (391/660), per-repo spread 17.5-65.4%.

Lot G — the gate itself:
- compare.mjs: fails when corpus agreement drops >1.5 pts, any repo drops
  >5 pts, or end-to-end coverage collapses >25% (a deliberate decline policy
  must update the baseline in the same PR, reasoning in the commit message).
  Agreement is the protected metric; coverage may fall on purpose — that
  asymmetry is the lesson of lots 1/C/E encoded as thresholds.
- .github/workflows/benchmark-gate.yml: runs on PRs touching the engine,
  clones cached keyed on the corpus hash (only the first run after a re-pin
  pays), explicit git>=2.38 check so an old git fails loudly instead of
  measuring zero conflicts silently.

Lot F gate re-run on the v2 additions: still flat (halves too thin to clear
the evidence floors) — the desktop deferral stands, recorded in the README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQoTe6RE4JpoQknCASS3hu
The first committed baseline was produced by a stale run.mjs in the measuring
container that predated the per-repo agreement block, so compare.mjs could only
gate on the aggregate — per-repo regressions passed silently. Regenerated with
the current runner (same corpus, same totals: 59.24%, 391/660); the gate now
verifiably reports all eight repos and fails on a simulated -6pt per-repo drop.

Per-repo v2 baseline: laravel 83.3, symfony 73.7, express 61.7, git/git 61.1,
prettier 49.6, tauri 37.9, hugo 30.2, bootstrap 25.7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQoTe6RE4JpoQknCASS3hu
Plan only — registry design (5 script-suppressed ecosystems), plan-in-core /
execution-in-callers split, consent + sandbox + offline model, dedicated
measurement harness (merge-tree replay cannot score this lot), desktop
surface gated on measured results.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQoTe6RE4JpoQknCASS3hu
Base automatically changed from seo/onpage-improvements-2026-08 to main August 26, 2026 16:25
@devlint devlint closed this Aug 27, 2026
@devlint devlint reopened this Aug 27, 2026
Laurent Guitton added 13 commits August 27, 2026 14:55
…les (accuracy lot D, task 1)

Core stays Node-free: the generatedGate decline now attaches a machine-usable
RegenerationPlan (npm/pnpm/yarn-berry/composer/cargo) whenever the declined
lockfile matches the v1 registry, sourced from a caller-supplied
regenerationContext. Core never spawns anything; it only emits the plan and
appends a --regenerate hint to the existing French decline reason.
…ne sites (lot D, fix round 1)

reclassifyIfGenerated turns a genuinely-complex hunk on a generated path into
generated_file BEFORE resolveHunk runs, so the generatedGate branch (which
explicitly skips hunk.type === "generated_file") never sees the majority case:
a real lockfile conflict. That decline happened in assembleResolution's
generated_file case with no plan attached (spec finding #1's 0%-agreement
case). Factored plan-attachment into attachRegenerationPlan() and call it at
generatedGate, the minConfidence decline, and right after assembleResolution
returns lines: null for a generated_file hunk. assemble.ts stays untouched.
…test (lot D, fix round 2)

Site 2 (regeneration-plan attachment on the minConfidence decline for
generated_file hunks) was unreachable: computeEffectiveMinConfidence always
returns the more permissive of policy/option, no MergePolicy preset exceeds
"high", and reclassifyIfGenerated fixes generated_file confidence at exactly
"high" — so the confidence-threshold branch can never reject that hunk type
through any public API. Removed the branch and its comment; two real sites
remain (generatedGate, post-assembleResolution).

Also replaced the registry test's per-ecosystem script-suppression check,
which short-circuited to true for yarn-berry/cargo (never actually checking
their args), with a real assertion per ecosystem: explicit flag for
npm/pnpm/composer, exact --mode=update-lockfile for yarn-berry, and an exact
["generate-lockfile"] args array for cargo — so a future edit that dropped
the safe command choice would fail the test.
Core (lot D task 1) only emits a RegenerationPlan for declined generated
files; this adds the CLI-side executor. `gitwand resolve --regenerate`
(or `.gitwandrc` `regenerate: true`) runs a pass 2 after the normal
resolution pool: it re-derives each plan with the real sibling-file state
(pass 1's plans are always non-runnable placeholders) and, when runnable,
regenerates the lockfile in a disposable `git worktree` seeded with the
resolved sources of truth - never the user's real working tree.

- regenerate-runner.ts: toolchain probe, offline probe (DNS, no new dep),
  worktree sandboxing, timeout + full trace, output validation (JSON for
  npm/composer, YAML for pnpm/yarn-berry, TOML for cargo), guaranteed
  worktree cleanup.
- resolve.ts: pass 2 wiring, per-file verbose regenerate trace line,
  failure leaves the file exactly as pass 1 left it plus the reason.
- Fixed a real env-stripping bug found via testing: the secret-scrubbing
  regex was deleting GIT_CONFIG_KEY_N (legitimate git plumbing) while
  keeping GIT_CONFIG_COUNT, breaking `git worktree add` in environments
  that inject config via env vars.
…, fix round 1)

Review Important #1: pass 2's siblingFiles map only covered files git
flagged as conflicted, so a source of truth that merged cleanly (the
common case: package.json untouched, package-lock.json diverges) never
got a "clean" entry and buildRegenerationPlan treated it as conflicted,
making runnable permanently unreachable for that shape (and yarn-berry
entirely, since .yarnrc.yml is essentially never itself conflicted).
Pre-seed every candidate ecosystem's sourcesOfTruth not in the conflicted
set as clean, and read its on-disk content directly when pass 1 never
touched it.

Review Important #2: replaced the env denylist (regex on "sensitive"
substrings) with an explicit allowlist, matching AGENTS.md's actual
wording ("pass only the specific env vars the child process needs").

Review Important #3: added an end-to-end regression test that drives
cmdResolve() itself (not just regenerate-runner.ts) against a repo shaped
exactly like Important #1 - verified it fails without the resolve.ts fix
and passes with it restored.
…ng (accuracy lot D, task 3)

Fixes two upstream bugs that silently blocked core's lot F convention
precedence from the CLI: resolveGeneratedFiles always reached resolve() as a
concrete false instead of undefined, and .git/gitwand/conventions.json was
never loaded into options.conventions at all. Adds .gitwandrc
resolveGeneratedFiles precedence over measured conventions, a default
(non-verbose) --regenerate offer in the resolve summary, and reporting-only
regenerate:true support on the 3 MCP resolve() tools (never executes,
mirrors the CLI's precedence via a duplicated MCP-local helper). Extends
website/reference/config.md's Generated Files section with the regenerate
tier's consent model, sandbox, and convention interplay.
… param (task 3, fix round 1)

buildRegenerationReport() defaulted any sourceOfTruth path absent from the
current call's own results to "clean", conflating "not seen by this call"
with "not actually conflicted" — the same bug Task 2's fix round closed for
the CLI's resolve.ts. Exploitable via gitwand_resolve_conflicts' files:
param: a narrowed list excluding an actually-conflicted package.json could
report a plan as runnable:true. Now threads the repo's full conflicted-file
set through as a second parameter and only defaults to "clean" when a path
is absent from both the call's own results AND that full set, mirroring
resolve.ts's guard exactly.
…ask 4)

scripts/replay-regenerate.mjs replays historical merges from a real,
already-cloned corpus repo: cheap merge-tree candidate discovery, then real
checkout + real npm/pnpm/yarn-berry/composer/cargo regeneration via the CLI's
own runRegeneration() executor, structurally compared (scripts/lib/
regenerate-compare.mjs) against the lockfile the team actually committed.
Fixture tests (node --test scripts/lib/regenerate-compare.test.mjs, also
`pnpm run test:regenerate-compare`) cover the comparison/scoring logic for
all five registry formats with no network. stripVolatileValues is now
exported from @gitwand/core for the fallback path.

Pilot run (bounded, ~5 real attempts/ecosystem, per Ruling P-9): laravel/
framework's composer leg is infeasible (it never commits composer.lock,
confirmed via full history; symfony/symfony has the same gap, so corpus v2
has no measurable composer repo). prettier/prettier turned out to be
yarn-berry, not npm as assumed; piloted that instead: 66.7% structural
agreement (n=3), below the 80% target. benchmark/README.md documents the
method, the real numbers, and the resulting gate verdict: keep CLI opt-in
only, do not build the desktop surface (task 5) on this evidence.
Closes the 5 Important findings from the whole-branch final review:

1. Nested (non-root) lockfiles could report a false "regenerated" success
   (silent take-ours wearing regeneration's provenance) because the CLI's
   worktree runner writes/reads sources of truth at the worktree root, not
   the lockfile's own directory. Fixed once in core's buildRegenerationPlan
   (blocks any non-root file, inherited by CLI, MCP reporting, and the
   measurement harness alike) rather than duplicated in three callers.
2. "Not conflicted" was conflated with "clean" in both the CLI's and MCP's
   sibling-map seeding, letting yarn-classic repos (no .yarnrc.yml) report
   runnable:true for yarn-berry, contradicting the registry's own documented
   berry-marker guard. Both now require the file to actually exist on disk.
3. Documented (not re-architected, per the review's own scope note) that
   regenerate-runner.ts seeds its worktree from HEAD (ours-only) rather than
   the in-progress merge index, and named it as hypothesis (d) for the
   66.7%/n=3 pilot result in benchmark/README.md.
4. Split the env allowlist: the GIT_* prefix (needed for git worktree
   plumbing) no longer reaches the spawned ecosystem installer, closing a
   path for CI-injected credentials (GIT_CONFIG_*/GIT_ASKPASS/
   GIT_SSH_COMMAND) to leak into npm/pnpm/yarn/composer/cargo's environment.
5. scripts/replay-regenerate.mjs now refuses to run against a non-bare repo
   (git update-ref HEAD follows the symref) and restores the operator's
   original HEAD from a SIGINT/SIGTERM handler, not just the happy path.

Also includes the review's opportunistic ask: a one-line notice when a
runnable plan's source turns out to be unreadable (previously silent).

disabled
…epo tests

Discovered during the pre-finish full-suite run: these tests spawn real
git subprocesses and were relying on vitest's 5s default, which flakes
under the full monorepo suite's concurrent load. Matches the 30s
timeout convention already used by every other real-git-repo test in
this plan.
Laurent Guitton added 8 commits August 28, 2026 10:00
…merge-tree result

replay-regenerate.mjs replays historical merges with no live in-progress-merge
index to read from, so it now builds a scratch index from the merge-tree it
already computed during candidate discovery and passes it as
runRegeneration's seedIndexFile, matching the disposable-worktree seeding fix
already shipped in the CLI's production path.
…-index-seeding fix

Real, full-scale replay-regenerate.mjs run (--max-real 20) against all four
in-scope corpus v2 repos (prettier/prettier, tauri-apps/tauri,
expressjs/express, twbs/bootstrap), after tasks 2-3's merge-index-seeding
fix landed. Result: n=1 comparable (1/1 = 100.0%), a smaller comparable
sample than the pilot's n=3 (66.7%) it was meant to supersede — reported as
genuinely inconclusive, not "met". CLI-opt-in-only status quo stands; the
desktop surface remains unjustified by this evidence.
…view

The measurement harness's scratch index (git read-tree of a single merge-tree
result) put every path at stage 0, including genuinely conflicted ones whose
blob content is literal diff3 markers - a worktree state production can never
reach, since a real merge index keeps conflicted paths off stage 0 entirely.
seedScratchIndex now takes a skipPaths param to force-remove those paths from
the scratch index after read-tree, and replay-regenerate.mjs retains each
candidate's conflicted path list from discovery to pass through.

Also: addWorktree's checkout-index overlay no longer throws (preserves
runRegeneration's never-throw contract) and no longer leaks an ambient
GIT_INDEX_FILE; corrected an overclaim in the fix's own doc comments and in
benchmark/README.md about what the overlay actually changes; fixed a 32/32
arithmetic error in the sweep write-up; marked the existing sweep numbers
invalidated pending a re-run against the fixed harness; rewrote seed-index's
existing test (it wasn't actually exercising seedScratchIndex) and added
coverage for the skip-paths behavior and for the CLI's seedIndexFile
interface end to end.

disabled
…iction with the invalidation notice

The gate verdict section still floated a "more realistic merge-index
state" hypothesis for the spawn-failed bottleneck, and asked for
further root-causing — both superseded by the invalidation notice's
own root-cause (the harness's marker-corruption bug, now fixed).
Also fixes a stale package.json script-name reference left over from
the test:regenerate-compare -> test:scripts-lib rename.
… harness

The seed-index skipPaths fix (43be17e) is real but incomplete: git
update-index --force-remove still requires a work tree even when only
editing an index file, and the corpus caches are intentionally bare, so
every runnable candidate now fails before ever reaching the installer.
This fresh, real re-run produced 0 comparable results (vs the invalidated
sweep's n=1 and the original pilot's n=3), documented honestly with the
root cause and the concrete next fix needed.
…ree plumbing

git update-index --force-remove still requires a work tree even when only
editing an index file, so seedScratchIndex failed 100% of the time against
the corpus's bare clones. Rebuilt it to remove skipped paths via targeted
git ls-tree -z / mktree -z --missing calls that walk only the directory
chain of each skip path, which needs no work tree, tolerates C-quoted
filenames, and doesn't require objects to be fetched in a blobless clone.

Re-ran the real regenerate-tier sweep against the fixed harness: n = 13
comparable, 5 matched, 38.5% agreement on prettier/prettier's yarn-berry
candidates, well below the 80% target. Documented in benchmark/README.md
alongside the two prior, invalidated sweep attempts.

disabled
Independent review of the third sweep attempt confirmed the 38.5%
(5/13) result itself is sound, but found the write-up overreached in
two places and understated the fix's remaining coverage gaps:

- The gate verdict implied "every runnable candidate ran to
  completion" meant the full 85-candidate population was covered; it
  didn't disclose that --max-real 20 only attempted the 20 most
  recent (recency-biased) candidates, leaving 65 unclassified.
- The hypothesis-(d) paragraph read a causal "seeding made it worse"
  comparison out of the pilot's 66.7% (n=3) vs this sweep's 38.5%
  (n=13) despite there being no matched-pair comparison and heavily
  overlapping confidence intervals - replaced with the narrower,
  actually-supported claim.
- Corrected a false "cannot be reproduced without network" claim
  about the blobless-clone bug (independently disproven) and
  disclosed two residual gaps found by the same review: the blobless
  dimension still has no regression test, and a C-quoted skip path
  from merge-tree's default output would silently fail to be
  stripped (zero blast radius on this sweep's 237 scanned merges,
  confirmed by the reviewer, but a live latent bug for future runs).
- Fixed a stale historical-section pointer chain and an inaccurate
  illustrative directory count.
…ession test

mergeTree() in replay-regenerate.mjs used git merge-tree's default (non-z)
output, which C-quotes any conflicted path containing a quote character or
non-ASCII byte. Those quoted, escaped strings never matched the raw bytes
seedScratchIndex compares skipPaths against (read via git ls-tree -z), so a
C-quoted conflicted path would silently fail to be skipped, leaking diff3
marker content into the scratch index. Extracted mergeTree into
scripts/lib/merge-tree.mjs and switched it to -z, parsing the verified real
output shape rather than guessing from documentation.

Also added the regression test the blobless-clone mktree --missing fix
(scripts/lib/seed-index.mjs) shipped without: a hermetic blobless bare clone
built via uploadpack.allowFilter + clone --bare --filter=blob:none, no
network required, disproving the prior claim that this case "cannot be
reproduced" hermetically.

Zero impact on the already-published regenerate-tier sweep numbers; this is
pure robustness/coverage work for future runs.
@devlint devlint changed the title Moteur de conflits : accord mesuré avec le merge humain (lots 1/C/E/F/G) Moteur de conflits : accord mesuré avec le merge humain (lots 1/C/E/F/G/D) Aug 28, 2026
Laurent Guitton added 3 commits August 28, 2026 17:59
README.md, the conflict-resolution guide and the package-lock.json fix
page all still described generated_file as auto-applying (accept-theirs
or a semantic key-wise merge) by default. It now declines by default,
measured on 1,662 real merges to diverge from what teams actually
shipped in almost every case — these three pages were the only
user-facing docs this branch left saying otherwise.

disabled
.impeccable/hook.cache.json showed up as an untracked file with no repo
content (empty session cache) — ignore the whole dir so it stops surfacing.
…accuracy

# Conflicts:
#	CHANGELOG.md
#	README.md
#	package.json
#	packages/cli/src/commands/resolve.ts
#	packages/core/src/__tests__/corpus.ts
#	packages/core/src/resolver/assemble.ts
#	packages/core/src/resolver/generated-detection.ts
#	packages/core/src/resolver/index.ts
#	pnpm-lock.yaml
@devlint
devlint merged commit d7b54a4 into main Aug 31, 2026
9 checks passed
@devlint
devlint deleted the feat/conflict-engine-accuracy branch August 31, 2026 08:19
devlint pushed a commit that referenced this pull request Aug 31, 2026
…state

CHANGELOG.md's Unreleased section stopped tracking after lot E: lot F
(gitwand conventions), lot G (the CI benchmark-gate), the full lot D
regenerate tier, and the corpus v2 re-pin never got an entry despite
shipping in the same PR (#170). Added the four missing entries.

ROADMAP.md's "Follow-ups" list for Engine Accuracy described those same
four items as future work; all are done. Moved Engine Accuracy from
"next release" to v3.9.0 in Shipped, and shifted every later planned
version down one slot (Live Repo v3.9.0 -> v3.10.0, preview-to-apply
v3.10.0 -> v3.11.0, Stacked Branches v3.11.0 -> v3.12.0, Combined Diffs
v3.12.0 -> v3.13.0, Voice Input v3.13.0 -> v3.14.0; v4.0.0 unaffected),
updating every cross-reference to the old numbers throughout the file.
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