From aea97fea65f2b89fe16826743b839ce279922a77 Mon Sep 17 00:00:00 2001 From: konard Date: Mon, 20 Apr 2026 23:12:55 +0000 Subject: [PATCH 1/2] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/netkeep80/pjson/issues/24 --- .gitkeep | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 0000000..ae2927b --- /dev/null +++ b/.gitkeep @@ -0,0 +1 @@ +# .gitkeep file auto-generated at 2026-04-20T23:12:54.984Z for PR creation at branch issue-24-4c32bd4f9a73 for issue https://github.com/netkeep80/pjson/issues/24 \ No newline at end of file From 4c333c14b5658b9de7763d3653f50e817e8a3347 Mon Sep 17 00:00:00 2001 From: konard Date: Mon, 20 Apr 2026 23:21:33 +0000 Subject: [PATCH 2/2] Sync repo-guard governance trust model --- .github/ISSUE_TEMPLATE/change-contract.yml | 42 +++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 2 + .github/workflows/repo-guard.yml | 2 +- .github/workflows/requirements.yml | 4 +- README.md | 14 ++++ docs/requirements-strict-profile.md | 17 +++++ repo-policy.json | 1 + requirements/README.md | 5 ++ scripts/validate-repo-guard-workflow.js | 82 +++++++++++++++++++++- 9 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/change-contract.yml diff --git a/.github/ISSUE_TEMPLATE/change-contract.yml b/.github/ISSUE_TEMPLATE/change-contract.yml new file mode 100644 index 0000000..6abca08 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/change-contract.yml @@ -0,0 +1,42 @@ +name: Change contract +description: Propose a pjson change with a repo-guard change contract. +title: "[change] " +body: + - type: markdown + attributes: + value: | + Describe the proposed change and include a YAML change contract block. + `authorized_governance_paths` is a privileged field: it belongs in the issue body, not the PR body. + - type: textarea + id: description + attributes: + label: Description + description: What does this change do and why? + validations: + required: true + - type: textarea + id: contract + attributes: + label: Change Contract + description: Paste a repo-guard YAML change contract block. + value: | + ```repo-guard-yaml + change_type: feature + scope: + - repo-policy.json + budgets: {} + anchors: + affects: [] + implements: [] + verifies: [] + # Uncomment and list governance files this issue sanctions changing. + # Only the contract in THIS issue body can unlock policy.paths.governance_paths. + # authorized_governance_paths: + # - repo-policy.json + must_touch: [] + must_not_touch: [] + expected_effects: + - Describe the expected effect + ``` + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6e08e79..93c6c8b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,6 +5,8 @@ ## Change Contract + ```repo-guard-yaml change_type: feature diff --git a/.github/workflows/repo-guard.yml b/.github/workflows/repo-guard.yml index aa56474..b8f0bf5 100644 --- a/.github/workflows/repo-guard.yml +++ b/.github/workflows/repo-guard.yml @@ -22,7 +22,7 @@ jobs: - name: Run repo-guard id: repo_guard - uses: netkeep80/repo-guard@88fdc275cbc9bd835cc20c638d83d832027182c7 + uses: netkeep80/repo-guard@99bf716da62c5d01070aa0d7e4d4f8031b43a351 with: mode: check-pr enforcement: blocking diff --git a/.github/workflows/requirements.yml b/.github/workflows/requirements.yml index 8981375..882c93f 100644 --- a/.github/workflows/requirements.yml +++ b/.github/workflows/requirements.yml @@ -10,6 +10,7 @@ on: - 'repo-policy.json' - '.github/workflows/**' - '.github/PULL_REQUEST_TEMPLATE.md' + - '.github/ISSUE_TEMPLATE/**' pull_request: paths: - 'requirements/**' @@ -19,6 +20,7 @@ on: - 'repo-policy.json' - '.github/workflows/**' - '.github/PULL_REQUEST_TEMPLATE.md' + - '.github/ISSUE_TEMPLATE/**' permissions: contents: read @@ -42,7 +44,7 @@ jobs: run: node scripts/validate-repo-guard-workflow.js - name: Валидация repo-guard policy - uses: netkeep80/repo-guard@88fdc275cbc9bd835cc20c638d83d832027182c7 + uses: netkeep80/repo-guard@99bf716da62c5d01070aa0d7e4d4f8031b43a351 with: mode: check-diff enforcement: blocking diff --git a/README.md b/README.md index 9698b06..2bf2cf4 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,20 @@ governance, requirements, docs, scripts и CI изменений. - `anchors.verifies` — требования, для которых PR добавляет тест или другой исполняемый check. +Governance trust model разделяет intent и authorization. PR body остаётся +каналом change intent: `change_type`, `scope`, `budgets`, `must_touch`, +`must_not_touch`, `expected_effects` и `anchors.*`. Поле +`authorized_governance_paths` в PR body игнорируется как untrusted source. +Governance edits санкционируются только через linked issue body: в issue должен +быть блок `repo-guard-yaml` с `authorized_governance_paths`, покрывающим +изменяемые файлы из `paths.governance_paths`. + +Для `check-pr` governance boundary читается из base branch repo-policy.json, а +не из версии policy внутри PR. Если trusted base boundary невозможно прочитать +или разобрать, gate должен fail closed и не принимать governance decision на +основе mutable head policy. Requirements-aware anchors, trace rules и evidence +rules остаются обязательным слоем поверх этой модели доверия. + Заявленные в `anchors.affects` требования должны сопровождаться evidence-файлом из списков `must_touch_any` в `repo-policy.json`. То же правило действует для `anchors.implements` и `anchors.verifies` со своими evidence surfaces; иначе diff --git a/docs/requirements-strict-profile.md b/docs/requirements-strict-profile.md index a3b6659..a7e0d25 100644 --- a/docs/requirements-strict-profile.md +++ b/docs/requirements-strict-profile.md @@ -39,6 +39,23 @@ pjson усиливает базовый профиль локальными пр | `anchors.verifies` принимают verification evidence в `tests/**`, `experiments/**`, `scripts/**`, `.github/workflows/**` | До появления C++ test tree исполняемые проверки живут в scripts и experiments | | Запрещены временные файлы, build outputs, logs и `node_modules/**` | Строгий профиль не должен пропускать operational мусор в PR diff | +## Governance trust model [FR-006](../requirements/functional/FR-006.json) + +`requirements-strict` остаётся requirements-aware policy: anchors, trace rules +и evidence rules не заменяются governance authorization, а выполняются вместе с +ней. Governance changes добавляют отдельную модель доверия: + +- PR body описывает только change intent (`change_type`, `scope`, `budgets`, + `must_touch`, `must_not_touch`, `expected_effects`, `anchors.*`); +- linked issue body является trusted channel для privileged field + `authorized_governance_paths`; +- `authorized_governance_paths` в PR body считается untrusted source и не + открывает правку файлов из `paths.governance_paths`; +- `check-pr` читает `paths.governance_paths` из trusted base policy, то есть из + `repo-policy.json` базовой ветки, а не из head policy текущего PR; +- если trusted base policy нельзя прочитать или распарсить, gate должен + fail closed вместо fallback на mutable policy из PR. + ## Оставшиеся bespoke проверки [FR-005](../requirements/functional/FR-005.json) `scripts/validate-requirements.js` остается нужным для проверок, которые diff --git a/repo-policy.json b/repo-policy.json index 6d69c1c..87a92da 100644 --- a/repo-policy.json +++ b/repo-policy.json @@ -27,6 +27,7 @@ "governance_paths": [ "repo-policy.json", ".github/PULL_REQUEST_TEMPLATE.md", + ".github/ISSUE_TEMPLATE/change-contract.yml", ".github/workflows/", "docs/requirements-strict-profile.md", "requirements/README.md", diff --git a/requirements/README.md b/requirements/README.md index 80fe345..c55e9c2 100644 --- a/requirements/README.md +++ b/requirements/README.md @@ -127,3 +127,8 @@ repo-guard check-diff 3. Ссылки на требования в Markdown разрешаются 4. Строгие архитектурные заголовки содержат bracketed requirement link 5. Измененные требования и declared PR anchors имеют evidence-файлы + +Governance-файлы проверяются дополнительным trust model слоем: PR body содержит +change intent, а `authorized_governance_paths` принимается только из linked +issue body. В `check-pr` список `paths.governance_paths` читается из policy +базовой ветки; если эта trusted boundary недоступна, gate должен fail closed. diff --git a/scripts/validate-repo-guard-workflow.js b/scripts/validate-repo-guard-workflow.js index ce74329..7f87e66 100644 --- a/scripts/validate-repo-guard-workflow.js +++ b/scripts/validate-repo-guard-workflow.js @@ -14,10 +14,13 @@ const fs = require('fs'); const path = require('path'); const PROJECT_ROOT = path.join(__dirname, '..'); -const EXPECTED_ACTION = 'netkeep80/repo-guard@88fdc275cbc9bd835cc20c638d83d832027182c7'; +const EXPECTED_ACTION_REF = '99bf716da62c5d01070aa0d7e4d4f8031b43a351'; +const EXPECTED_ACTION = `netkeep80/repo-guard@${EXPECTED_ACTION_REF}`; const EXPECTED_ENFORCEMENT = 'blocking'; let errors = 0; +let repoGuardWorkflowRefs = []; +let requirementsWorkflowRefs = []; function error(msg) { console.error(`ОШИБКА: ${msg}`); @@ -73,6 +76,32 @@ function requireArrayNotIncludes(label, values, forbidden) { } } +function extractRepoGuardActionRefs(text) { + const refs = []; + const pattern = /uses:\s+netkeep80\/repo-guard@([^\s#]+)/g; + let match; + while ((match = pattern.exec(text)) !== null) { + refs.push(match[1].replace(/^['"]|['"]$/g, '')); + } + return refs; +} + +function requireSingleExpectedRepoGuardAction(label, text) { + const refs = extractRepoGuardActionRefs(text); + if (refs.length !== 1) { + error(`${label}: должен быть ровно один uses: ${EXPECTED_ACTION}, найдено ${refs.length}`); + return refs; + } + const ref = refs[0]; + if (!/^[0-9a-f]{40}$/.test(ref)) { + error(`${label}: repo-guard Action должен быть pinned на commit SHA, получено '${ref}'`); + } + if (ref !== EXPECTED_ACTION_REF) { + error(`${label}: repo-guard Action должен использовать ${EXPECTED_ACTION_REF}, получено '${ref}'`); + } + return refs; +} + info('Проверка repo-guard workflow...'); const workflow = readText('.github/workflows/repo-guard.yml'); @@ -119,6 +148,7 @@ if (workflow !== null) { `uses: ${EXPECTED_ACTION}`, `workflow должен использовать pinned Action ${EXPECTED_ACTION}` ); + repoGuardWorkflowRefs = requireSingleExpectedRepoGuardAction('.github/workflows/repo-guard.yml', workflow); requireContains( '.github/workflows/repo-guard.yml', workflow, @@ -181,7 +211,10 @@ if (policy !== null) { const governancePaths = policy.paths && policy.paths.governance_paths; requireArrayIncludes('repo-policy.json paths.governance_paths', governancePaths, 'repo-policy.json'); requireArrayIncludes('repo-policy.json paths.governance_paths', governancePaths, '.github/PULL_REQUEST_TEMPLATE.md'); + requireArrayIncludes('repo-policy.json paths.governance_paths', governancePaths, '.github/ISSUE_TEMPLATE/change-contract.yml'); requireArrayIncludes('repo-policy.json paths.governance_paths', governancePaths, '.github/workflows/'); + requireArrayIncludes('repo-policy.json paths.governance_paths', governancePaths, 'requirements/README.md'); + requireArrayIncludes('repo-policy.json paths.governance_paths', governancePaths, 'requirements/schemas/'); requireArrayIncludes('repo-policy.json paths.governance_paths', governancePaths, 'scripts/validate-repo-guard-workflow.js'); requireArrayIncludes('repo-policy.json paths.governance_paths', governancePaths, 'docs/requirements-strict-profile.md'); requireArrayNotIncludes('repo-policy.json paths.governance_paths', governancePaths, 'scripts/validate-docs-headings.js'); @@ -250,12 +283,23 @@ info('Проверка PR template и README...'); const prTemplate = readText('.github/PULL_REQUEST_TEMPLATE.md'); if (prTemplate !== null) { requireContains('.github/PULL_REQUEST_TEMPLATE.md', prTemplate, '```repo-guard-yaml', 'шаблон должен содержать repo-guard YAML block'); + requireContains('.github/PULL_REQUEST_TEMPLATE.md', prTemplate, 'change intent', 'шаблон должен объяснять, что PR body хранит только change intent'); + requireContains('.github/PULL_REQUEST_TEMPLATE.md', prTemplate, 'authorized_governance_paths in this PR body is not trusted', 'шаблон должен запрещать доверять governance authorization из PR body'); + requireContains('.github/PULL_REQUEST_TEMPLATE.md', prTemplate, 'linked issue body', 'шаблон должен направлять governance authorization в linked issue body'); requireContains('.github/PULL_REQUEST_TEMPLATE.md', prTemplate, 'anchors:', 'шаблон должен содержать секцию anchors'); requireContains('.github/PULL_REQUEST_TEMPLATE.md', prTemplate, 'affects:', 'шаблон должен позволять объявлять affected anchors'); requireContains('.github/PULL_REQUEST_TEMPLATE.md', prTemplate, 'implements:', 'шаблон должен позволять объявлять implemented anchors'); requireContains('.github/PULL_REQUEST_TEMPLATE.md', prTemplate, 'verifies:', 'шаблон должен позволять объявлять verified anchors'); } +const issueTemplate = readText('.github/ISSUE_TEMPLATE/change-contract.yml'); +if (issueTemplate !== null) { + requireContains('.github/ISSUE_TEMPLATE/change-contract.yml', issueTemplate, '```repo-guard-yaml', 'issue form должен содержать repo-guard YAML block'); + requireContains('.github/ISSUE_TEMPLATE/change-contract.yml', issueTemplate, 'authorized_governance_paths', 'issue form должен документировать privileged authorization field'); + requireContains('.github/ISSUE_TEMPLATE/change-contract.yml', issueTemplate, 'privileged field', 'issue form должен явно называть authorized_governance_paths privileged field'); + requireContains('.github/ISSUE_TEMPLATE/change-contract.yml', issueTemplate, 'Only the contract in THIS issue body can unlock policy.paths.governance_paths', 'issue form должен фиксировать trusted issue channel'); +} + const readme = readText('README.md'); if (readme !== null) { requireContains('README.md', readme, '.github/workflows/repo-guard.yml', 'документация должна упоминать PR workflow repo-guard'); @@ -265,6 +309,10 @@ if (readme !== null) { requireContains('README.md', readme, 'anchors.affects', 'документация должна объяснять affected anchors в PR body'); requireContains('README.md', readme, 'anchors.implements', 'документация должна объяснять implemented anchors в PR body'); requireContains('README.md', readme, 'anchors.verifies', 'документация должна объяснять verified anchors в PR body'); + requireContains('README.md', readme, 'authorized_governance_paths', 'документация должна описывать issue-sourced governance authorization'); + requireContains('README.md', readme, 'linked issue body', 'документация должна фиксировать trusted issue channel'); + requireContains('README.md', readme, 'base branch repo-policy.json', 'документация должна фиксировать trusted base policy boundary'); + requireContains('README.md', readme, 'fail closed', 'документация должна описывать fail-closed поведение при недоступной trusted boundary'); } const strictProfileDoc = readText('docs/requirements-strict-profile.md'); @@ -272,6 +320,9 @@ if (strictProfileDoc !== null) { requireContains('docs/requirements-strict-profile.md', strictProfileDoc, 'requirements-strict', 'документ должен называть strict profile'); requireContains('docs/requirements-strict-profile.md', strictProfileDoc, 'requirement-json-req-refs-must-resolve', 'документ должен описывать перенос JSON trace ref resolution в repo-guard'); requireContains('docs/requirements-strict-profile.md', strictProfileDoc, 'scripts/validate-requirements.js', 'документ должен описывать оставшийся legacy validator'); + requireContains('docs/requirements-strict-profile.md', strictProfileDoc, 'authorized_governance_paths', 'документ должен описывать issue-sourced governance authorization'); + requireContains('docs/requirements-strict-profile.md', strictProfileDoc, 'trusted base policy', 'документ должен описывать trusted base branch policy boundary'); + requireContains('docs/requirements-strict-profile.md', strictProfileDoc, 'fail closed', 'документ должен описывать fail-closed behavior'); } const requirementsValidator = readText('scripts/validate-requirements.js'); @@ -306,6 +357,7 @@ if (requirementsWorkflow !== null) { `uses: ${EXPECTED_ACTION}`, `CI должен запускать pinned Action ${EXPECTED_ACTION} для repo-guard policy` ); + requirementsWorkflowRefs = requireSingleExpectedRepoGuardAction('.github/workflows/requirements.yml', requirementsWorkflow); requireContains( '.github/workflows/requirements.yml', requirementsWorkflow, @@ -336,12 +388,40 @@ if (requirementsWorkflow !== null) { '.github/PULL_REQUEST_TEMPLATE.md', 'CI должен перезапускаться при изменении PR template' ); + requireContains( + '.github/workflows/requirements.yml', + requirementsWorkflow, + '.github/ISSUE_TEMPLATE/**', + 'CI должен перезапускаться при изменении issue template' + ); requireContains( '.github/workflows/requirements.yml', requirementsWorkflow, 'repo-policy.json', 'CI должен перезапускаться при изменении repo-policy.json' ); + requireNotContains( + '.github/workflows/requirements.yml', + requirementsWorkflow, + 'git clone https://github.com/netkeep80/repo-guard.git', + 'CI должен использовать pinned Action, а не ручной clone' + ); + requireNotContains( + '.github/workflows/requirements.yml', + requirementsWorkflow, + 'node /tmp/repo-guard/src/repo-guard.mjs', + 'CI не должен запускать временно клонированный CLI напрямую' + ); +} + +if ( + repoGuardWorkflowRefs.length === 1 && + requirementsWorkflowRefs.length === 1 && + repoGuardWorkflowRefs[0] !== requirementsWorkflowRefs[0] +) { + error( + `.github/workflows/repo-guard.yml и .github/workflows/requirements.yml должны использовать один и тот же repo-guard pin: ${repoGuardWorkflowRefs[0]} != ${requirementsWorkflowRefs[0]}` + ); } console.log('');