mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
ci: specs live in the issue body — body digest in review anchors, gate without a spec file
The spec file solved exactly one problem — proving that a review verdict was passed on a given text — and created two: docs/specs/README.md conflicted between parallel tasks and served as a second, stale status dictionary, and every spec edit cost a commit, a push and a label. The proof moves into the pipeline. - review-doc-guard: normalizeIssueBody / issueBodyDigest (CRLF, trailing whitespace, trailing newlines), the anchor line `Тело issue: <sha256>`, anchorIssueBodyFrom, and issueBodyChanged — the finding "the spec changed after a green spec review", judged against the pipeline's own record in the last green SPEC-REVIEW, never against prose. - reusableGreenVerdict takes the current digest: reuse (#499) skips the model entirely, so without this a spec edit between rounds would pass unseen. Documents without the record (the whole backlog) keep judging by tree. - process.yml: the material step reads the body with `gh issue view` in the same run that fixes the material — the event snapshot describes a text the reviewer may never see; the digest goes into the anchors, into reuse and, when it differs, into the reviewer's prompt. - process-gate: rule 3 judges the text (a `## ТЗ` heading or an AC1) with the archived file still accepted; adding a new file under docs/specs/ warns — the directory is frozen. - task-packet reads AC from the body first, the archived file second. - PROCESS.md §2.3/§5/§7.1/§7.3/§10.5, AGENTS.md and docs/specs/README.md say so; the index table is gone with the long-standing §7.3 debt. Mutants: review-anchor-drops-issue-body, review-ignores-changed-spec-body, reuse-ignores-changed-issue-body, process-gate-requires-spec-file. Issue: #517 User-Visible: no
This commit is contained in:
+27
-16
@@ -16,6 +16,7 @@ import {
|
||||
checkIssueStatuses,
|
||||
checkReviewDocLimit,
|
||||
REVIEW_DOC_LIMIT,
|
||||
checkFrozenSpecs,
|
||||
checkSpecs,
|
||||
clampIssueBranchRange,
|
||||
classify,
|
||||
@@ -163,27 +164,37 @@ test('the branch name must agree with the Issue trailers', () => {
|
||||
assert.deepEqual(checkBranchRule('dev', [c]), []);
|
||||
});
|
||||
|
||||
test('a class A commit without a spec warns offline and fails with labels', () => {
|
||||
test('#517 AC3: ТЗ класса A судится по телу issue, архивный файл тоже годится', () => {
|
||||
const c = commit('Fix', 'Issue: #104', ['src/a.ts']);
|
||||
// Архивный файл ТЗ старой задачи — по-прежнему ТЗ.
|
||||
assert.deepEqual(checkSpecs([c], ['104-opening-ha-reference.md']), []);
|
||||
|
||||
// Офлайн отличить «ТЗ в теле issue» от «ТЗ нет» нельзя — только предупреждение.
|
||||
const offline = checkSpecs([c], ['111-something-else.md']);
|
||||
assert.equal(offline.length, 1);
|
||||
assert.equal(offline[0].level, 'warn');
|
||||
assert.equal(offline[0].rule, 3);
|
||||
// Тело с разделом «## ТЗ» либо с AC1 — это ТЗ, файла не требуется.
|
||||
assert.deepEqual(checkSpecs([c], [], () => '## ТЗ\n\nконтракт'), []);
|
||||
assert.deepEqual(checkSpecs([c], [], () => 'Проблема\n\n- AC1. Так и так'), []);
|
||||
assert.deepEqual(checkSpecs([c], null, () => '### ТЗ (лёгкий трек)'), []);
|
||||
|
||||
// С метками: small и trivial оправдывают отсутствие файла, их отсутствие — нет.
|
||||
assert.deepEqual(checkSpecs([c], [], () => ['small', 'S5-ready']), []);
|
||||
assert.deepEqual(checkSpecs([c], [], () => ['trivial', 'S5-ready']), []);
|
||||
const strict = checkSpecs([c], [], () => ['S5-ready']);
|
||||
assert.equal(strict.length, 1);
|
||||
assert.equal(strict[0].level, 'fail');
|
||||
assert.equal(strict[0].rule, 3);
|
||||
// Ни того, ни другого — предупреждение: настоящий рубеж — ревью ТЗ.
|
||||
const bare = checkSpecs([c], ['111-something-else.md'], () => 'просто описание бага');
|
||||
assert.equal(bare.length, 1);
|
||||
assert.equal(bare[0].level, 'warn');
|
||||
assert.equal(bare[0].rule, 3);
|
||||
assert.match(bare[0].msg, /в теле issue нет/);
|
||||
|
||||
// Метки недоступны — падать обратно на предупреждение: за недоступность
|
||||
// отвечает проверка 8, она уже краснеет fail closed.
|
||||
assert.equal(checkSpecs([c], [], () => null)[0].level, 'warn');
|
||||
// Офлайн тела нет — судить нечем, молчим (метки больше ничего не решают).
|
||||
assert.deepEqual(checkSpecs([c], [], () => null), []);
|
||||
assert.deepEqual(checkSpecs([c], null), []);
|
||||
});
|
||||
|
||||
test('#517: docs/specs заморожен — новый файл ТЗ даёт предупреждение, правка старого нет', () => {
|
||||
const added = { ...commit('Spec', 'Issue: #700', ['docs/specs/700-new.md']), addedFiles: ['docs/specs/700-new.md'] };
|
||||
const edited = { ...commit('Fix typo', 'Issue: #162', ['docs/specs/162-old.md']), addedFiles: [] };
|
||||
const readme = { ...commit('Archive note', 'Issue: #517', ['docs/specs/README.md']), addedFiles: ['docs/specs/README.md'] };
|
||||
const unknown = commit('Spec', 'Issue: #700', ['docs/specs/700-new.md']); // addedFiles не доказаны
|
||||
const out = checkFrozenSpecs([added, edited, readme, unknown]);
|
||||
assert.equal(out.length, 1);
|
||||
assert.equal(out[0].level, 'warn');
|
||||
assert.match(out[0].msg, /docs\/specs\/700-new\.md/);
|
||||
});
|
||||
|
||||
test('a rebase re-run may exceed the cycle limit in documents (#227)', () => {
|
||||
|
||||
Reference in New Issue
Block a user