From cf7a9cc4fc2efebbe1718781867c1f51d0d068d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 16:51:08 +0300 Subject: [PATCH] =?UTF-8?q?fix(process):=20reconcile=20canon=20and=20hints?= =?UTF-8?q?=20with=20the=20pipeline=20after=20#707=E2=80=93#730=20(#748)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five places still described the pipeline as it was before code that is already in dev: - the S3 hint of the task packet told the author to push the branch, while the spec lives in the issue body (§2.3, #517) and nothing is pushed before S5 (§11.8); - process-gate printed «FAIL п.9 Gates: light» for a trailer nobody writes or reads, while §10.2 item 9 is the unimplemented release:prerelease verdict check. The check is removed; a contract test ties every RULES key to an implemented item of §10.2 and every finding number to a RULES key; - §10.4 item 4 demanded a heredoc in run:, while #723/#730 and their tests demand the opposite: commit messages echo line by line into a file, comment and summary texts come from code; - the ship merge comment, AUTHOR.md, REVIEWER.md and AGENTS.md named only the pre-beta document, though since #727 the night reads ship code first; - the nightly publication committed «docs: ship review for nightly … перед бетой» with the beta step's Issue: #696. It now has its own subject (the document name), body and Issue: #727; the beta message is unchanged. The browser-guard inventory note still said growth above 200 fails mutation-gate --check; since #699 it is a guideline and --check warns. Its counts now match the inventory: 205, lifecycle 90. Issue: #748 User-Visible: no Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd --- .github/workflows/_process.yml | 4 +-- .github/workflows/_ship-review.yml | 17 ++++++++-- AGENTS.md | 6 ++-- PROCESS.md | 8 +++-- docs/process/AUTHOR.md | 5 +-- docs/process/REVIEWER.md | 7 ++-- docs/testing-notes/mutation-browser-guards.md | 11 ++++--- scripts/process-gate.mjs | 11 ++----- scripts/task-packet.mjs | 2 +- test/process-gate.test.mjs | 33 +++++++++++++++++-- test/process-track.test.mjs | 4 +++ test/publish-push-refusal.test.mjs | 25 ++++++++++++++ test/ship-review.test.mjs | 4 ++- test/task-packet.test.mjs | 11 +++++++ 14 files changed, 115 insertions(+), 33 deletions(-) diff --git a/.github/workflows/_process.yml b/.github/workflows/_process.yml index cf1d062f..0cea6b26 100644 --- a/.github/workflows/_process.yml +++ b/.github/workflows/_process.yml @@ -1763,11 +1763,11 @@ jobs: # #696: `ship` в рамках (PROCESS.md §5) сливается без ревью модели # после зелёного лёгкого Validate. Комментарий — не вердикт ревью и # так себя не называет: ревью не было. Машинный маркер находит - # пакетное ревью диапазона перед бетой (ship-review.mjs, §11.7). + # пакетное ревью ship — ночное и перед бетой (ship-review.mjs, §11.7). verdict=green; high=0 short=$(printf '%s' "$MATERIAL" | cut -c1-8) cat > /tmp/ship-merge.md <.md\`. + **Слияние без ревью модели: трек ship (PROCESS.md §5, #696).** Правка укладывается в механические рамки ship, лёгкий Validate на \`$short\` зелёный${VALIDATE_URL:+ ([прогон]($VALIDATE_URL))}. Код до слияния не читал никто: его прочтёт пакетное ревью ship — ночью (\`docs/reviews/SHIP-REVIEW-<база>-dev-.md\`) и перед бетой то, что ночь не прочла (\`docs/reviews/SHIP-REVIEW-<тег>.md\`), PROCESS.md §11.7. ${SHIP_RISK} diff --git a/.github/workflows/_ship-review.yml b/.github/workflows/_ship-review.yml index 17789b38..7369663a 100644 --- a/.github/workflows/_ship-review.yml +++ b/.github/workflows/_ship-review.yml @@ -327,6 +327,17 @@ jobs: medium=$(jq -r '.medium' "$dir/result.json") low=$(jq -r '.low' "$dir/result.json") # Сообщение коммита — построчно в файл, без heredoc в `run:` (#730, как #723). + # #748: ночью — своё: имя документа без префикса, задача ночного режима. + if [ "$MODE" = "nightly" ]; then + night=${DOC##*/SHIP-REVIEW-} + subject="docs: nightly ship review ${night%.md}" + lead="Ночное пакетное ревью задач track:ship (PROCESS.md §11.7)." + issue="#727" + else + subject="docs: ship review for $TAG" + lead="Пакетное ревью задач track:ship перед бетой (PROCESS.md §11.7)." + issue="#696" + fi msg="$RUNNER_TEMP/ship-review-commit.txt" push_err="$RUNNER_TEMP/ship-review-push.stderr" for attempt in 1 2 3; do @@ -351,12 +362,12 @@ jobs: git add -- "$DOC" docs/reviews/INDEX.md git diff --cached --name-only | node scripts/review-doc-guard.mjs { - echo "docs: ship review for $TAG" + echo "$subject" echo "" - echo "Пакетное ревью задач track:ship перед бетой (PROCESS.md §11.7)." + echo "$lead" echo "Задачи: $ISSUES. Итог: High $high · Medium $medium · Low $low." echo "" - echo "Issue: #696" + echo "Issue: $issue" echo "User-Visible: no" } > "$msg" git -c user.name="claude[bot]" \ diff --git a/AGENTS.md b/AGENTS.md index 2e259d44..fb47ed59 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,8 +92,10 @@ Several track labels at once read as the strictest. A risky changed hunk `small` and `trivial` read as `show`. An **infrastructure** task — not a single class A file — skips analysis and spec and enters at `S7-code-review` (`PROCESS.md` §1). Every change is code-reviewed; on `ship` the review moves to a -batch review of the beta range before the tag (`ship-review.yml`, `PROCESS.md` -§11.7). The review pipeline prices each round by track (§10.4): no mutants run +batch review (`ship-review.yml`, `PROCESS.md` §11.7): nightly +(`SHIP-REVIEW--dev-.md`), and before the beta tag whatever the night +did not read (`SHIP-REVIEW-.md`). The review pipeline prices each round by +track (§10.4): no mutants run during development on any track — the whole registry runs nightly (#709); a rebase before review only on `ask` or when the branch does not merge cleanly into `dev`. Review checks scope, risks and the diff --git a/PROCESS.md b/PROCESS.md index 7a27d4e8..5cb0d956 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -1281,8 +1281,12 @@ Medium-находки вне скоупа задачи (#202), кладёт до умолчанию», — общее для всех таких файлов; 3. слияние в `dev` происходит **до** простановки `S8-merged`, иначе метка врёт в промежутке — она утверждает, что код в `dev`; -4. многострочный текст внутри `run:` — только через heredoc: строка с нулевым - отступом обрывает блок YAML, и скрипт обрезается без ошибки парсера. +4. многострочный текст в `run:` строкой не пишется: сообщение коммита — + `echo` построчно в файл, текст комментария или сводки — из скрипта (файлом + или `--summary`). Строка с нулевым отступом обрывает блок YAML, и скрипт + обрезается без ошибки парсера; текст из кода к тому же проверяет тест, а + текст из YAML — нет. Новые шаги heredoc не добавляют, старые переводятся при + переработке шага (#723, #730). **Workflow из ветки по умолчанию: тонкий файл и тело из `dev`** (#623, #716). Для событий `issues`, `schedule` и `workflow_run` GitHub исполняет workflow из diff --git a/docs/process/AUTHOR.md b/docs/process/AUTHOR.md index 301cf3cd..c48a1316 100644 --- a/docs/process/AUTHOR.md +++ b/docs/process/AUTHOR.md @@ -245,8 +245,9 @@ `S6-in-progress` до ревью; `show`/`ship` с чистым слиянием ребейзятся один раз, при слиянии ([§10.4](../../PROCESS.md#104-событийный-конвейер-метка-как-триггер)). - `ship` в рамках сливается без ревью модели; выход за рамки конвейер сам - переводит в `track:show`. Код `ship` читает пакетное ревью перед бетой - ([§10.4](../../PROCESS.md#104-событийный-конвейер-метка-как-триггер), + переводит в `track:show`. Код `ship` читает пакетное ревью: ночью + (`SHIP-REVIEW-<база>-dev-.md`) и перед бетой — то, что ночь не прочла + (`SHIP-REVIEW-<тег>.md`) ([§10.4](../../PROCESS.md#104-событийный-конвейер-метка-как-триггер), [§11.7](../../PROCESS.md#117-пакетное-ревью-ship-перед-бетой)). - Автор обязан дождаться вердикта, а не заканчивать сессию: `node scripts/wait-verdict.mjs --issue NN`, смотреть на метку, а не на diff --git a/docs/process/REVIEWER.md b/docs/process/REVIEWER.md index ec285894..be82e770 100644 --- a/docs/process/REVIEWER.md +++ b/docs/process/REVIEWER.md @@ -179,9 +179,10 @@ ## Пакетное ревью ship -- Задачи `track:ship` слиты без ревью модели; перед бетой `ship-review.yml` - читает их код одной сессией: по строке ТЗ каждой задачи и её коммитам - ([§11.7](../../PROCESS.md#117-пакетное-ревью-ship-перед-бетой)). +- Задачи `track:ship` слиты без ревью модели; их код одной сессией читает + `ship-review.yml` — ночью (`SHIP-REVIEW-<база>-dev-.md`) и перед бетой + то, что ночь не прочла (`SHIP-REVIEW-<тег>.md`): по строке ТЗ каждой задачи и + её коммитам ([§11.7](../../PROCESS.md#117-пакетное-ревью-ship-перед-бетой)). - Вопросы к задаче: делает ли код заявленное и только его, не ломает ли соседнее, не вышла ли правка из ship по смыслу ([§11.7](../../PROCESS.md#117-пакетное-ревью-ship-перед-бетой), diff --git a/docs/testing-notes/mutation-browser-guards.md b/docs/testing-notes/mutation-browser-guards.md index b8581b35..2a003d27 100644 --- a/docs/testing-notes/mutation-browser-guards.md +++ b/docs/testing-notes/mutation-browser-guards.md @@ -1,9 +1,10 @@ # Browser guards mutation registry (#659) This is the reviewed classification of every mutation witness that still needs a browser. -The executable limit is `200`; `mutation-gate --check`, `npm run inventory` and the unit -contract all read this same inventory. A new browser guard must be added deliberately under -one reason below, and its mutant `because` must explain the concrete browser-only invariant. +The guideline is `200` (#699: a guideline, not a wall); `mutation-gate --check`, +`npm run inventory` and the unit contract all read this same inventory. A new browser guard +must be added deliberately under one reason below, and its mutant `because` must explain +the concrete browser-only invariant. Converted witnesses are not listed here: their registry guard names an explicit `node --test` suite plus `test/mutation-browser-offload.test.mjs`, and every converted mutant is run once @@ -16,8 +17,8 @@ to prove that the Node witness actually kills it. | Paint, cascade and layer composition | 26 | The invariant depends on computed CSS, SVG paint, clipping, stacking or pixels produced by Chromium. | | Pointer geometry and trusted interaction | 45 | The invariant depends on hit testing, pointer capture, touch/keyboard dispatch or live DOM geometry. | | Responsive DOM layout | 36 | The invariant depends on measured element boxes, responsive breakpoints, native/HA dialog shells or focusable target size. | -| Custom-element and HA browser lifecycle | 89 | The invariant crosses Lit/custom-element lifecycle, browser storage/events, lazy loading or a complete HA-card state transition. | -| **Total** | **204 / 200** | Growth above the cap fails `mutation-gate --check`. | +| Custom-element and HA browser lifecycle | 90 | The invariant crosses Lit/custom-element lifecycle, browser storage/events, lazy loading or a complete HA-card state transition. | +| **Total** | **205 / 200** | Above the guideline `mutation-gate --check` warns rather than fails (#699); each guard above it is held by its own reason in this inventory and its `because`. | ## Measured effect diff --git a/scripts/process-gate.mjs b/scripts/process-gate.mjs index 55e637bb..0d767fbe 100644 --- a/scripts/process-gate.mjs +++ b/scripts/process-gate.mjs @@ -56,6 +56,8 @@ const CHANGELOGS = ['docs/CHANGELOG.md', 'docs/CHANGELOG.ru.md']; export const ALLOWED_STATUS = ['S5-ready', 'S6-in-progress', 'S7-code-review', 'S8-merged']; export const STRICT_STATUS = ['S5-ready', 'S6-in-progress', 'S7-code-review']; +// Номер — пункт PROCESS.md §10.2 (0 — служебный). Пункт из «Не реализовано» +// ключа не имеет (§10.2 п.9); соответствие держит test/process-gate.test.mjs (#748). export const RULES = { 0: 'классификация путей', 1: 'трейлер Issue', @@ -66,7 +68,6 @@ export const RULES = { 6: 'релизный коммит', 7: 'лимит документов ревью', 8: 'статус issue', - 9: 'Gates: light', 10: 'DoR по моменту коммита', }; @@ -118,7 +119,6 @@ export function makeCommit({ baselineReviewedLocal: one('Baseline-Reviewed-Local'), // #729: все значения, а не первое — «ровно один» судит правило 10. specDrafts: all('Spec-Draft'), - gates: one('Gates'), // null = вызывающий не доказал содержимое diff. Для stable release это // намеренно fail-closed: одного имени разрешённого version source мало. releaseSourceViolations, @@ -203,9 +203,6 @@ export function evaluateCommit(c) { if (violations.length) { fail(6, `релизный коммит содержит не-версионное изменение продукта: ${violations.slice(0, 3).join(', ')}`); } - if ((c.gates ?? '').toLowerCase() === 'light') { - fail(9, '«Gates: light» на релизном коммите запрещён'); - } return out; } @@ -228,10 +225,6 @@ export function evaluateCommit(c) { fail(5, 'изменена только генерируемая часть (класс D) без «Release: vX.Y.Z», «Baseline-Reviewed: <ссылка>» либо «Baseline-Reviewed-Local: sha256:<хеш>»'); } - if ((c.gates ?? '').toLowerCase() === 'light' && onlyD) { - fail(9, '«Gates: light» на коммите класса D запрещён'); - } - const unknown = c.files.filter((f) => classify(f) === '?'); if (unknown.length) warn(0, `путь вне классов A/B/C/D: ${unknown.slice(0, 3).join(', ')}`); diff --git a/scripts/task-packet.mjs b/scripts/task-packet.mjs index 22433c5d..781c8cd1 100644 --- a/scripts/task-packet.mjs +++ b/scripts/task-packet.mjs @@ -60,7 +60,7 @@ export function rightsFor(status, labels = [], { infrastructure = false, infrast switch (status) { case 'S1-new': lines.push('следующий шаг: аналитика (S2) — оценки и трек метками (по умолчанию track:show); на track:ship — строка «что меняется и чем проверить» под «## ТЗ», затем S5'); break; case 'S2-analysis': lines.push('следующий шаг: track:show — до трёх AC под «## ТЗ», затем S5; track:ask — ТЗ (S3) с названным критерием §5'); break; - case 'S3-spec': lines.push('следующий шаг: ТЗ готово → push ветки → метка S4-spec-review (метку после push)'); break; + case 'S3-spec': lines.push('следующий шаг: ТЗ в теле issue (раздел «## ТЗ», §7.1) → метка S4-spec-review; ветку до S5 не пушить (§11.8)'); break; case 'S4-spec-review': lines.push('идёт ревью ТЗ: ждать вердикт (scripts/wait-verdict.mjs), не править материал'); break; case 'S5-ready': lines.push('следующий шаг: ветка issue/NN-slug от dev, код по ТЗ, метка S6-in-progress'); break; case 'S6-in-progress': lines.push('следующий шаг: gate:small + смоки по AC → push ветки → метка S7-code-review (метку после push)'); break; diff --git a/test/process-gate.test.mjs b/test/process-gate.test.mjs index c7f9b533..a9baf956 100644 --- a/test/process-gate.test.mjs +++ b/test/process-gate.test.mjs @@ -16,6 +16,7 @@ import { checkIssueStatuses, checkReviewDocLimit, REVIEW_DOC_LIMIT, + RULES, checkFrozenSpecs, checkSpecs, clampIssueBranchRange, @@ -153,14 +154,40 @@ test('a release commit allows only proven canonical version declarations', () => assert.deepEqual(rules(evaluateCommit(mixed)), [6]); }); -test('Gates: light is refused on release and generated commits', () => { +// #748 AC1: номер правила гейта — номер пункта PROCESS.md §10.2. Проверка +// «Gates: light» жила под п.9 с первого коммита гейта, а канон под п.9 держит +// нереализованное (вердикт код-ревью в release:prerelease): вывод печатал +// «FAIL п.9 Gates: light» правила, которого в каноне нет. +test('#748 AC1: номера RULES — пункты §10.2 вне «Не реализовано», и только они', () => { + const canon = readFileSync(fileURLToPath(new URL('../PROCESS.md', import.meta.url)), 'utf8').replace(/\r\n?/g, '\n'); + const start = canon.indexOf('\n### 10.2 '); + const end = canon.indexOf('\n### 10.3 ', start); + assert.ok(start > 0 && end > start, 'PROCESS.md: раздел §10.2 найден'); + const section = canon.slice(start, end); + const cut = section.indexOf('\nНе реализовано'); + assert.ok(cut > 0, '§10.2: блок «Не реализовано» найден'); + const items = (text) => [...text.matchAll(/^(\d+)\. /gm)].map((m) => Number(m[1])).sort((a, b) => a - b); + const implemented = items(section.slice(0, cut)); + const pending = items(section.slice(cut)); + assert.ok(implemented.length >= 8 && pending.length >= 1, `§10.2: пункты разобраны (${implemented} / ${pending})`); + const keys = Object.keys(RULES).map(Number).filter((n) => n !== 0).sort((a, b) => a - b); + assert.deepEqual(keys, implemented, 'каждый ключ RULES, кроме 0, — реализованный пункт §10.2, и наоборот'); + for (const n of pending) assert.ok(!(n in RULES), `п.${n} §10.2 не реализован — ключа RULES[${n}] быть не должно`); + // Номер находки в коде — тоже ключ RULES: иначе вывод печатает «п.N undefined». + const source = readFileSync(fileURLToPath(new URL('../scripts/process-gate.mjs', import.meta.url)), 'utf8'); + const used = new Set([...source.matchAll(/\b(?:fail|warn)\((\d+),|\brule: (\d+)/g)].map((m) => Number(m[1] ?? m[2]))); + assert.ok(used.size >= 8, `номера находок разобраны: ${[...used]}`); + for (const n of used) assert.ok(n in RULES, `находка п.${n} без RULES[${n}]`); +}); + +test('#748 AC1: мёртвого трейлера «Gates: light» гейт не судит', () => { assert.deepEqual( rules(evaluateCommit(commit('Release v1.62.0', 'Release: v1.62.0\nGates: light', ['dist/a.js']))), - [9], + [], ); assert.deepEqual( rules(evaluateCommit(commit('Rebuild', 'Issue: #1\nBaseline-Reviewed: x\nGates: light', ['dist/a.js']))), - [9], + [], ); }); diff --git a/test/process-track.test.mjs b/test/process-track.test.mjs index f08e7f76..e7533206 100644 --- a/test/process-track.test.mjs +++ b/test/process-track.test.mjs @@ -884,6 +884,10 @@ test('#707 AC4: шаг трека на настоящем bash — ship, под const plain = box.run(decide, { OUT: '', STAGE: 'code', REUSE: 'false', SHIP: 'true', SHIP_RISK: '', MATERIAL: material, VALIDATE_URL: '' }); assert.equal(SHIP_MERGE_MARKER_RE.exec(plain.comment)?.[1], material); assert.equal(shipRiskFrom([{ body: plain.comment }]), null); + // #748: код ship первой читает ночь (#727), бета — то, что ночь не прочла; + // комментарий называет оба документа §11.7, а не один бета-документ. + assert.ok(plain.comment.includes('ночью (`docs/reviews/SHIP-REVIEW-<база>-dev-.md`)'), plain.comment); + assert.ok(plain.comment.includes('перед бетой то, что ночь не прочла (`docs/reviews/SHIP-REVIEW-<тег>.md`)'), plain.comment); }); // #755 AC3: дифф вида #741 — удалённый член интерфейса в участке perf — ship не diff --git a/test/publish-push-refusal.test.mjs b/test/publish-push-refusal.test.mjs index 494742ca..dfb2e074 100644 --- a/test/publish-push-refusal.test.mjs +++ b/test/publish-push-refusal.test.mjs @@ -536,6 +536,31 @@ test('#730 _ship-review.yml на настоящем bash: dev ушёл впер assert.match(git(box.origin, 'show', `dev:${SHIP_DOC}`), /\n### Материал пакетного ревью\n\n```\ntag v1\.79\.0-beta\.1\ncandidate c{40}\nbase v1\.78\.0\nissues 731,733\nhigh 0\nmedium 1\nlow 0\n/); }); +// #748 AC2: ночью (#727) тот же шаг писал в dev «docs: ship review for nightly … +// перед бетой» с трейлером задачи бета-шага. Ночное сообщение — своё: заголовок +// по имени документа, задача ночного режима; бета-сообщение (тест выше) прежнее. +test('#748 AC2 _ship-review.yml на настоящем bash: ночная публикация — свой заголовок, тело и Issue: #727', (t) => { + if (!hasTools()) { t.skip('bash/tar/jq/sha256sum недоступны'); return; } + const box = sandbox(tempRoot(t, 'hp-748-ship-')); + // Ночной документ называется по прошлому тегу кандидата: тег — на родителе. + git(box.work, 'tag', 'v1.78.0'); + writeFileSync(join(box.work, 'b.mjs'), 'export const b = 1;\n'); + commitAll(box.work, 'fix: b'); + git(box.work, 'push', '-q', 'origin', 'dev'); + const candidate = git(box.work, 'rev-parse', 'HEAD'); + const night = `v1.78.0-dev-${candidate.slice(0, 12)}`; + const doc = `docs/reviews/SHIP-REVIEW-${night}.md`; + const r = runShip(box, { TAG: 'nightly', MODE: 'nightly', DOC: doc, CANDIDATE: candidate, BASE: 'v1.78.0' }); + assert.equal(r.status, 0, r.stderr + r.stdout); + assert.equal(r.pushes, 1); + assert.equal(git(box.origin, 'log', '-1', '--format=%B', 'dev'), + `docs: nightly ship review ${night}\n\nНочное пакетное ревью задач track:ship (PROCESS.md §11.7).\n` + + 'Задачи: 731,733. Итог: High 0 · Medium 1 · Low 0.\n\nIssue: #727\nUser-Visible: no'); + assert.deepEqual(git(box.origin, 'show', '--name-only', '--format=', 'dev').split('\n').sort(), + ['docs/reviews/INDEX.md', doc], 'коммит несёт ночной документ и индекс'); + assert.match(git(box.origin, 'show', `dev:${doc}`), /\nmode nightly\n/); +}); + for (const [label, stderr, kind, reason] of [ ['право на workflow', remoteRejected('dev', WORKFLOW_REASON), PUSH_REFUSAL.workflow, WORKFLOW_REASON], ['прочий [remote rejected] (с заголовком Authorization и чужим токеном)', noisyRejected('dev'), PUSH_REFUSAL.remote, 'protected branch hook declined'], diff --git a/test/ship-review.test.mjs b/test/ship-review.test.mjs index f48c1612..40235f18 100644 --- a/test/ship-review.test.mjs +++ b/test/ship-review.test.mjs @@ -526,8 +526,10 @@ function shipSandbox(t) { 'echo "unexpected gh $*" >&2; exit 1', '', ].join('\n'), { mode: 0o755 }); - // Коммит публикации документа несёт `Issue: #696` — задача не ship. + // Коммит публикации документа несёт `Issue: #696` (бета) или `Issue: #727` + // (ночь, #748) — задачи не ship. writeFileSync(join(fake, 'issue-696.json'), JSON.stringify({ number: 696, title: 'Пакетное ревью ship', body: '', labels: [], comments: [] })); + writeFileSync(join(fake, 'issue-727.json'), JSON.stringify({ number: 727, title: 'Ночное пакетное ревью ship', body: '', labels: [], comments: [] })); const box = { root, origin, work, temp, fake, git, commit, push() { git(work, 'push', '-q', '--tags', 'origin', 'HEAD:dev'); git(work, 'fetch', '-q', 'origin'); }, diff --git a/test/task-packet.test.mjs b/test/task-packet.test.mjs index d30fede5..a98d1811 100644 --- a/test/task-packet.test.mjs +++ b/test/task-packet.test.mjs @@ -34,6 +34,17 @@ test('права выводятся из статусной метки по пр assert.ok(hint.some((l) => l.includes('предварительный инфраструктурный вход'))); }); +// #748 AC1: ТЗ живёт в теле issue (§2.3, #517), SPEC-REVIEW без ветки ложится в +// dev, а до S5 ветка не пушится (§11.8). Подсказка S3 звала пушить ветку. +test('#748 AC1: подсказка S3 — ТЗ в теле issue и S4-spec-review, без push ветки', () => { + const hint = rightsFor('S3-spec').find((l) => l.startsWith('следующий шаг:')); + assert.ok(hint, 'у S3 есть следующий шаг'); + assert.doesNotMatch(hint, /push ветки/); + assert.match(hint, /S4-spec-review/); + assert.match(hint, /§11\.8/); + assert.match(hint, /тел[еоа] issue/); +}); + test('AC распознаются из таблицы ТЗ и из строк тела issue (#496)', () => { const table = '| # | AC | Чем краснеет |\n|---|---|---|\n| AC1 | initial View ≤ 290 000 Б | budget |\n| **AC2** | план с мебелью | смок |\n'; assert.deepEqual(extractAcceptanceCriteria(table).map((a) => a.id), ['AC1', 'AC2']);