infra(process): fast-forward merge rebuilds the review index too (#657 r1 H1)

The task branch no longer carries docs/reviews/INDEX.md (1b), and
merge-candidate rebuilt it only inside rebaseOnto. When dev did not move
the fast-forward pushed the stale index and reviews_index would turn dev
red. freshIndex(tip) commits the rebuilt index on top of the material
before the push; the merge stays a fast-forward.

Real-git test: fast-forward, then reviews-index --check on the dev head
is green. Mutant merge-ff-skips-review-index.

Issue: #657
User-Visible: no
This commit is contained in:
Claude
2026-09-26 07:28:24 +00:00
committed by claude[bot]
parent 78d4da4abe
commit b856dd33c8
4 changed files with 105 additions and 8 deletions
+14 -2
View File
@@ -167,6 +167,17 @@ export function realOps({
must(exec(process.execPath, [REVIEWS_INDEX_SCRIPT, '--dir=docs/reviews', '--commit-if-stale', `--issue=${issue}`]), 'reviews-index --commit-if-stale');
return must(git('rev-parse', 'HEAD'), 'rev-parse HEAD');
},
// #657 (1б) r1 H1: документ ревью ветки задачи больше не несёт индекс —
// его пересобирают только коммиты, идущие в dev. Ребейз делает это сам
// (выше); fast-forward, когда dev не двигался, ребейза не знает, и без
// этого шага в dev уехал бы устаревший INDEX.md — красный `reviews_index`
// на голове dev. Коммит индекса — doc-коммит конвейера поверх материала,
// слияние остаётся fast-forward.
freshIndex: (tip) => {
must(git('checkout', '-q', '-B', 'merge-into-dev', tip), 'checkout');
must(exec(process.execPath, [REVIEWS_INDEX_SCRIPT, '--dir=docs/reviews', '--commit-if-stale', `--issue=${issue}`]), 'reviews-index --commit-if-stale');
return must(git('rev-parse', 'HEAD'), 'rev-parse HEAD');
},
pushWithLease: (sha, ref, expected) => {
const r = git('push', '-q', `--force-with-lease=refs/heads/${ref}:${expected}`, pushUrl, `${sha}:refs/heads/${ref}`);
if (r.status === 0) return true;
@@ -249,10 +260,11 @@ export async function mergeCandidate({ branch, material, issue, ops, maxAttempts
ops.log(`попытка ${attempt}: dev@${devNow.slice(0, 8)}, база материала ${materialBase.slice(0, 8)}, dev ${devMoved ? 'двигался' : 'на месте'}`);
if (!devMoved) {
const pushed = ops.pushWithLease(tip, 'dev', devNow);
const target = ops.freshIndex(tip);
const pushed = ops.pushWithLease(target, 'dev', devNow);
const decision = decideMerge({ fresh: true, devMoved: false, leaseRejected: !pushed });
if (decision.action === 'retry') continue;
return finish(decision, { candidate: tip, devNow });
return finish(decision, { candidate: target, devNow });
}
const candidate = ops.rebaseOnto(tip, 'origin/dev');
+6
View File
@@ -9497,6 +9497,12 @@ const MUTANT_DEFINITIONS = [
because: "#657: the freshness check has to run in the publishing orchestrator, not only in a unit",
patches: [{ file: "scripts/release-prerelease.mjs", find: " assertCommittedBundleFresh(manifest, sourceFingerprint(root));\n", replace: "" }],
},
{
id: "merge-ff-skips-review-index",
guard: "node --test --test-name-pattern=\"#657 r1 H1\" test/merge-candidate.test.mjs",
because: "#657 r1 H1: the task branch no longer carries INDEX.md, so a fast-forward merge must rebuild it or dev turns red on reviews_index",
patches: [{ file: "scripts/merge-candidate.mjs", find: " const target = ops.freshIndex(tip);", replace: " const target = tip;" }],
},
{
id: "provenance-skips-bundle-rule",
guard: "node --test --test-name-pattern=\"#657 \u043f\u0440\u0430\u0432\u0438\u043b\u043e \u0438\u0441\u043f\u043e\u043b\u043d\u044f\u0435\u0442 validate-commit-provenance\" test/bundle-policy.test.mjs",