diff --git a/scripts/task-packet.mjs b/scripts/task-packet.mjs index 1a2de1d4..f4e08954 100644 --- a/scripts/task-packet.mjs +++ b/scripts/task-packet.mjs @@ -24,7 +24,7 @@ import { classify } from './process-gate.mjs'; export const STATUS_LABELS = ['S1-new', 'S2-analysis', 'S3-spec', 'S4-spec-review', 'S5-ready', 'S6-in-progress', 'S7-code-review', 'S8-merged']; /** Что разрешено в статусе — по PROCESS.md, без домыслов. */ -export function rightsFor(status, labels = [], { infrastructure = false } = {}) { +export function rightsFor(status, labels = [], { infrastructure = false, infrastructureHint = false } = {}) { const blocked = labels.includes('blocked'); const exhausted = labels.includes('review-4'); const code = !infrastructure && ['S5-ready', 'S6-in-progress', 'S7-code-review'].includes(status); @@ -36,6 +36,9 @@ export function rightsFor(status, labels = [], { infrastructure = false } = {}) : infrastructure ? 'файлы класса A трогать НЕЛЬЗЯ; инфраструктурную реализацию МОЖНО вести сразу по issue (#562)' : 'продуктовый код трогать НЕЛЬЗЯ: статус не S5/S6/S7 (правило №1)'); + if (infrastructureHint) { + lines.push('метка infra — только подсказка, не доказательство и не право: до ветки проверь предполагаемые пути; без class A начинай сразу, при любом class A нужен продуктовый S-flow (#562)'); + } switch (status) { case 'S1-new': lines.push('следующий шаг: аналитика (S2) — оценки метками, критерий лёгкого трека, затем ТЗ'); break; case 'S2-analysis': lines.push('следующий шаг: ТЗ (S3); трек по умолчанию small — отказ от него обосновать названным критерием §5'); break; @@ -48,7 +51,9 @@ export function rightsFor(status, labels = [], { infrastructure = false } = {}) default: lines.push(infrastructure ? 'инфраструктурный вход: реализовать и проверить → push ветки → S7-code-review; ТЗ и S1–S6 не нужны (#562)' - : 'статусной метки нет — продуктовая задача вне процесса; вход — первая S*-метка владельца'); + : infrastructureHint + ? 'предварительный инфраструктурный вход: после проверки отсутствия class A реализовать → push ветки → S7-code-review; diff станет окончательным доказательством' + : 'статусной метки нет — продуктовая задача вне процесса; вход — первая S*-метка владельца'); } return lines; } @@ -116,8 +121,10 @@ export function buildPacket(inputs) { // доказывается тем же механическим признаком, что process-gate: в реальном // diff опубликованной ветки нет ни одного файла класса A. const infrastructure = branch?.infrastructure === true; + const infrastructureHint = branch == null && status == null && labels.includes('infra'); const track = infrastructure ? 'инфраструктурный' + : infrastructureHint ? 'инфраструктурный (предварительно; подтвердить путями/diff)' : labels.includes('trivial') ? 'trivial' : labels.includes('small') ? 'small' : 'полный'; const stage = status === 'S4-spec-review' || status === 'S3-spec' || status === 'S5-ready' ? 'spec' : 'code'; const verdict = lastVerdict(comments, reviewDocs, stage); @@ -132,7 +139,7 @@ export function buildPacket(inputs) { const unverified = acs.filter((a) => a.evidence.startsWith('без записи')); const packet = { issue: { number: issue.number, title: issue.title, state: issue.state, url: issue.url }, - status, track, labels, rights: rightsFor(status, labels, { infrastructure }), + status, track, labels, rights: rightsFor(status, labels, { infrastructure, infrastructureHint }), decisions: ownerDecisions(comments, owner), material: branch ? { branch: branch.name, tip: branch.tip, base: branch.base, ahead: branch.ahead, behind: branch.behind, diff --git a/test/task-packet.test.mjs b/test/task-packet.test.mjs index be2f629e..df16f96e 100644 --- a/test/task-packet.test.mjs +++ b/test/task-packet.test.mjs @@ -23,6 +23,9 @@ test('права выводятся из статусной метки по пр assert.ok(infrastructure.every((l) => !l.includes('продуктовый код трогать МОЖНО'))); assert.ok(rightsFor('S6-in-progress', ['infra']).some((l) => l.includes('продуктовый код трогать МОЖНО')), 'the thematic infra label alone must not override an S6 product status'); + const hint = rightsFor(null, ['infra'], { infrastructureHint: true }); + assert.ok(hint.some((l) => l.includes('только подсказка, не доказательство и не право'))); + assert.ok(hint.some((l) => l.includes('предварительный инфраструктурный вход'))); }); test('AC распознаются из таблицы ТЗ и из строк тела issue (#496)', () => { @@ -119,6 +122,19 @@ test('#562: the infra label alone never grants the accelerated track', () => { assert.ok(packet.rights.some((l) => l.includes('продуктовый код трогать МОЖНО'))); }); +test('#562: before a branch exists the infra label prompts classification but grants no rights', () => { + const packet = buildPacket({ + issue: { number: 1000, title: 'unpublished infra candidate', state: 'OPEN', url: 'u', body: '' }, + labels: ['infra'], + branch: null, + }); + assert.equal(packet.status, null); + assert.match(packet.track, /предварительно/); + assert.ok(packet.rights.some((l) => l.includes('без class A начинай сразу'))); + assert.ok(packet.rights.some((l) => l.includes('не доказательство и не право'))); + assert.ok(packet.rights.every((l) => !l.includes('продуктовый код трогать МОЖНО'))); +}); + test('#517 AC5: AC берутся из тела issue, файл ТЗ — только когда в теле их нет', () => { const base = { issue: { number: 700, title: 'x', state: 'OPEN', url: 'u', body: '## ТЗ\n\n- AC1. Из тела\n' },