diff --git a/scripts/mutation-registry.mjs b/scripts/mutation-registry.mjs index 9dd0a8e3..f07f56b6 100644 --- a/scripts/mutation-registry.mjs +++ b/scripts/mutation-registry.mjs @@ -3162,6 +3162,17 @@ const MUTANT_DEFINITIONS = [ replace: " if (STATUS_LABELS.includes(status)) reasons.push(`статус ${status}`);", }], }, + { + id: 'task-packet-trivial-is-product-flow', + guard: 'node --test test/task-packet.test.mjs', + because: 'the trivial short track writes no spec and no spec review, so in S6/S7 its label is ' + + 'the only product-flow evidence; dropping it prints the false class A ban of #632 again (r1)', + patches: [{ + file: 'scripts/task-packet.mjs', + find: " if (labels.includes('trivial')) reasons.push('короткий трек trivial (ТЗ не пишется, §5.1)');", + replace: "", + }], + }, { id: 'fit-house-hidden-walls-vote', guard: 'node demo/smoke_space_card.mjs', diff --git a/scripts/task-packet.mjs b/scripts/task-packet.mjs index e033c101..c7bfebd2 100644 --- a/scripts/task-packet.mjs +++ b/scripts/task-packet.mjs @@ -131,10 +131,15 @@ const PRE_CODE_STATUSES = ['S1-new', 'S2-analysis', 'S3-spec', 'S4-spec-review', * сразу на S7 и никогда не несёт S1–S5, ТЗ и ревью ТЗ; поэтому любой из этих * признаков делает эвристику «дифф без класса A» неприменимой. S6/S7/S8 сами по * себе признаком не являются: их носит и инфраструктурная задача после ревью. + * Метка `trivial` — признак сама по себе (r1 #632): короткий трек (PROCESS §5.1) + * идёт S2 → S5 без ТЗ и без ревью ТЗ, и в S6/S7 никакого другого следа потока у + * него нет. Ускоренный инфраструктурный вход понятия трека не имеет, поэтому + * `trivial` на нём не бывает; `infra` рядом с ней — тематическая метка. */ -export function productFlowEvidence({ status = null, issue = {}, specs = [], reviewDocs = [], comments = [] } = {}) { +export function productFlowEvidence({ status = null, labels = [], issue = {}, specs = [], reviewDocs = [], comments = [] } = {}) { const reasons = []; if (PRE_CODE_STATUSES.includes(status)) reasons.push(`статус ${status}`); + if (labels.includes('trivial')) reasons.push('короткий трек trivial (ТЗ не пишется, §5.1)'); if (/^#{1,3}\s*ТЗ(?![\p{L}\p{N}_])/mu.test(String(issue?.body ?? ''))) reasons.push('раздел «## ТЗ» в теле issue'); if (specs.length) reasons.push('файл ТЗ в docs/specs'); if (reviewDocs.some((d) => String(d.name).startsWith('SPEC-REVIEW-'))) reasons.push('документ ревью ТЗ'); @@ -150,7 +155,7 @@ export function buildPacket(inputs) { // Трек сначала определяется статусом и историей issue (#632): прошедшая // S3/S4/S5 или несущая ТЗ задача — продуктовая, и её право на класс A в // S5–S7 не отнимается пустым пока диффом. - const productFlow = productFlowEvidence({ status, issue, specs, reviewDocs, comments }); + const productFlow = productFlowEvidence({ status, labels, issue, specs, reviewDocs, comments }); // `infra` — тематическая метка и не даёт процессных прав. Ускоренный трек // доказывается тем же механическим признаком, что process-gate: в реальном // diff опубликованной ветки нет ни одного файла класса A — и только вне diff --git a/test/task-packet.test.mjs b/test/task-packet.test.mjs index 5cbca549..15b9c047 100644 --- a/test/task-packet.test.mjs +++ b/test/task-packet.test.mjs @@ -182,6 +182,26 @@ test('#632: statusless or returned infra issue without spec keeps the class A ba } }); +test('#632 r1: trivial issue in S6/S7 keeps class A rights without any spec artefact', () => { + // Форма реального trivial-бага #612: дефекты и AC в теле, без «## ТЗ», без + // docs/specs и без ревью ТЗ — короткий трек их не пишет (PROCESS §5.1). + const body = '## Дефект 1\nx\n\n## Дефект 2\ny\n\n## AC\n- AC1. a\n- AC2. b\n- AC3. c\n'; + for (const labels of [['bug', 'P2', 'trivial', 'S6-in-progress'], ['bug', 'P2', 'trivial', 'infra', 'S7-code-review']]) { + const packet = buildPacket({ + issue: { number: 612, title: 'trivial bug', state: 'OPEN', url: 'u', body }, + labels, + branch: { name: 'issue/612-x', tip: 'e'.repeat(40), base: 'f'.repeat(40), ahead: 1, behind: 0, treeWithoutReviews: null, infrastructure: true }, + }); + assert.equal(packet.track, 'trivial', labels.join(',')); + assert.ok(packet.rights.some((l) => l.includes('продуктовый код трогать МОЖНО')), labels.join(',')); + assert.ok(packet.rights.every((l) => !l.includes('файлы класса A трогать НЕЛЬЗЯ')), labels.join(',')); + } + assert.deepEqual(productFlowEvidence({ status: 'S6-in-progress', labels: ['trivial'], issue: { body } }), + ['короткий трек trivial (ТЗ не пишется, §5.1)']); + assert.deepEqual(productFlowEvidence({ status: 'S6-in-progress', labels: ['small', 'infra'], issue: { body } }), [], + 'только trivial: small несёт ТЗ в теле и доказывается разделом «## ТЗ»'); +}); + test('#517 AC5: AC берутся из тела issue, файл ТЗ — только когда в теле их нет', () => { const base = { issue: { number: 700, title: 'x', state: 'OPEN', url: 'u', body: '## ТЗ\n\n- AC1. Из тела\n' },