mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 13:48:57 +00:00
process: infrastructure without a track label runs as show (#695 r1)
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
@@ -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, а не только в логе прогона.
|
||||
|
||||
@@ -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');
|
||||
|
||||
+10
-2
@@ -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);
|
||||
|
||||
@@ -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/,
|
||||
'явная метка трека главнее признака инфраструктуры');
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user