mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
feat(tools): архив документов ревью выпущенных линий (#682)
Волна 5 эпика #674, инструментальная часть (класс B). `scripts/reviews-archive.mjs --through=vX.Y.Z` печатает план переноса документов ревью в `legacy/reviews/<тег>/`, `--apply` делает `git mv` и пересобирает `docs/reviews/INDEX.md`. Членство — трейлеры `Issue: #NN` в диапазоне линии, как у манифеста беты (#547) и ревью линии (#638). Правила — в чистой `archivePlan`: задача уходит в последнюю свою линию; задача с трейлером после тега остаётся целиком (её раунды ссылаются на прошлые); закрытая без выпуска уходит с линией, где лёг её документ; документ задачи без трейлера — с линией, где его добавили; RELEASE-REVIEW — в каталог своего тега; чужие имена не трогаются. План по v1.77.0: 965 документов 332 задач, 154 остаются в открытой линии. `legacy/` — класс C в process-gate. Сравнения деревьев с якорем вердикта (`review-doc-guard.mjs` #499, `task-packet.mjs`) не видят переноса в `legacy/reviews/`. `process-metrics.mjs` считает раунды по живому каталогу и архиву. Порог «>900 документов» в тесте индекса снят: в каталоге остаётся текущая линия. PROCESS.md §2.10 уточнён (правила членства, пустая очередь S7, ревью линии до переноса), в DEVELOPMENT › Release — шаг чеклиста. Юнит-тесты и два мутанта (`reviews-archive-moves-open-line-issue`, `reviews-archive-first-line-wins`). Issue: #682 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
@@ -49,6 +49,8 @@ test('paths classify into A/B/C/D with the generated tree winning over source',
|
||||
assert.equal(classify('docs/SCOPE.md'), 'C');
|
||||
assert.equal(classify('PROCESS.md'), 'C');
|
||||
assert.equal(classify('CODE-REVIEW-111-r1.md'), 'C');
|
||||
assert.equal(classify('legacy/reviews/v1.77.0/CODE-REVIEW-111-r1.md'), 'C'); // #682
|
||||
assert.equal(classify('legacy/README.md'), 'C');
|
||||
assert.equal(classify('something-unheard-of.xyz'), '?');
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import {
|
||||
buildReport, issueMetrics, jobMinutes, pipelineMetrics, renderMarkdown, reviewRounds, runMetrics,
|
||||
buildReport, issueMetrics, jobMinutes, pipelineMetrics, renderMarkdown, reviewDocNames, reviewRounds, runMetrics,
|
||||
} from '../scripts/process-metrics.mjs';
|
||||
|
||||
const T = (h) => new Date(Date.UTC(2026, 8, 15, 0, Math.round(h * 60))).toISOString();
|
||||
@@ -140,3 +140,18 @@ test('#637 workflow: еженедельный запуск читает толь
|
||||
assert.match(wf, /GITHUB_STEP_SUMMARY/);
|
||||
assert.ok(!/issues: write/.test(wf), 'метрики ничего не пишут в issue');
|
||||
});
|
||||
|
||||
test('#682 reviewDocNames: живой каталог и архив legacy/reviews/<тег>/ считаются вместе', () => {
|
||||
const listing = [
|
||||
'docs/reviews/CODE-REVIEW-600-r2.md',
|
||||
'docs/reviews/INDEX.md',
|
||||
'legacy/reviews/v1.77.0/CODE-REVIEW-600-r1.md',
|
||||
'legacy/reviews/v1.77.0/SPEC-REVIEW-601-r1.md',
|
||||
'legacy/docs/ROADMAP.md',
|
||||
'',
|
||||
].join('\n');
|
||||
const names = reviewDocNames(listing);
|
||||
assert.deepEqual(names, ['CODE-REVIEW-600-r2.md', 'INDEX.md', 'CODE-REVIEW-600-r1.md', 'SPEC-REVIEW-601-r1.md']);
|
||||
// Перенос r1 в архив не уменьшает число раундов задачи.
|
||||
assert.equal(reviewRounds(names).get('CODE:600'), 2);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// #682: архив документов ревью выпущенных линий — кому куда, решают трейлеры.
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { ARCHIVE_DIR, LIVE_DIR, archivePlan, renderPlan, stableTagsThrough } from '../scripts/reviews-archive.mjs';
|
||||
|
||||
const lines = [
|
||||
{ tag: 'v1.76.0', issues: [500, 510, 520] },
|
||||
{ tag: 'v1.77.0', issues: [520, 530, 540] },
|
||||
];
|
||||
const names = [
|
||||
'INDEX.md',
|
||||
'SPEC-REVIEW-500-r1.md', 'CODE-REVIEW-500-r1.md', 'CODE-REVIEW-500-r2.md',
|
||||
'SPEC-REVIEW-520-r1.md', 'CODE-REVIEW-520-r1.md',
|
||||
'CODE-REVIEW-540-r1.md', 'CODE-REVIEW-540-r2.md',
|
||||
'CODE-REVIEW-600-r1.md',
|
||||
'CODE-REVIEW-issue-068-2026-08-12.md',
|
||||
'RELEASE-REVIEW-v1.77.0.md', 'RELEASE-REVIEW-v1.78.0.md',
|
||||
'notes.md',
|
||||
];
|
||||
const where = (plan, name) => plan.moves.find((m) => m.name === name)?.to
|
||||
?? plan.kept.find((k) => k.name === name)?.reason;
|
||||
|
||||
test('#682 архив: задача уходит в каталог последней своей линии, документы не разъезжаются', () => {
|
||||
const plan = archivePlan({ names, lines, open: [600], through: 'v1.77.0' });
|
||||
assert.equal(where(plan, 'CODE-REVIEW-500-r2.md'), `${ARCHIVE_DIR}/v1.76.0/CODE-REVIEW-500-r2.md`);
|
||||
// #520 — трейлеры в обеих линиях: все раунды в последней.
|
||||
assert.equal(where(plan, 'SPEC-REVIEW-520-r1.md'), `${ARCHIVE_DIR}/v1.77.0/SPEC-REVIEW-520-r1.md`);
|
||||
assert.equal(where(plan, 'CODE-REVIEW-520-r1.md'), `${ARCHIVE_DIR}/v1.77.0/CODE-REVIEW-520-r1.md`);
|
||||
assert.equal(where(plan, 'RELEASE-REVIEW-v1.77.0.md'), `${ARCHIVE_DIR}/v1.77.0/RELEASE-REVIEW-v1.77.0.md`);
|
||||
assert.ok(plan.moves.every((m) => m.from === `${LIVE_DIR}/${m.name}`));
|
||||
assert.ok(!plan.moves.some((m) => m.name === 'INDEX.md') && !plan.kept.some((k) => k.name === 'INDEX.md'));
|
||||
});
|
||||
|
||||
test('#682 архив: задача из открытой линии остаётся целиком — её раунды ссылаются на прошлые', () => {
|
||||
// #540 выпущена в v1.77.0, но у неё есть трейлер и после тега.
|
||||
const plan = archivePlan({ names, lines, open: [540, 600], through: 'v1.77.0' });
|
||||
assert.equal(where(plan, 'CODE-REVIEW-540-r1.md'), 'задача есть в открытой линии');
|
||||
assert.equal(where(plan, 'CODE-REVIEW-540-r2.md'), 'задача есть в открытой линии');
|
||||
assert.equal(where(plan, 'CODE-REVIEW-600-r1.md'), 'задача есть в открытой линии');
|
||||
assert.equal(where(plan, 'RELEASE-REVIEW-v1.78.0.md'), 'ревью линии v1.78.0 не входит в архивируемые линии');
|
||||
});
|
||||
|
||||
test('#682 архив: без трейлера — по линии добавления документа, иначе остаётся; чужое имя не трогается', () => {
|
||||
const bare = archivePlan({ names, lines, open: [600], through: 'v1.77.0' });
|
||||
assert.equal(where(bare, 'CODE-REVIEW-issue-068-2026-08-12.md'), 'нет трейлера ни в одной линии');
|
||||
assert.equal(where(bare, 'notes.md'), 'вне схемы имён');
|
||||
const resolved = archivePlan({
|
||||
names, lines, open: [600], through: 'v1.77.0',
|
||||
addedIn: new Map([['CODE-REVIEW-issue-068-2026-08-12.md', 'v1.76.0'], ['notes.md', 'v1.76.0']]),
|
||||
});
|
||||
assert.equal(where(resolved, 'CODE-REVIEW-issue-068-2026-08-12.md'), `${ARCHIVE_DIR}/v1.76.0/CODE-REVIEW-issue-068-2026-08-12.md`);
|
||||
assert.equal(where(resolved, 'notes.md'), 'вне схемы имён');
|
||||
// Тег добавления вне архивируемых линий (бета, будущая линия) — не повод переносить.
|
||||
const future = archivePlan({ names, lines, open: [600], through: 'v1.77.0', addedIn: new Map([['CODE-REVIEW-issue-068-2026-08-12.md', 'v1.78.0']]) });
|
||||
assert.equal(where(future, 'CODE-REVIEW-issue-068-2026-08-12.md'), 'нет трейлера ни в одной линии');
|
||||
const report = renderPlan({ ...bare, through: 'v1.77.0' });
|
||||
assert.match(report, /переносится 8 \(задач 3\), остаётся 4/);
|
||||
assert.match(report, /CODE-REVIEW-issue-068-2026-08-12\.md — нет трейлера/);
|
||||
});
|
||||
|
||||
test('#682 архив: линии — только стабильные теги не новее границы', () => {
|
||||
assert.deepEqual(
|
||||
stableTagsThrough(['v1.78.0-beta.1', 'v1.77.0', 'v1.76.0', 'v1.10.0', 'v1.9.0', 'v1.78.0', 'x'], 'v1.77.0'),
|
||||
['v1.9.0', 'v1.10.0', 'v1.76.0', 'v1.77.0'],
|
||||
);
|
||||
assert.throws(() => stableTagsThrough([], 'v1.78.0-beta.1'), /not a stable release tag/);
|
||||
assert.throws(() => archivePlan({ names: [], lines: [{ tag: 'v1.78.0', issues: [] }], open: [], through: 'v1.77.0' }), /newer than/);
|
||||
});
|
||||
@@ -119,7 +119,9 @@ test('#670 CLI --strict принимает ревью линии и отклон
|
||||
test('#635 живой каталог docs/reviews: индекс свеж и покрывает все документы', () => {
|
||||
const { entries, skipped } = collectEntries(fileURLToPath(new URL('../docs/reviews/', import.meta.url)));
|
||||
assert.equal(skipped.length, 0, `вне схемы имён: ${skipped.join(', ')}`);
|
||||
assert.ok(entries.length > 900);
|
||||
// #682: выпущенные линии уезжают в legacy/reviews/<тег>/ — здесь только
|
||||
// текущая линия, и её размер не константа. Пустой каталог — тоже поломка.
|
||||
assert.ok(entries.length > 0);
|
||||
const recognised = entries.filter((e) => e.verdict !== '—').length;
|
||||
assert.ok(recognised / entries.length > 0.9, `вердикт распознан у ${recognised} из ${entries.length}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user