mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 04:09:17 +00:00
fix: индексировать ревью стабильного релиза (#670)
Issue: #670 User-Visible: no
This commit is contained in:
@@ -9832,6 +9832,46 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: " const names = readdirSync(dir).filter((name) => name.endsWith('.md')).sort(); // mutant: indexes itself",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'reviews-index-release-name-unsupported',
|
||||
guard: 'node --test --test-name-pattern="#635 индекс покрывает|#670" test/reviews-index.test.mjs',
|
||||
because: '#670: the first line review document must be indexed instead of making the next dev Validate red',
|
||||
patches: [{
|
||||
file: 'scripts/reviews-index.mjs',
|
||||
find: " if (release) return { stage: 'release', issue: null, round: null, suffix: null, tag: release[1] };",
|
||||
replace: " if (false && release) return { stage: 'release', issue: null, round: null, suffix: null, tag: release[1] }; // mutant: release review is unknown",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'reviews-index-release-counts-ignored',
|
||||
guard: 'node --test --test-name-pattern="#635 счётчики|#635 индекс покрывает" test/reviews-index.test.mjs',
|
||||
because: '#670: release reviews use the mandated `Итог: High N · Medium N` summary without colons',
|
||||
patches: [{
|
||||
file: 'scripts/reviews-index.mjs',
|
||||
find: ' if (release) return release;\n for (const scope of [verdictLine(text, true), verdictSection(text), verdictLine(text)]) {',
|
||||
replace: ' void release; // mutant: ignore the release summary\n for (const scope of [verdictLine(text, true), verdictSection(text), verdictLine(text)]) {',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'reviews-index-strict-cli-disabled',
|
||||
guard: 'node --test --test-name-pattern="#670" test/reviews-index.test.mjs',
|
||||
because: '#670: strict publication must reject a new unknown document before it writes an incomplete index',
|
||||
patches: [{
|
||||
file: 'scripts/reviews-index.mjs',
|
||||
find: " if (process.argv.includes('--strict')) assertAllDocumentsIndexed(collected);",
|
||||
replace: " if (false && process.argv.includes('--strict')) assertAllDocumentsIndexed(collected); // mutant: strict is disabled",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'release-review-index-without-strict',
|
||||
guard: 'node --test --test-name-pattern="#638" test/release-review.test.mjs',
|
||||
because: '#670: the publisher itself must opt into strict indexing so a future filename drift fails at its source',
|
||||
patches: [{
|
||||
file: '.github/workflows/release-review.yml',
|
||||
find: 'node scripts/reviews-index.mjs --dir=docs/reviews --strict',
|
||||
replace: 'node scripts/reviews-index.mjs --dir=docs/reviews',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'reviews-index-verdict-substring',
|
||||
guard: 'node --test --test-name-pattern="#635 вердикт" test/reviews-index.test.mjs',
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
// заголовки находок. Он генерируется (класс C), а не пишется руками, и
|
||||
// пересобирается конвейером после публикации каждого документа ревью.
|
||||
//
|
||||
// node scripts/reviews-index.mjs [--dir=docs/reviews] [--output=docs/reviews/INDEX.md] [--check]
|
||||
// node scripts/reviews-index.mjs [--dir=docs/reviews] [--output=docs/reviews/INDEX.md] [--check] [--strict]
|
||||
// node scripts/reviews-index.mjs --commit-if-stale --issue=NN [--dir=…]
|
||||
//
|
||||
// `--check` — не писать, а сравнить с существующим файлом (гейт «индекс свеж»;
|
||||
// тот же инвариант держит тест `#635 индекс свеж`).
|
||||
// `--strict` — отказать до записи, если в каталоге есть неизвестное имя
|
||||
// документа; точка публикации release-review использует этот режим.
|
||||
// `--commit-if-stale` — пересобрать и, если файл изменился, закоммитить его
|
||||
// коммитом конвейера (класс C). Индекс — снимок каталога: ребейз ветки на
|
||||
// dev, получивший новые документы, устаревает его молча (r2 #635 H1), поэтому
|
||||
@@ -25,6 +27,7 @@ import { isMainModule } from './spawn-portable.mjs';
|
||||
|
||||
export const INDEX_FILE = 'INDEX.md';
|
||||
const DOC_NAME = /^(CODE|SPEC)-REVIEW-(?:issue-)?(\d+)(?:-r(\d+))?(?:-([a-z0-9-]+))?\.md$/i;
|
||||
const RELEASE_DOC_NAME = /^RELEASE-REVIEW-(v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))\.md$/i;
|
||||
const COLOUR = {
|
||||
'зелёный': 'зелёный', 'зеленый': 'зелёный', green: 'зелёный',
|
||||
'жёлтый': 'жёлтый', 'желтый': 'жёлтый', yellow: 'жёлтый',
|
||||
@@ -38,6 +41,8 @@ const VERDICT_OWN_LINE_RE = /^[ \t]*(?:[-*]\s*)?\**(?:Вердикт|Verdict)[^\
|
||||
|
||||
/** Разобрать имя документа: этап, issue, раунд. */
|
||||
export function parseDocName(name) {
|
||||
const release = RELEASE_DOC_NAME.exec(String(name));
|
||||
if (release) return { stage: 'release', issue: null, round: null, suffix: null, tag: release[1] };
|
||||
const match = DOC_NAME.exec(String(name));
|
||||
if (!match) return null;
|
||||
return {
|
||||
@@ -99,6 +104,11 @@ const countsIn = (scope) => {
|
||||
return high || medium ? { high: high ? Number(high[1]) : 0, medium: medium ? Number(medium[1]) : 0 } : null;
|
||||
};
|
||||
|
||||
const releaseCounts = (text) => {
|
||||
const match = /(?:^|\n)Итог:\s*High\s+(\d+)\s*·\s*Medium\s+(\d+)(?:\s*·\s*Low\s+\d+)?(?:\s|$)/i.exec(text);
|
||||
return match ? { high: Number(match[1]), medium: Number(match[2]) } : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Заголовки находок с их severity и телом секции. Тело — строки до
|
||||
* следующего заголовка того же или более высокого уровня.
|
||||
@@ -175,6 +185,8 @@ function numberedItems(body) {
|
||||
* документа («ТЗ прошло зелёным на r3 (High: 0, Medium: 0)»).
|
||||
*/
|
||||
export function parseCounts(text) {
|
||||
const release = releaseCounts(text);
|
||||
if (release) return release;
|
||||
for (const scope of [verdictLine(text, true), verdictSection(text), verdictLine(text)]) {
|
||||
const counts = scope ? countsIn(scope) : null;
|
||||
if (counts) return counts;
|
||||
@@ -281,11 +293,22 @@ export function collectEntries(dir, read = (name) => readFileSync(join(dir, name
|
||||
return { entries, skipped };
|
||||
}
|
||||
|
||||
/** Строгая граница публикации: новый документ обязан попасть в индекс сразу. */
|
||||
export function assertAllDocumentsIndexed({ skipped }) {
|
||||
if (skipped.length) throw new Error(`вне схемы имён: ${skipped.join(', ')}`);
|
||||
}
|
||||
|
||||
const badge = (verdict) => ({ 'зелёный': '🟢', 'жёлтый': '🟡', 'красный': '🔴' }[verdict] || '⚪');
|
||||
|
||||
export function renderIndex({ entries, skipped = [] }) {
|
||||
const releaseDocs = entries.filter((entry) => entry.stage === 'release').sort((a, b) => {
|
||||
const av = a.tag.slice(1).split('.').map(Number);
|
||||
const bv = b.tag.slice(1).split('.').map(Number);
|
||||
for (let i = 0; i < 3; i += 1) if (av[i] !== bv[i]) return bv[i] - av[i];
|
||||
return 0;
|
||||
});
|
||||
const byIssue = new Map();
|
||||
for (const entry of entries) {
|
||||
for (const entry of entries.filter((item) => item.stage !== 'release')) {
|
||||
const list = byIssue.get(entry.issue) || [];
|
||||
list.push(entry);
|
||||
byIssue.set(entry.issue, list);
|
||||
@@ -298,6 +321,9 @@ export function renderIndex({ entries, skipped = [] }) {
|
||||
lines.push('');
|
||||
lines.push('| Issue | Документ | Этап · раунд | Вердикт | H | M | Находки | Файлы |');
|
||||
lines.push('|---|---|---|---|---:|---:|---|---|');
|
||||
for (const doc of releaseDocs) {
|
||||
lines.push(`| линия ${doc.tag} | [${doc.name}](${doc.name}) | ревью линии · — | ${badge(doc.verdict)} ${doc.verdict} | ${doc.high} | ${doc.medium} | ${doc.findings.join('; ').replace(/\|/g, '\\|') || '—'} | ${(doc.files || []).map((f) => `\`${f}\``).join(' ') || '—'} |`);
|
||||
}
|
||||
for (const issue of issues) {
|
||||
const docs = byIssue.get(issue).sort((a, b) => (a.stage === b.stage ? (a.round || 0) - (b.round || 0) : a.stage === 'spec' ? -1 : 1));
|
||||
for (const doc of docs) {
|
||||
@@ -353,7 +379,9 @@ if (isMainModule(import.meta.url)) {
|
||||
console.log(changed ? `${output} пересобран и закоммичен: ${sha}` : `${output} свеж — коммит не нужен`);
|
||||
process.exit(0);
|
||||
}
|
||||
const markdown = buildIndex(dir);
|
||||
const collected = collectEntries(dir);
|
||||
if (process.argv.includes('--strict')) assertAllDocumentsIndexed(collected);
|
||||
const markdown = renderIndex(collected);
|
||||
if (process.argv.includes('--check')) {
|
||||
const current = existsSync(output) ? readFileSync(output, 'utf8') : '';
|
||||
if (current !== markdown) {
|
||||
|
||||
Reference in New Issue
Block a user