diff --git a/.github/governance/policy.json b/.github/governance/policy.json new file mode 100644 index 0000000..eeb93b4 --- /dev/null +++ b/.github/governance/policy.json @@ -0,0 +1,10 @@ +{ + "schema_version": 1, + "mode": "SHADOW", + "reviewers": [], + "owners": [], + "codex_app_ids": [], + "max_evidence_age_hours": 24, + "baseline_exceptions": false, + "merge_authorized": false +} diff --git a/.github/workflows/governance-acceptance.yml b/.github/workflows/governance-acceptance.yml new file mode 100644 index 0000000..5b84d16 --- /dev/null +++ b/.github/workflows/governance-acceptance.yml @@ -0,0 +1,207 @@ +name: Governance shadow engine + +on: + workflow_call: + inputs: + pr-number: + required: true + type: number + policy-revision: + description: Same immutable central commit as the reusable workflow reference + required: true + type: string + mode: + default: SHADOW + type: string + +permissions: + contents: read + pull-requests: read + issues: read + +jobs: + observe: + name: Governance shadow observation + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Observe native candidate evidence + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + PR_NUMBER: ${{ inputs.pr-number }} + POLICY_REVISION: ${{ inputs.policy-revision }} + MODE: ${{ inputs.mode }} + with: + script: | + // Trusted reusable workflow code only; never load candidate source. + const crypto = require('node:crypto'); + const digest = value => crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex'); + const revision = process.env.POLICY_REVISION; + const number = Number(process.env.PR_NUMBER); + const fail = code => { throw new Error(code); }; + const hash = value => typeof value === 'string' && /^[0-9a-f]{40}$/.test(value); + let report = {schema_version: 1, mode: 'SHADOW', merge_authorized: false, + observation: 'UNKNOWN', reason: 'READ_OR_INTEGRITY_FAILURE'}; + try { + if (process.env.MODE !== 'SHADOW' || !hash(revision) + || !Number.isSafeInteger(number) || number < 1) fail('INVALID_INPUT'); + const {data: content} = await github.rest.repos.getContent({owner: 'QuantStrategyLab', + repo: '.github', path: '.github/governance/policy.json', ref: revision}); + if (content.encoding !== 'base64' || typeof content.content !== 'string') fail('INVALID_POLICY'); + const policy = JSON.parse(Buffer.from(content.content, 'base64').toString('utf8')); + if (policy.schema_version !== 1 || policy.mode !== 'SHADOW' || policy.merge_authorized !== false + || policy.baseline_exceptions !== false || !Array.isArray(policy.reviewers) + || !Array.isArray(policy.owners) || ![...policy.reviewers, ...policy.owners].every(x => typeof x === 'string') + || !Array.isArray(policy.codex_app_ids) + || !policy.codex_app_ids.every(x => Number.isSafeInteger(x) && x > 0) + || !Number.isInteger(policy.max_evidence_age_hours) + || policy.max_evidence_age_hours < 1 || policy.max_evidence_age_hours > 24) fail('INVALID_POLICY'); + const policyDigest = digest(policy); + const args = {...context.repo, pull_number: number}; + const readPR = async () => (await github.rest.pulls.get(args)).data; + const readEvidence = async () => ({ + reviews: await github.paginate(github.rest.pulls.listReviews, {...args, per_page: 100}), + comments: await github.paginate(github.rest.issues.listComments, + {...context.repo, issue_number: number, per_page: 100}) + }); + const binding = pr => [pr.id, pr.state, pr.head?.sha, pr.base?.sha, pr.base?.ref, + pr.base?.repo?.id, pr.head?.repo?.id, pr.user?.login, pr.changed_files, pr.additions, pr.deletions]; + const pr = await readPR(); + if (pr.state !== 'open' || !hash(pr.head?.sha) || !hash(pr.base?.sha) + || !Number.isSafeInteger(pr.changed_files) || pr.changed_files < 1 || pr.changed_files >= 3000 + || !pr.user?.login || pr.base?.repo?.full_name?.toLowerCase() + !== `${context.repo.owner}/${context.repo.repo}`.toLowerCase()) fail('INVALID_CANDIDATE'); + const files = await github.paginate(github.rest.pulls.listFiles, {...args, per_page: 100}); + if (files.length !== pr.changed_files || new Set(files.map(f => f.filename)).size !== files.length) + fail('INCOMPLETE_FILES'); + function complete(file) { + if (!['added', 'modified', 'removed', 'renamed'].includes(file.status) + || typeof file.filename !== 'string' || !file.filename || file.filename.includes('\0') + || (file.status === 'renamed' && !file.previous_filename) + || !Number.isInteger(file.additions) || file.additions < 0 + || !Number.isInteger(file.deletions) || file.deletions < 0 + || typeof file.patch !== 'string' || !file.patch) return false; + let adds = 0, dels = 0, oldLeft = 0, newLeft = 0, hunk = false; + for (const line of file.patch.split('\n')) { + const m = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line); + if (m) { + if (oldLeft || newLeft) return false; + oldLeft = m[2] === undefined ? 1 : Number(m[2]); + newLeft = m[4] === undefined ? 1 : Number(m[4]); hunk = true; + } else if (line.startsWith('\\ No newline')) { + if (!hunk) return false; + } else { + if (!hunk) return false; + if (line[0] === '+') { adds++; newLeft--; } + else if (line[0] === '-') { dels++; oldLeft--; } + else if (line[0] === ' ') { oldLeft--; newLeft--; } + else return false; + if (oldLeft < 0 || newLeft < 0) return false; + } + } + return hunk && oldLeft === 0 && newLeft === 0 && adds === file.additions && dels === file.deletions; + } + if (!files.every(complete) + || files.reduce((n,f) => n + f.additions, 0) !== pr.additions + || files.reduce((n,f) => n + f.deletions, 0) !== pr.deletions) fail('INCOMPLETE_PATCH'); + const scope = digest(files.map(f => [f.filename, f.previous_filename || null, f.status, + f.additions, f.deletions, digest(f.patch)]).sort((a,b) => a[0].localeCompare(b[0]))); + const authority = /authoriz|approv|governance|acceptance|permission|secret|credential|token|account|iam|risk|trad(?:e|ing)|kelly|stop.?loss|deploy|publish|baseline|safety|mask|redact|门禁|审批|授权|风控|密钥|止损/i; + const sensitive = /(^|\/)(\.github|tests?|fixtures?|config|deploy|runtime)(\/|$)|(^|\/)(AGENTS\.md|CODEOWNERS|SECURITY\.md)$|lock$|requirements|package\.json|pyproject\.toml/i; + const isR2 = files.some(f => [f.filename, f.previous_filename || ''].some(p => sensitive.test(p) || authority.test(p)) + || authority.test(f.patch) || (f.filename.endsWith('.md') && (f.deletions > 0 || f.status !== 'added'))); + const proseOnly = files.every(f => /^docs\/[a-z0-9/-]+\.md$/.test(f.filename) + && f.status === 'added' && f.deletions === 0 && !/[<`\[\]{}]|https?:|\bmust\b|\bshall\b|\brequir|必须|应当/i.test(f.patch)); + const tier = isR2 ? 'R2' : proseOnly ? 'R0' : 'R1'; + const evidence = await readEvidence(); + function codexObservation(comments) { + if (policy.codex_app_ids.length === 0) return 'NOT_CONFIGURED'; + const candidates = comments.filter(entry => entry.user?.type === 'Bot' + && policy.codex_app_ids.includes(entry.performed_via_github_app?.id) + && typeof entry.body === 'string' && entry.body.includes('codex-review-state:v1')); + if (candidates.some(entry => !Number.isSafeInteger(entry.id) + || !Number.isFinite(Date.parse(entry.updated_at)))) return 'UNKNOWN'; + candidates.sort((a, b) => Date.parse(b.updated_at || '') - Date.parse(a.updated_at || '') || b.id - a.id); + if (!candidates.length) return 'NOT_FOUND'; + if ((candidates[0].body.match(/codex-review-state:v1/g) || []).length !== 1) return 'UNKNOWN'; + const match = //.exec(candidates[0].body); + if (!match) return 'UNKNOWN'; + let review; + try { review = JSON.parse(match[1]).code_review; } catch { return 'UNKNOWN'; } + if (!review || review.commit_sha !== pr.head.sha) return 'STALE_OR_UNKNOWN'; + if (review.status === 'running') return 'IN_PROGRESS'; + if (review.status !== 'completed' || typeof review.has_any_findings !== 'boolean' + || typeof review.has_unresolved_findings !== 'boolean') return 'UNKNOWN'; + return review.has_unresolved_findings ? 'UNRESOLVED_FINDINGS' + : review.has_any_findings ? 'FINDINGS_REPORTED' : 'NO_REPORTED_FINDINGS'; + } + const now = Date.now(); + const findingsClear = findings => Array.isArray(findings) && findings.every(finding => + finding && (finding.kind === 'advisory' || (finding.kind === 'defect' + && ['resolved', 'false_positive', 'obsolete'].includes(finding.disposition)))); + function decision(kind, list, allowed) { + const candidates = []; + for (const entry of list) { + if (entry.user?.type !== 'User' || !allowed.includes(entry.user?.login) + || entry.user.login === pr.user.login) continue; + // No labels, provider completion, executor receipt or shared identity inference. + let record; + try { record = JSON.parse(entry.body); } catch { continue; } + if (record.schema_version !== 1 || record.kind !== kind || record.head !== pr.head.sha + || record.base !== pr.base.sha || record.scope !== scope || record.policy !== policyDigest + || record.repository !== `${context.repo.owner}/${context.repo.repo}` || record.pr !== number) continue; + const created = Date.parse(entry.submitted_at || entry.created_at); + const expires = Date.parse(record.expires_at); + const valid = Number.isFinite(created) && Number.isFinite(expires) && created <= now && expires > now + && expires > created && expires - created <= policy.max_evidence_age_hours * 3600000 + && now - created <= policy.max_evidence_age_hours * 3600000 + && (!entry.updated_at || entry.updated_at === entry.created_at) + && (kind !== 'review' || (entry.state === 'APPROVED' && entry.commit_id === pr.head.sha)) + && findingsClear(record.findings); + // Newer revoked/invalid records must dominate an older approval. + candidates.push({id: entry.id, valid, decision: record.decision}); + } + candidates.sort((a,b) => b.id - a.id); + return candidates[0]?.valid === true && candidates[0]?.decision === 'approve'; + } + const unresolvedByReviewer = new Map(); + for (const review of [...evidence.reviews].sort((a,b) => a.id - b.id)) { + if (review.user?.type !== 'User' || !policy.reviewers.includes(review.user.login) + || review.user.login === pr.user.login || review.commit_id !== pr.head.sha + || review.state === 'DISMISSED') continue; + const reviewer = review.user.login; + if (review.state === 'CHANGES_REQUESTED') { + unresolvedByReviewer.set(reviewer, true); + } else if (review.state === 'APPROVED' && decision('review', [review], policy.reviewers)) { + unresolvedByReviewer.set(reviewer, false); + } else { + try { + const record = JSON.parse(review.body); + if (record.head === pr.head.sha && record.base === pr.base.sha && record.scope === scope + && record.policy === policyDigest && !findingsClear(record.findings)) + unresolvedByReviewer.set(reviewer, true); + } catch { /* A comment without a record cannot clear a change request. */ } + } + } + const unresolved = [...unresolvedByReviewer.values()].some(Boolean); + const reviewed = !unresolved && decision('review', evidence.reviews, policy.reviewers); + const owned = decision('owner', evidence.comments, policy.owners); + const currentEvidence = await readEvidence(); + const current = await readPR(); + if (digest(binding(pr)) !== digest(binding(current)) || digest(evidence) !== digest(currentEvidence)) fail('STALE_SNAPSHOT'); + report = {...report, tier, file_count: files.length, head: pr.head.sha, base: pr.base.sha, + scope, policy: policyDigest, policy_revision: revision, + observation: tier === 'R0' ? 'PROSE_CANDIDATE' : !reviewed ? 'PENDING_REVIEW' + : tier === 'R2' && !owned ? 'PENDING_OWNER' : 'EVIDENCE_PRESENT', + reason: 'SHADOW_ONLY', independent_review: reviewed, owner_approval: owned, + semantic_classification: 'HEURISTIC_NOT_ACCEPTANCE', required_checks: 'NOT_EVALUATED', + event_freshness: 'SNAPSHOT_ONLY', baseline_exception: 'UNSUPPORTED', + codex_observation: codexObservation(evidence.comments)}; + } catch { + // Deliberately omit exception text, paths, patches, user bodies and API response data. + report = {...report, observation: 'UNKNOWN', reason: 'READ_OR_INTEGRITY_FAILURE'}; + core.setFailed('Shadow observation unavailable; existing gates remain authoritative'); + } + await core.summary.addHeading('Governance shadow observation').addCodeBlock(JSON.stringify(report, null, 2), 'json').write(); + core.setOutput('observation', report.observation); + return report; diff --git a/.github/workflows/governance-shadow-tests.yml b/.github/workflows/governance-shadow-tests.yml new file mode 100644 index 0000000..2057b1a --- /dev/null +++ b/.github/workflows/governance-shadow-tests.yml @@ -0,0 +1,17 @@ +name: Governance shadow tests +on: + pull_request: + push: + branches: [main] +permissions: + contents: read +jobs: + test-shadow: + name: Governance shadow tests + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + - run: node --test scripts/test-governance-shadow.cjs diff --git a/AGENTS.md b/AGENTS.md index b5e65b2..231776a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,7 +65,8 @@ Organization documentation rules are checked by `python scripts/check_style.py`, - Do not assume this file applies to other repositories; each repository carries its own `AGENTS.md` and `.github/engineering.md`. -- Do not add organization-wide automation or branch-protection policy here. +- Organization-wide automation or branch-protection policy requires an explicit + owner request and a reviewed change. - Do not weaken the style guide's rules on credentials, account identifiers or private infrastructure without a stated reason. diff --git a/README.md b/README.md index 39a292c..703b654 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,5 @@ Organization-wide files for QuantStrategyLab repositories. ## Contributing Open a pull request. Describe the problem, the change, and the effect on existing repositories. AI assistants and maintainers follow [AGENTS.md](AGENTS.md). + +- [Governance shadow engine](docs/governance-shadow.md) — reusable report-only review observations. diff --git a/README.zh-CN.md b/README.zh-CN.md index 8f2c3c1..e632add 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -21,3 +21,5 @@ QuantStrategyLab 各仓库共用的组织级文件。 ## 参与贡献 提交 pull request,说明要解决的问题、改了什么,以及对现有仓库的影响。AI 助手与维护者遵循 [AGENTS.md](AGENTS.md)。 + +- [Governance shadow engine](docs/governance-shadow.md) — reusable report-only review observations. diff --git a/docs/governance-shadow.md b/docs/governance-shadow.md new file mode 100644 index 0000000..30db82e --- /dev/null +++ b/docs/governance-shadow.md @@ -0,0 +1,83 @@ +# Governance shadow engine + +The reusable `governance-acceptance.yml` workflow observes pull requests without +changing merge authorization. It is public and can be called from repositories +under any owner. Both the workflow reference and `policy-revision` must use the +same full, reviewed central commit. There are no inherited secrets, candidate +checkouts, comment writes, status writes or merge operations. + +## Caller contract + +Use `pull_request` with opened, synchronize, reopened, edited and ready-for-review +events. Pass the event pull request number, `mode: SHADOW` and the central policy +revision. Use the dedicated `Governance shadow observation` job name; do not add +it to required checks or replace an existing check. Read-only contents, pull +requests and issues permissions suffice. Existing required checks remain separate. +The public workflow requires no organization access-setting change. Hosting +restrictions or an unavailable pin must be reported rather than bypassed. + +## Observations and evidence + +The engine reads all changed files and verifies file counts, aggregate additions +and deletions, and hunk lengths. Missing or binary patches are unknown. It hashes +the complete scope and central policy. Authority-related paths and prose, test +changes and modified Markdown are conservatively R2; ordinary source changes +are R1. Only narrowly constrained added explanatory prose is an R0 candidate. +These are heuristic observations, never semantic review acceptance. + +Native reviews and owner comments may carry a JSON record containing +`schema_version: 1`, `kind: review` or `kind: owner`, `repository`, numeric `pr`, +full `head` and `base`, `scope`, `policy`, `decision: approve` or `decision: revoke`, +`expires_at` and `findings: []`. Findings may also contain optional +`kind: advisory` advice, or `kind: defect` records with a reviewed disposition of +`resolved`, `false_positive` or `obsolete`; unresolved or malformed defects block +review evidence. Digest values come from the candidate summary. +An independent review also needs an APPROVED native review at that head. +Only human identities listed in the pinned policy qualify; the author cannot +approve their own work. Owner comments edited after creation do not qualify. +Newer invalid or revoked bound records dominate older approvals. No label or +provider availability supplies authorization. Default identity lists are empty, +so R1/R2 observations remain pending until a separately reviewed source policy +establishes identities. No identities or settings are provisioned by this pilot. +An unresolved change request survives comment-only reviews and can be cleared +only by a valid later approval from that reviewer or dismissal of the request. + +An optional `codex_app_ids` list enables a separate observation of the native +Codex summary comment. The source must be a bot posted through a configured +GitHub App and its review state must name the current head. The observation +distinguishes running, stale, malformed and unresolved findings. An empty list +leaves this observation unconfigured. This signal does not establish full +coverage or base binding, does not accept a review, and never authorizes merge. +Multiple receipt blocks in one comment are ambiguous and reported as unknown. + +The final read compares the candidate and evidence with the initial reads. +Failures produce UNKNOWN and a failed, non-required observation job. Reports use +fixed fields, candidate bindings and counts; paths, titles, patches, identities, +comment text and raw API errors are never exported. A green workflow means the +observer ran; it never means the pull request is approved. + +## Limits and promotion + +`merge_authorized` is always false. Required-check acceptance, baseline-red +exceptions, durable writer admission, atomic freshness, event completeness, +merge-queue support and identity independence from execution are not established. +Review/comment and base-only changes do not currently schedule a new observation; +a later pull request event refreshes the snapshot. This is suitable only for +report-only comparison, not enforcement. There is no persistent decision cache: +parallel or restarted observations independently re-read native state and cannot +mutate repository state. No deployment, publication or live operation is added. + +Keep the central PR and pilot callers under review. Callers pin the central +candidate commit; merge the reviewed central source before integrating callers. +The PR check run can exercise the pinned public candidate before that merge, +subject to repository Actions policy. Promote only after native runtime evidence +and separately approved identity, required-check and event-freshness changes. +Rollback consists of reverting only the new caller through a normal reviewed PR; +existing gates stay in place. + +## Local verification + +Run `node --test scripts/test-governance-shadow.cjs`, the existing Python checker +and secret-scan tests, and `python scripts/check_style.py --phase 2`. The new +`Governance shadow tests` workflow exercises the exact inline reusable code with +fake API responses. Offline tests do not prove hosted execution or adoption. diff --git a/scripts/test-governance-shadow.cjs b/scripts/test-governance-shadow.cjs new file mode 100644 index 0000000..dbce45d --- /dev/null +++ b/scripts/test-governance-shadow.cjs @@ -0,0 +1,232 @@ +const {test} = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const workflowPath = process.env.SHADOW_WORKFLOW || path.join(__dirname, '../.github/workflows/governance-acceptance.yml'); +const workflow = fs.readFileSync(workflowPath, 'utf8'); +const script = workflow.split(' script: |\n')[1]?.split('\n').map(x => x.slice(12)).join('\n'); +assert.ok(script, 'Reusable engine must contain executable observer'); +const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor; +const execute = new AsyncFunction('github', 'context', 'core', 'require', 'process', script); +const H = 'a'.repeat(40), B = 'b'.repeat(40), R = 'c'.repeat(40); +const policy = JSON.parse(fs.readFileSync(path.join(__dirname, '../.github/governance/policy.json'))); +function fixture() { + return {pr: {id: 1, state: 'open', head: {sha: H, repo: {id: 2}}, + base: {sha: B, ref: 'main', repo: {id: 2, full_name: '/sample'}}, + user: {login: '-author'}, changed_files: 1, additions: 1, deletions: 0}, + files: [{filename: 'docs/example.md', status: 'added', additions: 1, deletions: 0, patch: '@@ -0,0 +1 @@\n+Plain explanatory prose.'}], + evidence: {reviews: [], comments: []}, policy: structuredClone(policy)}; +} +async function observe(f = fixture()) { + const state = {failed: false, summary: '', calls: 0}; + const clone = x => structuredClone(x); + const github = {rest: {repos: {getContent: async () => { + if (f.apiError) throw new Error('private token '); + return {data: {encoding: 'base64', content: Buffer.from(JSON.stringify(f.policy)).toString('base64')}}; + }}, pulls: {get: async () => ({data: clone(++state.calls > 1 && f.current || f.pr)}), + listFiles: 'files', listReviews: 'reviews'}, issues: {listComments: 'comments'}}, + paginate: async method => clone(method === 'files' ? f.files : f.evidence[method])}; + const core = {setFailed: () => {state.failed = true;}, setOutput: () => {}, summary: { + addHeading() {return this;}, addCodeBlock(s) {state.summary = s; return this;}, async write() {}}}; + const report = await execute(github, {repo: {owner: '', repo: 'sample'}}, core, require, + {env: {MODE: f.mode || 'SHADOW', POLICY_REVISION: R, PR_NUMBER: '1'}}); + assert.equal(report.merge_authorized, false); + assert.ok(!state.summary.includes('private token')); + return {report, state}; +} +test('Narrow added prose produces an observation, never merge authorization', async () => { + const {report} = await observe(); assert.equal(report.tier, 'R0'); assert.equal(report.observation, 'PROSE_CANDIDATE'); +}); +test('Ordinary code needs independent review', async () => { + const f = fixture(); f.files[0].filename = 'src/example.js'; + assert.equal((await observe(f)).report.observation, 'PENDING_REVIEW'); +}); +function codexFixture() { + const f = fixture(); f.files[0].filename = 'src/example.js'; f.policy.codex_app_ids = [12345]; + const record = {code_review: {commit_sha: H, status: 'completed', + has_any_findings: false, has_unresolved_findings: false}}; + const entry = {id: 11, user: {type: 'Bot'}, performed_via_github_app: {id: 12345}, + updated_at: new Date().toISOString(), body: ``}; + f.evidence.comments = [entry]; + return {f, entry, record}; +} +test('Codex current-head receipt is observation only, never approval', async () => { + const {f} = codexFixture(); const {report} = await observe(f); + assert.equal(report.codex_observation, 'NO_REPORTED_FINDINGS'); + assert.equal(report.observation, 'PENDING_REVIEW'); + assert.equal(report.independent_review, false); +}); +for (const [mutation, expected] of [['stale', 'STALE_OR_UNKNOWN'], ['running', 'IN_PROGRESS'], + ['unresolved', 'UNRESOLVED_FINDINGS'], ['resolved', 'FINDINGS_REPORTED'], + ['malformed', 'UNKNOWN'], ['untrusted', 'NOT_FOUND'], ['user', 'NOT_FOUND'], ['timestamp', 'UNKNOWN']]) { + test(`Codex receipt preserves untrusted or incomplete state: ${mutation}`, async () => { + const {f, entry, record} = codexFixture(); + if (mutation === 'stale') record.code_review.commit_sha = 'd'.repeat(40); + if (mutation === 'running') record.code_review.status = 'running'; + if (mutation === 'unresolved') record.code_review.has_unresolved_findings = true; + if (mutation === 'resolved') record.code_review.has_any_findings = true; + if (mutation === 'untrusted') entry.performed_via_github_app.id = 54321; + if (mutation === 'user') entry.user.type = 'User'; + if (mutation === 'timestamp') entry.updated_at = 'invalid'; + entry.body = mutation === 'malformed' ? '' + : ``; + const {report} = await observe(f); + assert.equal(report.codex_observation, expected); + assert.equal(report.observation, 'PENDING_REVIEW'); + }); +} +test('Unconfigured Codex observer never trusts arbitrary receipts', async () => { + const {f} = codexFixture(); f.policy.codex_app_ids = []; + assert.equal((await observe(f)).report.codex_observation, 'NOT_CONFIGURED'); +}); +test('A malformed newer native receipt cannot revive an older clean receipt', async () => { + const {f, entry} = codexFixture(); + f.evidence.comments.push({...entry, id: 12, updated_at: 'invalid'}); + assert.equal((await observe(f)).report.codex_observation, 'UNKNOWN'); +}); +for (const duplicate of ['unresolved', 'malformed']) { + test(`Duplicate Codex receipt blocks an ambiguous observation: ${duplicate}`, async () => { + const {f, entry, record} = codexFixture(); + record.code_review.has_unresolved_findings = true; + entry.body += duplicate === 'unresolved' + ? `\n` + : '\n'; + assert.equal((await observe(f)).report.codex_observation, 'UNKNOWN'); + }); +} +for (const filename of ['.github/workflows/example.yml', 'tests/example.js', 'docs/acceptance-policy.md', 'AGENTS.md']) { + test(`Sensitive scope is R2: ${filename}`, async () => { + const f = fixture(); f.files[0].filename = filename; + assert.equal((await observe(f)).report.tier, 'R2'); + }); +} +test('Authority-changing Markdown is R2', async () => { + const f = fixture(); f.files[0].patch = '@@ -0,0 +1 @@\n+Approval is optional.'; + assert.equal((await observe(f)).report.tier, 'R2'); +}); +for (const mutation of ['missing', 'truncated', 'count', 'binary', 'duplicate', 'rename', 'api', 'mode', 'policy']) { + test(`Unknown integrity never becomes valid: ${mutation}`, async () => { + const f = fixture(); + if (mutation === 'missing') f.files = []; + if (mutation === 'truncated') f.files[0].patch = '@@ -0,0 +1,2 @@\n+Only one line'; + if (mutation === 'count') f.pr.additions = 2; + if (mutation === 'binary') delete f.files[0].patch; + if (mutation === 'duplicate') {f.files.push(f.files[0]); f.pr.changed_files = 2;} + if (mutation === 'rename') f.files[0].status = 'renamed'; + if (mutation === 'api') f.apiError = true; + if (mutation === 'mode') f.mode = 'ENFORCE'; + if (mutation === 'policy') f.policy.merge_authorized = true; + const {report, state} = await observe(f); + assert.equal(report.observation, 'UNKNOWN'); assert.equal(state.failed, true); + }); +} +for (const drift of ['head', 'base', 'ref', 'files', 'author']) { + test(`Re-read rejects snapshot drift: ${drift}`, async () => { + const f = fixture(); f.current = structuredClone(f.pr); + if (drift === 'head') f.current.head.sha = 'd'.repeat(40); + if (drift === 'base') f.current.base.sha = 'e'.repeat(40); + if (drift === 'ref') f.current.base.ref = 'other'; + if (drift === 'files') f.current.changed_files = 2; + if (drift === 'author') f.current.user.login = '-other'; + assert.equal((await observe(f)).report.observation, 'UNKNOWN'); + }); +} +async function recordFixture() { + const f = fixture(); f.files[0].filename = '.github/example.yml'; + f.policy.reviewers = ['-reviewer', '-other']; f.policy.owners = ['-owner']; + const {report} = await observe(f); + const record = {schema_version: 1, kind: 'review', repository: '/sample', pr: 1, + head: H, base: B, scope: report.scope, policy: report.policy, decision: 'approve', findings: [], + expires_at: new Date(Date.now() + 3600000).toISOString()}; + const created_at = new Date(Date.now() - 60000).toISOString(); + f.evidence.reviews = [{id: 1, user: {login: '-reviewer', type: 'User'}, state: 'APPROVED', commit_id: H, + submitted_at: created_at, body: JSON.stringify(record)}]; + return {f, record, created_at}; +} +test('Configured independent review still requires distinct owner evidence for R2', async () => { + const {f} = await recordFixture(); assert.equal((await observe(f)).report.observation, 'PENDING_OWNER'); +}); +for (const invalid of ['expired', 'base', 'scope', 'policy', 'author', 'bot', 'dismissed', 'defect', 'revoked']) { + test(`Reject invalid independent review: ${invalid}`, async () => { + const {f, record} = await recordFixture(); + if (invalid === 'expired') record.expires_at = new Date(Date.now() - 1000).toISOString(); + if (['base', 'scope', 'policy'].includes(invalid)) record[invalid] = 'f'.repeat(40); + if (invalid === 'author') {f.policy.reviewers.push(f.pr.user.login); f.evidence.reviews[0].user.login = f.pr.user.login;} + if (invalid === 'bot') f.evidence.reviews[0].user.type = 'Bot'; + if (invalid === 'dismissed') f.evidence.reviews[0].state = 'DISMISSED'; + if (invalid === 'defect') record.findings = [{severity: 'defect'}]; + if (invalid === 'revoked') record.decision = 'revoke'; + f.evidence.reviews[0].body = JSON.stringify(record); + assert.equal((await observe(f)).report.observation, 'PENDING_REVIEW'); + }); +} +test('Edited owner comment cannot authorize; subsequent revoke dominates approval', async () => { + const {f, record, created_at} = await recordFixture(); record.kind = 'owner'; + f.evidence.comments = [{id: 2, user: {login: '-owner', type: 'User'}, created_at, updated_at: created_at, + body: JSON.stringify(record)}]; + assert.equal((await observe(f)).report.observation, 'EVIDENCE_PRESENT'); + f.evidence.comments[0].updated_at = new Date().toISOString(); + assert.equal((await observe(f)).report.observation, 'PENDING_OWNER'); + f.evidence.comments[0].updated_at = created_at; + f.evidence.comments.push({...f.evidence.comments[0], id: 3, body: JSON.stringify({...record, decision: 'revoke'})}); + assert.equal((await observe(f)).report.observation, 'PENDING_OWNER'); +}); +test('Workflow has no checkout, secret inheritance, write permission or legacy status publisher', () => { + assert.doesNotMatch(workflow, /checkout|secrets:|: write|createCommitStatus|checks\.create|pull_request_target/); + assert.match(workflow, /timeout-minutes: 5/); +}); + +test('An unresolved concrete defect cannot be erased by another reviewer approval', async () => { + const {f, record} = await recordFixture(); + f.evidence.reviews.push({...f.evidence.reviews[0], id: 2, state: 'CHANGES_REQUESTED', + body: JSON.stringify({...record, findings: [{severity: 'defect'}]})}); + f.evidence.reviews.push({...f.evidence.reviews[0], id: 3, user: {login: '-other', type: 'User'}}); + assert.equal((await observe(f)).report.observation, 'PENDING_REVIEW'); +}); +test('A comment-only review does not clear a prior change request', async () => { + const {f, record} = await recordFixture(); + f.evidence.reviews.push({...f.evidence.reviews[0], id: 2, state: 'CHANGES_REQUESTED'}); + f.evidence.reviews.push({...f.evidence.reviews[0], id: 3, state: 'COMMENTED', body: 'Follow-up comment'}); + f.evidence.reviews.push({...f.evidence.reviews[0], id: 4, user: {login: '-other', type: 'User'}, + body: JSON.stringify(record)}); + const {report} = await observe(f); + assert.equal(report.independent_review, false); + assert.equal(report.observation, 'PENDING_REVIEW'); +}); +test('A valid later approval by the requesting reviewer clears the request', async () => { + const {f} = await recordFixture(); + f.evidence.reviews.push({...f.evidence.reviews[0], id: 2, state: 'CHANGES_REQUESTED'}); + f.evidence.reviews.push({...f.evidence.reviews[0], id: 3, state: 'COMMENTED', body: 'Follow-up'}); + f.evidence.reviews.push({...f.evidence.reviews[0], id: 4}); + const {report} = await observe(f); + assert.equal(report.independent_review, true); + assert.equal(report.observation, 'PENDING_OWNER'); +}); +test('A dismissed change request no longer blocks an independent approval', async () => { + const {f} = await recordFixture(); + f.evidence.reviews.push({...f.evidence.reviews[0], id: 2, state: 'DISMISSED'}); + f.evidence.reviews.push({...f.evidence.reviews[0], id: 3, + user: {login: '-other', type: 'User'}}); + assert.equal((await observe(f)).report.observation, 'PENDING_OWNER'); +}); +test('An expired approval cannot clear a prior change request', async () => { + const {f, record} = await recordFixture(); + f.evidence.reviews.push({...f.evidence.reviews[0], id: 2, state: 'CHANGES_REQUESTED'}); + record.expires_at = new Date(Date.now() - 1000).toISOString(); + f.evidence.reviews.push({...f.evidence.reviews[0], id: 3, body: JSON.stringify(record)}); + f.evidence.reviews.push({...f.evidence.reviews[0], id: 4, + user: {login: '-other', type: 'User'}}); + assert.equal((await observe(f)).report.observation, 'PENDING_REVIEW'); +}); +test('Parallel observations do not share mutable decisions', async () => { + const f = fixture(); f.apiError = true; + const [bad, good] = await Promise.all([observe(f), observe()]); + assert.equal(bad.report.observation, 'UNKNOWN'); + assert.equal(good.report.observation, 'PROSE_CANDIDATE'); +}); + +test('Optional style advice does not require repair', async () => { + const {f, record} = await recordFixture(); + record.findings = [{kind: 'advisory'}]; f.evidence.reviews[0].body = JSON.stringify(record); + assert.equal((await observe(f)).report.observation, 'PENDING_OWNER'); +});