diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index 37f550cc..3be57742 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -1082,10 +1082,11 @@ jobs: legacy="docs/reviews/${marker}-${NUM}-r${CYCLE}.md" if [ ! -f "$SOURCE" ] && [ -f "$legacy" ]; then cp "$legacy" "$SOURCE"; fi test -s "$SOURCE" || { echo "::error::модель не оставила документ ревью"; exit 1; } - printf '%s' "$OUT" | jq -e ' + printf '%s' "$OUT" > "$RUNNER_TEMP/verdict.json" + jq -e ' (.verdict == "green" or .verdict == "yellow" or .verdict == "red") and (.high | type == "number") and (.medium | type == "number") - and (.summary | type == "string")' > "$RUNNER_TEMP/verdict.json" + and (.summary | type == "string")' "$RUNNER_TEMP/verdict.json" >/dev/null dir="$RUNNER_TEMP/review-result" mkdir -p "$dir" cp "$RUNNER_TEMP/review-prepared/prepared.json" "$dir/prepared.json" diff --git a/scripts/mutation-gate.mjs b/scripts/mutation-gate.mjs index d397fb7d..d37126dd 100644 --- a/scripts/mutation-gate.mjs +++ b/scripts/mutation-gate.mjs @@ -7858,6 +7858,17 @@ const MUTANT_DEFINITIONS = [ replace: ' ref: dev # mutant: moving material', }], }, + { + id: 'review-model-seals-verdict-as-boolean', + guard: 'node --test --test-name-pattern="#551" test/review-doc-guard.test.mjs', + because: '#551: результат JSON-schema review должен дойти до интеграции объектом; ' + + 'перенаправление stdout предиката jq записывает boolean true и роняет публикацию', + patches: [{ + file: '.github/workflows/process.yml', + find: ' printf \'%s\' "$OUT" > "$RUNNER_TEMP/verdict.json"', + replace: ' printf \'%s\' "$OUT" | jq -e \'true\' > "$RUNNER_TEMP/verdict.json" # mutant: boolean payload', + }], + }, { id: 'review-integration-skips-evidence-checksum', guard: 'node --test --test-name-pattern="#551" test/review-doc-guard.test.mjs', diff --git a/test/review-doc-guard.test.mjs b/test/review-doc-guard.test.mjs index 94401309..231fc1d4 100644 --- a/test/review-doc-guard.test.mjs +++ b/test/review-doc-guard.test.mjs @@ -768,6 +768,17 @@ test('#551: gates, модель и интеграция имеют незави assert.doesNotMatch(model, /validate-gate\.mjs/, 'ожидания Validate нет в бюджете модели'); assert.match(model, /needs: \[guard, prepare\]/); assert.match(integrate, /needs: \[guard, prepare, model_review\]/); + const seal = model.slice( + model.indexOf('- name: Запечатать результат модели'), + model.indexOf('- name: Передать результат интеграции'), + ); + assert.match(seal, /printf '%s' "\$OUT" > "\$RUNNER_TEMP\/verdict\.json"/, + 'structured verdict сохраняется как объект до проверки'); + assert.match(seal, + /and \(\.summary \| type == "string"\)' "\$RUNNER_TEMP\/verdict\.json" >\/dev\/null/, + 'jq валидирует сохранённый объект, не записывает boolean предиката вместо него'); + assert.doesNotMatch(seal, /jq -e '[\s\S]*> "\$RUNNER_TEMP\/verdict\.json"/, + 'stdout предиката jq не становится payload интеграции'); assert.match(model, /ref: \$\{\{ needs\.prepare\.outputs\.material_sha \}\}/, 'модель получает exact material, а не подвижную ветку');