fix(process): reconcile canon and hints with the pipeline after #707–#730 (#748)

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
Claude
2026-10-01 16:49:55 +00:00
committed by claude[bot]
parent 146da62b0d
commit cf7a9cc4fc
14 changed files with 115 additions and 33 deletions
+2 -2
View File
@@ -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 <<EOF
**Слияние без ревью модели: трек ship (PROCESS.md §5, #696).** Правка укладывается в механические рамки ship, лёгкий Validate на \`$short\` зелёный${VALIDATE_URL:+ ([прогон]($VALIDATE_URL))}. Код до слияния не читал никто: его прочтёт пакетное ревью диапазона перед бетой (PROCESS.md §11.7), документ \`docs/reviews/SHIP-REVIEW-<тег>.md\`.
**Слияние без ревью модели: трек ship (PROCESS.md §5, #696).** Правка укладывается в механические рамки ship, лёгкий Validate на \`$short\` зелёный${VALIDATE_URL:+ ([прогон]($VALIDATE_URL))}. Код до слияния не читал никто: его прочтёт пакетное ревью ship — ночью (\`docs/reviews/SHIP-REVIEW-<база>-dev-<sha12>.md\`) и перед бетой то, что ночь не прочла (\`docs/reviews/SHIP-REVIEW-<тег>.md\`), PROCESS.md §11.7.
${SHIP_RISK}
+14 -3
View File
@@ -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]" \
+4 -2
View File
@@ -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-<base>-dev-<sha12>.md`), and before the beta tag whatever the night
did not read (`SHIP-REVIEW-<tag>.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
+6 -2
View File
@@ -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 из
+3 -2
View File
@@ -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-<sha12>.md`) и перед бетой — то, что ночь не прочла
(`SHIP-REVIEW-<тег>.md`) ([§10.4](../../PROCESS.md#104-событийный-конвейер-метка-как-триггер),
[§11.7](../../PROCESS.md#117-пакетное-ревью-ship-перед-бетой)).
- Автор обязан дождаться вердикта, а не заканчивать сессию:
`node scripts/wait-verdict.mjs --issue NN`, смотреть на метку, а не на
+4 -3
View File
@@ -179,9 +179,10 @@
## Пакетное ревью ship
- Задачи `track:ship` слиты без ревью модели; перед бетой `ship-review.yml`
читает их код одной сессией: по строке ТЗ каждой задачи и её коммитам
([§11.7](../../PROCESS.md#117-пакетное-ревью-ship-перед-бетой)).
- Задачи `track:ship` слиты без ревью модели; их код одной сессией читает
`ship-review.yml` — ночью (`SHIP-REVIEW-<база>-dev-<sha12>.md`) и перед бетой
то, что ночь не прочла (`SHIP-REVIEW-<тег>.md`): по строке ТЗ каждой задачи и
её коммитам ([§11.7](../../PROCESS.md#117-пакетное-ревью-ship-перед-бетой)).
- Вопросы к задаче: делает ли код заявленное и только его, не ломает ли
соседнее, не вышла ли правка из ship по смыслу
([§11.7](../../PROCESS.md#117-пакетное-ревью-ship-перед-бетой),
@@ -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
+2 -9
View File
@@ -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(', ')}`);
+1 -1
View File
@@ -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;
+30 -3
View File
@@ -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],
[],
);
});
+4
View File
@@ -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-<sha12>.md`)'), plain.comment);
assert.ok(plain.comment.includes('перед бетой то, что ночь не прочла (`docs/reviews/SHIP-REVIEW-<тег>.md`)'), plain.comment);
});
// #755 AC3: дифф вида #741 — удалённый член интерфейса в участке perf — ship не
+25
View File
@@ -536,6 +536,31 @@ test('#730 _ship-review.yml на настоящем bash: dev ушёл впер
assert.match(git(box.origin, 'show', `dev:${SHIP_DOC}`), /<!-- hp-ship-review-anchors -->\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'],
+3 -1
View File
@@ -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'); },
+11
View File
@@ -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']);