From f6c76b7351b1d0b9a61954b0153a44bb098bc7f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 22:50:19 +0300 Subject: [PATCH] process: infrastructure without a track label runs as show (#695 r1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CODE-REVIEW-695-r1 Medium: PROCESS §5.1 says an infrastructure task (§1) without a track label reads as track:show, but neither the pipeline guard nor the task packet did that. - _process.yml guard: with no track:* and no small/trivial label, the diff of the task branch against dev (compare API) with no class A file gives the show cycle limit 2. A truncated compare answer (300 files) proves nothing and keeps the limit 4. - task-packet.mjs: an infrastructure packet names the track it runs on: «инфраструктурный · show» without a label, the owner's label otherwise. - Mutants guard-infra-keeps-ask-limit and packet-infra-track-ignores-show-default. Issue: #695 User-Visible: no Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd --- .github/workflows/_process.yml | 20 ++++++++++++++++++++ scripts/mutation-registry.mjs | 23 +++++++++++++++++++++++ scripts/task-packet.mjs | 12 ++++++++++-- test/review-doc-guard.test.mjs | 12 ++++++++++++ test/task-packet.test.mjs | 19 ++++++++++++++++--- 5 files changed, 81 insertions(+), 5 deletions(-) diff --git a/.github/workflows/_process.yml b/.github/workflows/_process.yml index a00aec35..c8f51e73 100644 --- a/.github/workflows/_process.yml +++ b/.github/workflows/_process.yml @@ -220,6 +220,26 @@ jobs: - документы: $new_blocking" fi echo "ветка материала: ${branch:-нет, читался dev}" + + # §5.1 (r1 #695): инфраструктурная задача (§1) без трековой метки + # идёт как `track:show` — лимит 2. Признак механический, как в §1: + # в диффе ветки против dev ни одного файла класса A. Guard историю + # не клонирует, поэтому пути — из compare API; 300 файлов — потолок + # ответа, и полный список тогда не доказан: остаётся лимит 4. + if [ -n "$branch" ] && ! has track:ship && ! has track:show && ! has track:ask \ + && ! has small && ! has trivial; then + changed=$(mktemp) + if gh api "repos/$REPO/compare/dev...$branch" --jq '.files[].filename' > "$changed" 2>/dev/null \ + && node --input-type=module -e ' + import { readFileSync } from "node:fs"; + import { classify } from "./scripts/process-gate.mjs"; + const files = readFileSync(process.argv[1], "utf8").split("\n").filter(Boolean); + process.exit(files.length > 0 && files.length < 300 && files.every((f) => classify(f) !== "A") ? 0 : 1); + ' "$changed"; then + limit=2 + echo "инфраструктурная задача без трековой метки — трек show (§5.1), лимит 2" + fi + fi fi # Отказ обязан быть виден в issue, а не только в логе прогона. diff --git a/scripts/mutation-registry.mjs b/scripts/mutation-registry.mjs index 0b2711df..541c7ff3 100644 --- a/scripts/mutation-registry.mjs +++ b/scripts/mutation-registry.mjs @@ -13334,6 +13334,29 @@ const MUTANT_DEFINITIONS = [ replace: ' if True: # mutant: concurrent flushes are not serialised\n', }], }, + // r1 #695: инфраструктура без трековой метки — трек show (§5.1). + { + id: 'guard-infra-keeps-ask-limit', + guard: 'node --test --test-name-pattern="r1 #695" test/review-doc-guard.test.mjs', + because: 'r1 #695: an infrastructure task without a track label reads as track:show (PROCESS §5.1); ' + + 'the guard must give it the show cycle limit 2, not the ask limit 4', + patches: [{ + file: '.github/workflows/_process.yml', + find: ' limit=2\n echo "инфраструктурная задача без трековой метки', + replace: ' limit=4 # mutant: infra keeps the ask limit\n echo "инфраструктурная задача без трековой метки', + }], + }, + { + id: 'packet-infra-track-ignores-show-default', + guard: 'node --test --test-name-pattern="r1 #695" test/task-packet.test.mjs', + because: 'r1 #695: the packet names the track an infrastructure task actually runs on — show ' + + 'without a label, the owner label otherwise', + patches: [{ + file: 'scripts/task-packet.mjs', + find: " const infraTrack = hasTrackLabel(labels) ? trackFromLabels(labels) : 'show';", + replace: ' const infraTrack = trackFromLabels(labels); // mutant: unlabelled infra reads as ask', + }], + }, ]; const mutationCardSource = readFileSync(join(repoRoot, 'src/houseplan-card.ts'), 'utf8'); diff --git a/scripts/task-packet.mjs b/scripts/task-packet.mjs index bd91fbc8..a4b58982 100644 --- a/scripts/task-packet.mjs +++ b/scripts/task-packet.mjs @@ -141,6 +141,10 @@ export function trackFromLabels(labels = []) { return 'ask'; } +/** Есть ли у задачи трековая метка — новая или прежняя (§5.1). */ +export const hasTrackLabel = (labels = []) => ['track:ship', 'track:show', 'track:ask', 'trivial', 'small'] + .some((label) => labels.includes(label)); + /** * Признаки продуктового S-flow (#632). Инфраструктурная задача входит в поток * сразу на S7 и никогда не несёт S1–S5, ТЗ и ревью ТЗ; поэтому любой из этих @@ -177,9 +181,13 @@ export function buildPacket(inputs) { // продуктового потока. const infrastructure = branch?.infrastructure === true && productFlow.length === 0; const infrastructureHint = branch == null && status == null && labels.includes('infra') && productFlow.length === 0; + // §5.1 (r1 #695): инфраструктурная задача без трековой метки идёт как + // `show`; явная метка владельца главнее. Маршрут при этом остаётся + // инфраструктурным — вход сразу на S7, без S1–S5. + const infraTrack = hasTrackLabel(labels) ? trackFromLabels(labels) : 'show'; const track = infrastructure - ? 'инфраструктурный' - : infrastructureHint ? 'инфраструктурный (предварительно; подтвердить путями/diff)' + ? `инфраструктурный · ${infraTrack}` + : infrastructureHint ? `инфраструктурный · ${infraTrack} (предварительно; подтвердить путями/diff)` : trackFromLabels(labels); const stage = status === 'S4-spec-review' || status === 'S3-spec' || status === 'S5-ready' ? 'spec' : 'code'; const verdict = lastVerdict(comments, reviewDocs, stage); diff --git a/test/review-doc-guard.test.mjs b/test/review-doc-guard.test.mjs index a844a56e..f0c66764 100644 --- a/test/review-doc-guard.test.mjs +++ b/test/review-doc-guard.test.mjs @@ -968,3 +968,15 @@ test('guard перечисляет docs/reviews деревом, а не contents // потолка нет. assert.match(guard, /contents\/docs\/reviews\/\$name\?ref=\$target/); }); + +test('r1 #695: guard даёт инфраструктуре без трековой метки лимит show (§5.1)', () => { + const workflow = readFileSync(new URL('../.github/workflows/_process.yml', import.meta.url), 'utf8'); + const guard = workflow.slice(workflow.indexOf('\n guard:'), workflow.indexOf('\n prepare:')); + assert.match(guard, /gh api "repos\/\$REPO\/compare\/dev\.\.\.\$branch" --jq '\.files\[\]\.filename'/, + 'признак инфраструктуры — пути диффа ветки против dev'); + assert.match(guard, /files\.every\(\(f\) => classify\(f\) !== "A"\)/, 'механический признак §1: ни одного файла класса A'); + assert.match(guard, /files\.length < 300/, 'обрезанный ответ compare инфраструктуру не доказывает'); + assert.match(guard, /; then\n\s+limit=2\n\s+echo "инфраструктурная задача без трековой метки/); + assert.match(guard, /! has track:ship && ! has track:show && ! has track:ask \\\n\s+&& ! has small && ! has trivial; then/, + 'явная метка трека главнее признака инфраструктуры'); +}); diff --git a/test/task-packet.test.mjs b/test/task-packet.test.mjs index 0e5af118..1c95d428 100644 --- a/test/task-packet.test.mjs +++ b/test/task-packet.test.mjs @@ -2,7 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { branchIsInfrastructure, buildPacket, evidenceFor, productFlowEvidence, extractAcceptanceCriteria, lastVerdict, ownerDecisions, renderPacket, rightsFor, - trackFromLabels, + trackFromLabels, hasTrackLabel, } from '../scripts/task-packet.mjs'; import { materialAnchorBlock } from '../scripts/review-doc-guard.mjs'; @@ -107,7 +107,7 @@ test('#562: statusless infra issue is the accelerated track ending at S7 review' branch: { name: 'issue/562-process', tip: 'e'.repeat(40), base: 'f'.repeat(40), ahead: 1, behind: 0, treeWithoutReviews: null, infrastructure: true }, }); assert.equal(packet.status, null); - assert.equal(packet.track, 'инфраструктурный'); + assert.equal(packet.track, 'инфраструктурный · show', 'r1 #695: инфраструктура без трековой метки — show (§5.1)'); const md = renderPacket(packet); assert.match(md, /инфраструктурный вход/); assert.match(md, /S7-code-review/); @@ -177,7 +177,7 @@ test('#632: statusless or returned infra issue without spec keeps the class A ba reviewDocs: [{ name: 'CODE-REVIEW-632-r1.md', text: 'Вердикт: жёлтый' }], }); assert.deepEqual(packet.productFlow, [], labels.join(',')); - assert.equal(packet.track, 'инфраструктурный', labels.join(',')); + assert.equal(packet.track, 'инфраструктурный · show', labels.join(',')); assert.ok(packet.rights.some((l) => l.includes('файлы класса A трогать НЕЛЬЗЯ')), labels.join(',')); assert.ok(packet.rights.every((l) => !l.includes('продуктовый код трогать МОЖНО')), labels.join(',')); } @@ -228,3 +228,16 @@ test('#695: трек по меткам — track:* главнее прежних assert.equal(trackFromLabels(['bug', 'P2']), 'ask'); assert.equal(trackFromLabels([]), 'ask'); }); + +test('r1 #695: инфраструктурная задача с явной меткой трека несёт её, без метки — show', () => { + const at = (labels) => buildPacket({ + issue: { number: 7, title: 'infra', state: 'OPEN', url: 'u', body: '' }, + labels, + branch: { name: 'issue/7-x', tip: 'e'.repeat(40), base: 'f'.repeat(40), ahead: 1, behind: 0, treeWithoutReviews: null, infrastructure: true }, + }).track; + assert.equal(at(['infra']), 'инфраструктурный · show'); + assert.equal(at(['infra', 'track:ask']), 'инфраструктурный · ask', 'метка владельца главнее'); + assert.equal(at(['infra', 'track:ship']), 'инфраструктурный · ship'); + assert.equal(hasTrackLabel(['bug', 'infra']), false); + assert.equal(hasTrackLabel(['small']), true); +});