Przeglądaj źródła

Merge pull request #4002 from deepseek-harness/turtle/blame-weighted-approval

feat(ci): scale approval weights with production blame
Turtle 1 tydzień temu
rodzic
commit
cc190b07d5

+ 6 - 0
.agents/notes/implemented/process/2026-09-11-production-blame-approval-weight.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-11-production-blame-approval-weight.md
+2026-09-11-production-blame-approval-weight.md: d9a223c2f332cc110ddd8f6a3f592febe0072987
+2026-09-11-production-blame-approval-weight.zh.md: f4dfc9d4fc5cefcccadb2785f58893ec92043ba7

+ 35 - 0
.agents/notes/implemented/process/2026-09-11-production-blame-approval-weight.md

@@ -0,0 +1,35 @@
+# Agent Note: Weight approvals by changed production-line ownership
+
+Status: implemented
+
+English | [中文](2026-09-11-production-blame-approval-weight.zh.md)
+
+## Problem
+
+A fixed one-point reviewer weight does not reflect authorship of the code a pull request changes. Directory-level ownership can reward unrelated code in the same folder and distort the contribution relevant to the review.
+
+## Decision
+
+The [approval policy](../../../../.github/review-ownership/README.md) scales a one-point approval by `min(2, 1 + 4 × ownedLines / totalLines)` over changed old production lines. Ownership of 0% gives one point, 12.5% gives 1.5 points, and 25% or more gives two points. Scores are not rounded before comparison with the two-point success threshold. The merge base supplies both classification and blame, while GitHub associates blame commits with reviewer accounts. Unlinked authors remain in the denominator. Additions have no prior owner and contribute no lines; an empty denominator produces no boost.
+
+The publisher marks the head pending before dependency setup and evaluation, so an interrupted history fetch cannot preserve an earlier success. It uses the live base branch and exact reviewed head, reads complete Git history without checking out PR code, and skips attribution when base points or blockers already decide the result. A maintained lexer separates comments from code across the repository’s source languages. Author lookups batch commits and all reviewers share one measurement. Existing [pending-status semantics](2026-09-09-blocked-weighted-approvals-remain-pending.md) and [review-event validation](2026-09-10-approval-review-workflow-identity.md) remain independent requirements.
+
+## Alternatives considered
+
+**A hard cutoff.** Linear weighting distinguishes partial ownership below 25%. The 25% cap lets a reviewer responsible for one quarter of the changed old code satisfy the two-point requirement; additional ownership does not increase their vote beyond that requirement.
+
+**Directory-weighted ownership.** Code elsewhere in a changed directory does not establish ownership of the lines under review.
+
+**Include newly added lines in the denominator.** Those lines have no merge-base owner and would dilute the authorship signal for existing code.
+
+**Match author names or email strings to reviewer logins.** Display names are not account identifiers, and one account can own commits under multiple emails. GitHub’s commit-author account association supplies that mapping.
+
+## Consequences
+
+History fetching dominates cold execution. Complete history is required before selecting the merge base: a shallow apparent ancestor can exclude changes from the denominator, so shallow classification cannot safely decide that blame is unnecessary. On 2026-09-11, local measurements of PRs #3969 and #3977 count 73 and 535 old production lines across 4 and 14 files. Three local runs take 0.31–0.32 seconds and 0.87–0.93 seconds; one batched author query adds approximately one second per PR. A fresh single-branch bare clone of master takes 47 seconds on the same host. These are host observations, not CI time guarantees.
+
+Blame measures last-touch authorship, not review quality or semantic expertise. The production classifier excludes unsupported source locations and file extensions; adding shipped source outside its inventory requires extending that classifier. Lexer classification is lexical rather than a semantic test of executable behavior. Evaluation errors retain the error status instead of silently awarding or withholding the boost.
+
+## Verification
+
+[Policy tests](../../../../.github/review-ownership/check-approval.test.mjs) cover the threshold, zero denominators, blockers, shared measurements, and failed attribution. [Author tests](../../../../.github/review-ownership/blame-ownership.test.mjs) cover batching, account aggregation, and incomplete responses. [Git integration tests](../../../../.github/review-ownership/test_blame_production.py) exercise merge bases, renames, deletions, mixed comment lines, exclusions, shallow history, and fetching without checking out PR code.

+ 35 - 0
.agents/notes/implemented/process/2026-09-11-production-blame-approval-weight.zh.md

@@ -0,0 +1,35 @@
+# Agent Note: 按变更生产代码行的归属调整审批权重
+
+Status: implemented
+
+[English](2026-09-11-production-blame-approval-weight.md) | 中文
+
+## Problem
+
+固定的一分评审权重无法反映评审者对拉取请求所修改代码的作者贡献。目录级归属可能奖励同一文件夹中的无关代码,扭曲与本次评审有关的贡献。
+
+## Decision
+
+[审批策略](../../../../.github/review-ownership/README.md) 按变更旧生产代码行的归属比例,以 `min(2, 1 + 4 × ownedLines / totalLines)` 调整一分批准的权重。归属比例为 0% 时计一分,12.5% 时计 1.5 分,25% 及以上时计两分。分数在与两分通过线比较前不做舍入。合并基点同时提供代码分类和 blame 依据,GitHub 将归属提交关联到评审者账号。无法关联账号的作者仍计入分母。新增行没有原作者,不计入行数;分母为空时不提升权重。
+
+发布器在依赖安装和评估前将头提交标记为待定,避免历史拉取中断后保留此前的成功状态。它使用实时 base 分支和被评审的精确 head,读取完整 Git 历史,不检出 PR 代码;基础分数或阻塞评审已决定结果时跳过归属计算。维护中的词法分析器区分仓库各源码语言中的注释与代码。作者查询按提交批量执行,所有评审者共用一次统计。现有的[待定状态语义](2026-09-09-blocked-weighted-approvals-remain-pending.zh.md)和[评审事件验证](2026-09-10-approval-review-workflow-identity.zh.md)仍是独立要求。
+
+## Alternatives considered
+
+**硬阈值。** 线性权重区分 25% 以下的部分归属。25% 封顶让负责四分之一变更旧代码的评审者满足两分要求;更多归属不会使其票数超过该要求。
+
+**目录加权归属。** 变更目录中其他代码的归属不能证明待评审代码行的归属。
+
+**将新增行计入分母。** 这些行没有合并基点上的作者,会稀释既有代码的作者贡献信号。
+
+**将作者姓名或邮箱字符串与评审者登录名匹配。** 显示名称不是账号标识,一个账号也可能使用多个邮箱提交代码。GitHub 的提交作者账号关联提供该映射。
+
+## Consequences
+
+冷启动的主要开销是拉取历史。选择合并基点前必须具备完整历史:浅历史中看似共同祖先的提交可能使部分变更被排除出分母,因此不能据此安全判定无需 blame。2026-09-11,在本机测量 PR #3969 和 #3977,得到 4 和 14 个文件中的 73 和 535 行旧生产代码。三次本地运行分别耗时 0.31–0.32 秒和 0.87–0.93 秒;每个 PR 的一次批量作者查询额外耗时约一秒。同一主机上,全新单分支裸克隆 master 耗时 47 秒。这些是本机观测值,不是 CI 耗时保证。
+
+Blame 衡量最后修改者归属,不代表评审质量或语义上的专业能力。生产代码分类器排除未支持的源码位置和扩展名;在其清单之外添加交付源码时,必须扩展分类器。词法分类不判断代码是否在语义上可执行。评估错误保留错误状态,不静默授予或取消加权。
+
+## Verification
+
+[策略测试](../../../../.github/review-ownership/check-approval.test.mjs)覆盖阈值、零分母、阻塞评审、共享统计和归属计算失败。[作者测试](../../../../.github/review-ownership/blame-ownership.test.mjs)覆盖批量查询、账号聚合和不完整响应。[Git 集成测试](../../../../.github/review-ownership/test_blame_production.py)验证合并基点、重命名、删除、混合注释行、排除规则、浅历史,以及不检出 PR 代码的拉取过程。

+ 1 - 0
.github/review-ownership/.gitignore

@@ -0,0 +1 @@
+__pycache__/

+ 9 - 5
.github/review-ownership/README.md

@@ -15,19 +15,23 @@ The [`weighted-approval` workflow](../workflows/weighted-approval.yml) publishes
 
 ## Approval scoring
 
-The weighted approval workflow exposes two pull-request checks. The `weighted approval publisher` Actions job reports whether evaluation and status publication completed, while the `weighted approval` commit status carries the approval decision on the pull request head. Branch rules must require only the commit status with GitHub Actions as its expected source; a context-only requirement can accept a same-named status from another integration. A completed evaluation returns `pending` below two approval points, while the pull request is a draft, or while a write-capable reviewer has an effective `CHANGES_REQUESTED` review; the blocker keeps the status pending even when counted approvals reach the threshold. It returns `success` only when the threshold is met, the pull request is ready, and no such blocker exists. If evaluation fails, the publisher writes an `error` status.
+The weighted approval workflow exposes two pull-request checks. The `weighted approval publisher` Actions job reports whether evaluation and status publication completed, while the `weighted approval` commit status carries the approval decision on the pull request head. Branch rules must require only the commit status with GitHub Actions as its expected source; a context-only requirement can accept a same-named status from another integration. The publisher marks the head pending before Python setup or lexer installation, so failed setup or an interrupted history fetch cannot leave a previous success in place. Setup and evaluation failures publish an error status. A completed evaluation returns `pending` below two approval points, while the pull request is a draft, or while a write-capable reviewer has an effective `CHANGES_REQUESTED` review; the blocker keeps the status pending even when counted approvals reach the threshold. It returns `success` only when the threshold is met, the pull request is ready, and no such blocker exists. If evaluation fails, the publisher writes an `error` status.
 
 Reviewers whose calculated base repository permission is `write` or `admin` count. The [approval policy](approval-policy.json) gives `@07akioni`, `@imccyu`, `@tianyicui`, `@tianyicui-bot`, `@turtle1999`, and `@turtle2099` two points each; every other write-capable reviewer gets one point. The pull-request author and reviewers without write permission do not count.
 
+A one-point approval receives weight `min(2, 1 + 4 × ownedLines / totalLines)` from modified or deleted old production-code lines, attributed by `git blame` at the merge base of the live base branch and exact reviewed head. Ownership of 0%, 12.5%, and 25% gives 1, 1.5, and 2 points; higher ownership remains capped at 2. The success threshold remains 2 total points, without rounding the score. New lines do not enter the denominator, and an empty denominator gives no boost. Existing two-point weights remain unchanged. GitHub commit-author accounts identify reviewers across author emails; unlinked authors remain in the denominator without contributing to a reviewer. The publisher logs measured ownership. It skips attribution when base approval points already meet the threshold or a blocking review exists. Displayed scores use at most two decimal places; the decision uses the unrounded score. The curve endpoints come from the policy’s default and required points.
+
+Production source means supported code files under `src/` in `packages/`, `apps/`, `python/`, and `native/`, plus the Desktop renderer, Python interpreter scripts, and committed runtime/packer launchers. The [classifier](blame-production.py) excludes documentation, tests, fixtures, snapshots, test support (including `src/testing/` and `src/testing.ts`), examples, generated source, dependencies, vendored code, declarations, comments, and blank lines. Pygments lexers distinguish comments from strings; mixed code/comment lines count, as do C preprocessor directives. Classification uses the old path and content, so changes to the PR’s file locations or generated headers cannot remove old lines from the denominator. Pure renames have no changed lines; renames with edits use the old path for blame.
+
 Each reviewer contributes only the current `APPROVED` or `CHANGES_REQUESTED` decision that GitHub returns. A `DISMISSED` record clears that reviewer's standing decision, including earlier approvals. Comment-only and pending records do not replace a decision. Reviews from deleted accounts and reviewers without current repository access do not count. The workflow does not invalidate an approval by its review commit; the repository's native pull-request rules own stale-review and latest-push requirements.
 
-The publisher runs when a pull request opens, synchronizes, reopens, becomes ready, or becomes a draft. Review submissions, edits, and dismissals run the no-permission [`weighted-approval-review-event` workflow](../workflows/weighted-approval-review-event.yml); its validated run title supplies the pull-request number to the default-branch publisher. The publisher validates the current head, fetches every review, and resolves current repository permission before publishing the status. Permission changes take effect on the next subscribed pull-request or review event.
+The publisher runs when a pull request opens, synchronizes, reopens, becomes ready, becomes a draft, or is edited, including a base-branch change. Pull-request and review events share one concurrency group per PR. Review submissions, edits, and dismissals run the no-permission [`weighted-approval-review-event` workflow](../workflows/weighted-approval-review-event.yml); its validated run title supplies the pull-request number to the default-branch publisher. The publisher validates the current head, fetches every review, and resolves current repository permission before publishing the status. Permission changes take effect on the next subscribed pull-request or review event.
 
 <a id="security"></a>
 
 ## Security
 
-The status-writing job checks out only the repository default branch. It does not check out or execute pull-request code and does not use repository secrets. The review-event workflow has no `GITHUB_TOKEN` permissions and passes only a decimal pull-request number in its run title. The publisher accepts only successful `pull_request_review` runs from the review-event workflow file, identified by `workflow_run.path`; GitHub can populate `workflow_run.name` with the expanded run title. The publisher rejects an invalid run title and a number that does not resolve to the workflow run's current pull-request head. Pull-request reviews are treated as API data and escaped in logs.
+All actions in the status-writing job are pinned to commit SHAs. The job checks out only the repository default branch. It does not check out or execute pull-request code and does not use repository secrets. Only when there is no blocker, base points are insufficient, and an approval has the policy’s default weight, it fetches complete history using the job token, passes Git objects to the trusted classifier as data, and resolves commit authors in batches of 50. Fetch credentials exist only in the Git child environment. Missing history, parsing failures, or incomplete author queries fail evaluation rather than producing a partial score. The review-event workflow has no `GITHUB_TOKEN` permissions and passes only a decimal pull-request number in its run title. The publisher accepts only successful `pull_request_review` runs from the review-event workflow file, identified by `workflow_run.path`; GitHub can populate `workflow_run.name` with the expanded run title. The publisher rejects an invalid run title and a number that does not resolve to the workflow run's current pull-request head. Pull-request reviews are treated as API data and escaped in logs.
 
 Approval policy changes take effect only after they merge into the default branch. This prevents an untrusted pull request from changing the program or policy for its own run.
 
@@ -35,10 +39,10 @@ Approval policy changes take effect only after they merge into the default branc
 
 ## Verification
 
-Run `pnpm run test:approval-policy` for policy parsing, effective review decisions, review-event validation, pagination, permission filtering, weighted scoring, blockers, drafts, status publication, and API failures. [Workflow tests](../../scripts/ci-workflow.spec.ts) pin the trusted checkout, no-permission review handoff, permissions, events, and commands. The repository gate graph runs the approval policy and workflow tests in CI.
+Run `pnpm run test:approval-policy` for policy parsing, effective review decisions, review-event validation, pagination, permission filtering, weighted scoring, blockers, drafts, status publication, and API failures. [Workflow tests](../../scripts/ci-workflow.spec.ts) pin the trusted checkout, no-permission review handoff, permissions, events, and commands. The repository gate graph runs the approval policy and workflow tests in CI. The Python SDK job runs `uv run --python 3.10 --with-requirements .github/review-ownership/requirements.txt python -m unittest discover -s .github/review-ownership -p 'test_*.py'` for real Git histories, lexers, renames, shallow-history rejection, and the publisher’s fetch/analysis integration.
 
 <a id="dev-note"></a>
 
 ## Dev Note
 
-None.
+[Production blame weighting](../../.agents/notes/implemented/process/2026-09-11-production-blame-approval-weight.md) records the scoring rationale and measured costs.

+ 86 - 0
.github/review-ownership/blame-ownership.mjs

@@ -0,0 +1,86 @@
+/** Merge-base production ownership for approval scoring; PR blobs are data, never programs. */
+import { execFile } from 'node:child_process'
+import { fileURLToPath } from 'node:url'
+import { promisify } from 'node:util'
+
+const exec = promisify(execFile)
+/** GitHub account syntax shared by policy and commit-author validation. */
+export const LOGIN = /^[A-Za-z0-9-]+(?:\[bot\])?$/u
+const SHA = /^[0-9a-f]{40}$/u
+
+/**
+ * Resolve GitHub author accounts for counted commits, retaining unknown authors in the denominator.
+ * @param {{totalLines: number, commitLines: Record<string, number>}} measurement Local blame counts.
+ * @param {string} repository Owner/name.
+ * @param {(path: string, options: object) => Promise<unknown>} api GitHub API caller.
+ * @returns {Promise<{totalLines: number, reviewerLines: Record<string, number>}>} Case-folded account counts.
+ */
+export async function resolveBlameAuthors(measurement, repository, api) {
+  const entries = Object.entries(measurement.commitLines)
+  if (!Number.isSafeInteger(measurement.totalLines) || measurement.totalLines < 0
+    || entries.some(([sha, count]) => !SHA.test(sha) || !Number.isSafeInteger(count) || count <= 0)
+    || entries.reduce((sum, [, count]) => sum + count, 0) !== measurement.totalLines) {
+    throw new Error('invalid production blame counts')
+  }
+  const [owner, name] = repository.split('/')
+  const reviewerLines = Object.create(null)
+  for (let offset = 0; offset < entries.length; offset += 50) {
+    const batch = entries.slice(offset, offset + 50)
+    const fields = batch.map(([sha], index) => `c${index}: object(oid: "${sha}") { ... on Commit { author { user { login } } } }`)
+    const response = await api('/graphql', {
+      method: 'POST',
+      body: {
+        query: `query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { ${fields.join('\n')} } }`,
+        variables: { owner, name },
+      },
+    })
+    if (response.errors?.length || !response.data?.repository) throw new Error('GitHub blame author lookup failed')
+    for (const [index, [, count]] of batch.entries()) {
+      const author = response.data.repository[`c${index}`]?.author
+      if (!author || !Object.hasOwn(author, 'user')) throw new Error('GitHub returned no blame commit author')
+      if (author.user === null) continue
+      const login = author.user.login
+      if (typeof login !== 'string' || !LOGIN.test(login)) {
+        throw new Error('GitHub returned an invalid blame author login')
+      }
+      const key = login.toLowerCase()
+      reviewerLines[key] = (reviewerLines[key] ?? 0) + count
+    }
+  }
+  return { totalLines: measurement.totalLines, reviewerLines }
+}
+
+/**
+ * Fetch complete history without checking out the PR and measure its old production lines once.
+ * @param {{repository: string, number: number, headSha: string}} pull Reviewed pull request.
+ * @param {(path: string, options?: object) => Promise<unknown>} api GitHub API caller.
+ * @returns {Promise<{totalLines: number, reviewerLines: Record<string, number>}>} Production ownership.
+ */
+export async function productionOwnership(pull, api) {
+  const current = await api(`/repos/${pull.repository}/pulls/${pull.number}`)
+  if (current.head?.sha !== pull.headSha || !SHA.test(pull.headSha) || typeof current.base?.ref !== 'string') {
+    throw new Error('pull request changed or has no valid base branch')
+  }
+  const baseRef = `refs/heads/${current.base.ref}`
+  await exec('git', ['check-ref-format', baseRef])
+  const token = process.env.GITHUB_TOKEN
+  if (!token) throw new Error('GITHUB_TOKEN is not set')
+  const server = process.env.GITHUB_SERVER_URL ?? 'https://github.com'
+  const environment = {
+    ...process.env,
+    GIT_TERMINAL_PROMPT: '0',
+    GIT_CONFIG_COUNT: '1',
+    GIT_CONFIG_KEY_0: `http.${server}/.extraheader`,
+    GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString('base64')}`,
+  }
+  const { stdout: shallow } = await exec('git', ['rev-parse', '--is-shallow-repository'])
+  await exec('git', [
+    'fetch', '--no-tags', ...(shallow.trim() === 'true' ? ['--unshallow'] : []),
+    `${server}/${pull.repository}.git`, baseRef, pull.headSha,
+  ], { env: environment, maxBuffer: 16 * 1024 * 1024 })
+  const { stdout: baseSha } = await exec('git', ['rev-parse', 'FETCH_HEAD'])
+  const { stdout } = await exec('python3', [
+    fileURLToPath(new URL('blame-production.py', import.meta.url)), baseSha.trim(), pull.headSha,
+  ], { maxBuffer: 16 * 1024 * 1024 })
+  return resolveBlameAuthors(JSON.parse(stdout), pull.repository, api)
+}

+ 52 - 0
.github/review-ownership/blame-ownership.test.mjs

@@ -0,0 +1,52 @@
+import assert from 'node:assert/strict'
+import test from 'node:test'
+
+import { resolveBlameAuthors } from './blame-ownership.mjs'
+
+const sha = number => number.toString(16).padStart(40, '0')
+
+test('combines commit author accounts and retains unlinked authors in the denominator', async () => {
+  const result = await resolveBlameAuthors({
+    totalLines: 10, commitLines: { [sha(1)]: 2, [sha(2)]: 3, [sha(3)]: 5 },
+  }, 'owner/repo', async (path, { body }) => {
+    assert.equal(path, '/graphql')
+    assert.deepEqual(body.variables, { owner: 'owner', name: 'repo' })
+    assert.equal(body.query, `query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { ${[1, 2, 3].map((number, index) => `c${index}: object(oid: "${sha(number)}") { ... on Commit { author { user { login } } } }`).join('\n')} } }`)
+    return { data: { repository: {
+      c0: { author: { user: { login: 'Writer' } } },
+      c1: { author: { user: { login: 'writer' } } },
+      c2: { author: { user: null } },
+    } } }
+  })
+  assert.equal(result.totalLines, 10)
+  assert.deepEqual({ ...result.reviewerLines }, { writer: 5 })
+})
+
+test('batches author lookup and skips zero-line changes', async () => {
+  let calls = 0
+  await resolveBlameAuthors({
+    totalLines: 51, commitLines: Object.fromEntries(Array.from({ length: 51 }, (_, index) => [sha(index), 1])),
+  }, 'owner/repo', async (path, { body }) => {
+    const offset = calls * 50
+    const length = calls++ === 0 ? 50 : 1
+    for (let index = 0; index < length; index++) {
+      assert.ok(body.query.includes(`c${index}: object(oid: "${sha(offset + index)}") { ... on Commit { author { user { login } } } }`))
+    }
+    assert.equal((body.query.match(/object\(oid:/gu) ?? []).length, length)
+    return { data: { repository: Object.fromEntries(Array.from({ length }, (_, index) =>
+      [`c${index}`, { author: { user: null } }])) } }
+  })
+  assert.equal(calls, 2)
+  await resolveBlameAuthors({ totalLines: 0, commitLines: {} }, 'owner/repo', async () => {
+    throw new Error('empty changes must not query authors')
+  })
+})
+
+test('rejects incomplete or failed author lookups rather than lowering the denominator', async () => {
+  for (const response of [{ errors: [{ message: 'rate limited' }] }, { data: { repository: { c0: null } } }]) {
+    await assert.rejects(resolveBlameAuthors({ totalLines: 1, commitLines: { [sha(1)]: 1 } },
+      'owner/repo', async () => response), /author/u)
+  }
+  await assert.rejects(resolveBlameAuthors({ totalLines: 2, commitLines: { [sha(1)]: 1 } },
+    'owner/repo', async () => { throw new Error('invalid counts must not reach GitHub') }), /invalid production/u)
+})

+ 135 - 0
.github/review-ownership/blame-production.py

@@ -0,0 +1,135 @@
+"""Count changed old production lines by merge-base blame commit; never execute PR files."""
+
+import argparse
+from collections import Counter
+import json
+import os
+from pathlib import PurePosixPath
+import re
+import subprocess
+
+from pygments import lex
+from pygments.lexers import get_lexer_for_filename
+from pygments.token import Comment, Literal
+
+
+EXCLUDED = frozenset((
+    'vendor', 'node_modules', 'dist', 'lib', 'build', 'coverage', 'target',
+    'test', 'tests', '__tests__', 'fixture', 'fixtures', 'snapshot', 'snapshots',
+    'testing', 'test-support', 'support', 'examples', 'docs', 'gen', 'generated', 'generated-effect',
+))
+EXTENSIONS = frozenset(('.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.css', '.scss', '.py', '.c', '.h', '.cpp', '.hpp', '.rs', '.html'))
+SHIPPED_ROOTS = ('apps/desktop/renderer/', 'packages/experimental/code-runtime-python/py/')
+SHIPPED_FILES = frozenset(('python/sdk-runtime/runtime-bootstrap.mjs', 'packages/experimental/webworker-packer/bin.js'))
+GENERATED = re.compile(r'auto-generated|automatically generated|generated by|do not edit', re.I)
+SHA = re.compile(r'[0-9a-f]{40}')
+
+
+def git(repo, *args):
+    """Run a bounded read-only Git command without external diff drivers or replacements."""
+    return subprocess.run(
+        ['git', '-C', repo, '--no-replace-objects', *args], check=True,
+        stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120,
+        env={**os.environ, 'GIT_TERMINAL_PROMPT': '0'},
+    ).stdout
+
+
+def production_path(path):
+    """Recognize shipped source roots, excluding tests, generated files and tooling."""
+    name = PurePosixPath(path)
+    shipped = (name.parts[0] in ('packages', 'apps', 'python', 'native') and 'src' in name.parts) \
+        or path.startswith(SHIPPED_ROOTS) or path in SHIPPED_FILES
+    return (
+        shipped
+        and not EXCLUDED.intersection(name.parts)
+        and not re.search(r'\.(?:test|spec|e2e|gen|generated|d)\.', name.name)
+        and name.stem != 'testing'
+        and name.suffix in EXTENSIONS
+    )
+
+
+def code_lines(path, source):
+    """Return physical lines containing non-comment, nonblank tokens, including mixed lines."""
+    lexer = get_lexer_for_filename(path, stripnl=False, ensurenl=False)
+    lines = set()
+    line = 1
+    leading = True
+    for kind, value in lex(source, lexer):
+        if leading and (kind in Comment or kind in Literal.String.Doc):
+            if GENERATED.search(value):
+                return set()
+        elif value.strip():
+            leading = False
+        for index, fragment in enumerate(value.split('\n')):
+            if index:
+                line += 1
+            if (kind not in Comment or kind in Comment.Preproc or kind in Comment.PreprocFile) \
+                    and kind not in Literal.String.Doc and fragment.strip():
+                lines.add(line)
+    return lines
+
+
+def changed_files(repo, base, head):
+    """Yield old paths and blob IDs, preserving rename detection and arbitrary filenames."""
+    fields = git(repo, 'diff', '--raw', '-z', '--no-abbrev', '--find-renames',
+                 '--no-ext-diff', '--no-textconv', base, head, '--').split(b'\0')
+    index = 0
+    while index < len(fields) - 1:
+        metadata = fields[index].decode('ascii').split()
+        path = os.fsdecode(fields[index + 1])
+        index += 2
+        if metadata[4].startswith(('R', 'C')):
+            index += 1
+        if metadata[0] not in (':100644', ':100755') or not production_path(path):
+            continue
+        if metadata[2] != metadata[3]:
+            yield path, metadata[2], metadata[3]
+
+
+def measure(repo, base, head):
+    """Compute the merge base and attribute changed old code lines to their last commits."""
+    for revision in (base, head):
+        if not SHA.fullmatch(revision):
+            raise ValueError('base and head must be full commit SHAs')
+    if git(repo, 'rev-parse', '--is-shallow-repository').strip() != b'false':
+        raise ValueError('blame requires complete history')
+    merge_base = git(repo, 'merge-base', base, head).decode().strip()
+    counts = Counter()
+    files = 0
+    for path, old_blob, new_blob in changed_files(repo, merge_base, head):
+        source = git(repo, 'cat-file', 'blob', old_blob).decode('utf-8', errors='replace')
+        eligible = code_lines(path, source)
+        if not eligible:
+            continue
+        if new_blob == '0' * 40:
+            changed = eligible
+        else:
+            patch = git(repo, 'diff', '--no-ext-diff', '--no-textconv', '--text', '--unified=0',
+                        old_blob, new_blob, '--').decode('utf-8', errors='replace')
+            changed = set()
+            for start, length in re.findall(r'^@@ -(\d+)(?:,(\d+))? \+\d+(?:,\d+)? @@', patch, re.M):
+                start = int(start)
+                changed.update(eligible.intersection(range(start, start + int(length or '1'))))
+        if not changed:
+            continue
+        files += 1
+        # One blame per file covers all changed ranges; unchanged intervening lines never count.
+        blame = git(repo, 'blame', '--no-textconv', '--line-porcelain', '-L', f'{min(changed)},{max(changed)}',
+                    merge_base, '--', path).decode('utf-8', errors='replace')
+        attributed = 0
+        for commit, line in re.findall(r'^([0-9a-f]{40}) \d+ (\d+)(?: \d+)?$', blame, re.M):
+            if int(line) in changed:
+                counts[commit] += 1
+                attributed += 1
+        if attributed != len(changed):
+            raise ValueError(f'incomplete blame for {path!r}')
+    return {'mergeBase': merge_base, 'files': files, 'totalLines': sum(counts.values()), 'commitLines': dict(counts)}
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument('base')
+    parser.add_argument('head')
+    parser.add_argument('--repo', default='.')
+    args = parser.parse_args()
+    print(json.dumps(measure(args.repo, args.base, args.head)))

+ 41 - 10
.github/review-ownership/check-approval.mjs

@@ -4,13 +4,14 @@ import { readFileSync } from 'node:fs'
 import process from 'node:process'
 import { pathToFileURL } from 'node:url'
 
+import { productionOwnership, LOGIN } from './blame-ownership.mjs'
+
 const API_VERSION = '2026-03-10'
 const MAX_PULL_REQUEST_REVIEWS = 3_000
 const PAGE_SIZE = 100
 const STATUS_CONTEXT = 'weighted approval'
 const WRITABLE_PERMISSIONS = new Set(['admin', 'write'])
 const REVIEW_STATES = new Set(['APPROVED', 'CHANGES_REQUESTED', 'COMMENTED', 'DISMISSED', 'PENDING'])
-const LOGIN = /^[A-Za-z0-9-]+(?:\[bot\])?$/u
 
 class GitHubApiError extends Error {
   constructor(message, status) {
@@ -128,10 +129,10 @@ export async function listPullRequestReviews(api, repository, pullNumber) {
 
 /**
  * Evaluate approval points from current reviews and repository permissions.
- * @param {{event: unknown, policySource: string, api: (path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>}} options Runtime inputs.
- * @returns {Promise<{pull: {repository: string, number: number, headSha: string}, state: 'pending' | 'success', description: string, points: number, requiredPoints: number, approvals: Array<{login: string, points: number}>, blockers: string[], ignoredReviewers: string[]}>} Approval decision and status payload fields.
+ * @param {{event: unknown, policySource: string, api: (path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>, getOwnership?: typeof productionOwnership}} options Runtime inputs.
+ * @returns {Promise<{pull: {repository: string, number: number, headSha: string}, state: 'pending' | 'success', description: string, points: number, requiredPoints: number, approvals: Array<{login: string, points: number, ownership?: {ownedLines: number, totalLines: number}}>, blockers: string[], ignoredReviewers: string[]}>} Approval decision and status payload fields.
  */
-export async function evaluateApproval({ event, policySource, api }) {
+export async function evaluateApproval({ event, policySource, api, getOwnership = productionOwnership }) {
   const pull = pullRequestFromEvent(event)
   const policy = parseApprovalPolicy(policySource)
   if (pull.draft) {
@@ -160,12 +161,24 @@ export async function evaluateApproval({ event, policySource, api }) {
       })
     }
   }
+  const unboostedPoints = approvals.reduce((sum, approval) => sum + approval.points, 0)
+  if (blockers.length === 0 && unboostedPoints < policy.requiredPoints
+    && approvals.some(approval => approval.points === policy.defaultPoints)) {
+    const ownership = await getOwnership(pull, api)
+    for (const approval of approvals) {
+      if (approval.points !== policy.defaultPoints) continue
+      const ownedLines = ownership.reviewerLines[approval.login.toLowerCase()] ?? 0
+      approval.ownership = { ownedLines, totalLines: ownership.totalLines }
+      if (ownership.totalLines > 0) approval.points = Math.min(policy.requiredPoints, policy.defaultPoints
+        + (policy.requiredPoints - policy.defaultPoints) * 4 * ownedLines / ownership.totalLines)
+    }
+  }
   approvals.sort((left, right) => left.login.localeCompare(right.login, 'en'))
   blockers.sort((left, right) => left.localeCompare(right, 'en'))
   ignoredReviewers.sort((left, right) => left.localeCompare(right, 'en'))
   const points = approvals.reduce((total, approval) => {
     const next = total + approval.points
-    if (!Number.isSafeInteger(next)) throw new Error('approval points exceed the safe integer range')
+    if (!Number.isFinite(next) || next > Number.MAX_SAFE_INTEGER) throw new Error('approval points must be finite and at most Number.MAX_SAFE_INTEGER')
     return next
   }, 0)
   if (blockers.length > 0) {
@@ -180,26 +193,28 @@ export async function evaluateApproval({ event, policySource, api }) {
     blockers,
     ignoredReviewers,
     state,
-    `${points}/${policy.requiredPoints} approval points`,
+    `${Number(points.toFixed(2))}/${policy.requiredPoints} approval points`,
   )
 }
 
 /**
  * Evaluate and publish the required commit status, publishing an error status when evaluation fails.
- * @param {{event: unknown, policySource: string, api: (path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>, runUrl: string, write?: (line: string) => void}} options Runtime inputs.
+ * @param {{event: unknown, policySource: string, api: (path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>, runUrl: string, write?: (line: string) => void, getOwnership?: typeof productionOwnership}} options Runtime inputs.
  * @returns {Promise<Awaited<ReturnType<typeof evaluateApproval>>>} Published approval decision.
  */
-export async function runApprovalCheck({ event, policySource, api, runUrl, write = line => process.stdout.write(`${line}\n`) }) {
+export async function runApprovalCheck({ event, policySource, api, runUrl, getOwnership = productionOwnership, write = line => process.stdout.write(`${line}\n`) }) {
   const pull = pullRequestFromEvent(event)
+  await publishStatus(api, pull, 'pending', 'Evaluating approval points.', runUrl)
   let result
   try {
-    result = await evaluateApproval({ event, policySource, api })
+    result = await evaluateApproval({ event, policySource, api, getOwnership })
   } catch (error) {
     await publishStatus(api, pull, 'error', 'Approval evaluation failed.', runUrl)
     throw error
   }
   write(`Approval score: ${result.points}/${result.requiredPoints}.`)
-  writeList(write, 'Counted approvals', result.approvals.map(({ login, points }) => `@${login}: ${points}`))
+  writeList(write, 'Counted approvals', result.approvals.map(({ login, points, ownership }) =>
+    `@${login}: ${points}${ownership ? ` (${ownership.ownedLines}/${ownership.totalLines} old production lines)` : ''}`))
   writeList(write, 'Blocking change requests', result.blockers.map(login => `@${login}`))
   writeList(write, 'Ignored reviewers without write access', result.ignoredReviewers.map(login => `@${login}`))
   await publishStatus(api, pull, result.state, result.description, runUrl)
@@ -207,6 +222,17 @@ export async function runApprovalCheck({ event, policySource, api, runUrl, write
   return result
 }
 
+/**
+ * Revoke a previous success before dependency installation, or report its failure.
+ * @param {{event: unknown, api: (path: string, options: object) => Promise<unknown>, runUrl: string, phase: string}} options Publication inputs.
+ * @returns {Promise<void>} Completion of the status write.
+ */
+export async function publishApprovalPhase({ event, api, runUrl, phase }) {
+  if (!['pending', 'error'].includes(phase)) throw new Error('invalid approval setup phase')
+  await publishStatus(api, pullRequestFromEvent(event), phase,
+    phase === 'pending' ? 'Preparing approval evaluation.' : 'Approval setup or evaluation failed.', runUrl)
+}
+
 /**
  * Resolve the reviewed pull request from a completed run of the review-event workflow file.
  * @param {{event: unknown, api: (path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>}} options Trusted workflow inputs.
@@ -360,6 +386,11 @@ async function main() {
     }
     event = resolved
   }
+  const phase = process.argv[2]
+  if (phase) {
+    await publishApprovalPhase({ event, api, runUrl: process.env.GITHUB_RUN_URL ?? '', phase })
+    return
+  }
   await runApprovalCheck({
     event,
     policySource,

+ 132 - 2
.github/review-ownership/check-approval.test.mjs

@@ -10,6 +10,7 @@ import {
   listPullRequestReviews,
   parseApprovalPolicy,
   runApprovalCheck,
+  publishApprovalPhase,
 } from './check-approval.mjs'
 
 const policySource = readFileSync(new URL('approval-policy.json', import.meta.url), 'utf8')
@@ -149,6 +150,7 @@ test('accepts one two-point approval from a write-capable reviewer', async () =>
   const result = await evaluateApproval({
     event: pullRequestEvent(),
     policySource,
+    getOwnership: async () => ({ totalLines: 0, reviewerLines: {} }),
     api: async (path) => {
       calls.push(path)
       if (path.includes('/reviews?')) return [review('07akioni', 'APPROVED')]
@@ -166,6 +168,7 @@ test('accepts two one-point approvals and ignores reviews without write access',
   const result = await evaluateApproval({
     event: pullRequestEvent(),
     policySource,
+    getOwnership: async () => ({ totalLines: 0, reviewerLines: {} }),
     api: async (path) => {
       if (path.includes('/reviews?')) {
         return [
@@ -193,6 +196,7 @@ test('keeps one one-point approval pending without failing the status', async ()
   const result = await evaluateApproval({
     event: pullRequestEvent(),
     policySource,
+    getOwnership: async () => ({ totalLines: 0, reviewerLines: {} }),
     api: async (path) => {
       if (path.includes('/reviews?')) return [review('writer', 'APPROVED')]
       if (path.includes('/collaborators/writer/permission')) return { permission: 'write' }
@@ -227,6 +231,7 @@ test('keeps the status pending on a write-capable change request while ignoring
   const result = await runApprovalCheck({
     event: pullRequestEvent({ author: 'author' }),
     policySource,
+    getOwnership: async () => ({ totalLines: 0, reviewerLines: {} }),
     runUrl: 'https://github.example/actions/runs/1',
     api: async (path, options = {}) => {
       if (path.includes('/reviews?')) {
@@ -254,6 +259,11 @@ test('keeps the status pending on a write-capable change request while ignoring
   assert.deepEqual(result.blockers, ['blocker'])
   assert.deepEqual(result.ignoredReviewers, ['reader'])
   assert.deepEqual(statuses, [{
+    state: 'pending',
+    context: 'weighted approval',
+    description: 'Evaluating approval points.',
+    target_url: 'https://github.example/actions/runs/1',
+  }, {
     state: 'pending',
     context: 'weighted approval',
     description: '1 blocking change request.',
@@ -265,6 +275,7 @@ test('keeps drafts pending without reading reviews', async () => {
   const result = await evaluateApproval({
     event: pullRequestEvent({ draft: true }),
     policySource,
+    getOwnership: async () => ({ totalLines: 0, reviewerLines: {} }),
     api: async () => { throw new Error('draft evaluation must not call GitHub') },
   })
   assert.equal(result.state, 'pending')
@@ -278,6 +289,7 @@ test('publishes the required status and replaces stale success with error on eva
   const result = await runApprovalCheck({
     event: pullRequestEvent(),
     policySource,
+    getOwnership: async () => ({ totalLines: 0, reviewerLines: {} }),
     runUrl: 'https://github.example/actions/runs/1',
     api: async (path, options = {}) => {
       calls.push({ path, options })
@@ -307,6 +319,7 @@ test('publishes the required status and replaces stale success with error on eva
   await assert.rejects(runApprovalCheck({
     event: pullRequestEvent(),
     policySource,
+    getOwnership: async () => ({ totalLines: 0, reviewerLines: {} }),
     runUrl: 'https://github.example/actions/runs/2',
     api: async (path, options = {}) => {
       if (path.includes('/reviews?')) throw new Error('reviews unavailable')
@@ -318,8 +331,9 @@ test('publishes the required status and replaces stale success with error on eva
     },
     write: () => {},
   }), /reviews unavailable/u)
-  assert.equal(failures[0].options.body.state, 'error')
-  assert.equal(failures[0].options.body.description, 'Approval evaluation failed.')
+  assert.equal(failures[0].options.body.state, 'pending')
+  assert.equal(failures[1].options.body.state, 'error')
+  assert.equal(failures[1].options.body.description, 'Approval evaluation failed.')
 })
 
 test('sends authenticated JSON and escapes an API error body', async () => {
@@ -346,3 +360,119 @@ test('sends authenticated JSON and escapes an API error body', async () => {
   })
   await assert.rejects(failing('/failure'), /"::error::untrusted\\nbody"/u)
 })
+
+for (const [ownedLines, totalLines, expectedPoints] of [[9, 100, 1.3599999999999999], [0, 100, 1], [1, 8, 1.5], [24, 100, 1.96], [1, 4, 2], [25, 100, 2], [26, 100, 2], [100, 100, 2], [0, 0, 1]]) {
+  test(`scores ${ownedLines}/${totalLines} old production lines as ${expectedPoints} points`, async () => {
+    let measurements = 0
+    const result = await evaluateApproval({
+      event: pullRequestEvent(), policySource,
+      getOwnership: async () => {
+        measurements++
+        return { totalLines, reviewerLines: { writer: ownedLines } }
+      },
+      api: async path => path.includes('/reviews?')
+        ? [review('Writer', 'APPROVED')]
+        : { permission: 'write' },
+    })
+    assert.equal(result.points, expectedPoints)
+    assert.equal(result.description, `${Number(expectedPoints.toFixed(2))}/2 approval points.`)
+    assert.equal(result.state, expectedPoints === 2 ? 'success' : 'pending')
+    assert.equal(measurements, 1)
+    assert.deepEqual(result.approvals[0].ownership, { ownedLines, totalLines })
+  })
+}
+
+test('does not fetch ownership when a change request blocks approval', async () => {
+  let measurements = 0
+  const result = await evaluateApproval({
+    event: pullRequestEvent(), policySource,
+    getOwnership: async () => {
+      measurements++
+      return { totalLines: 2, reviewerLines: { first: 1, second: 1 } }
+    },
+    api: async path => path.includes('/reviews?')
+      ? [review('first', 'APPROVED'), review('second', 'APPROVED'), review('blocker', 'CHANGES_REQUESTED')]
+      : { permission: 'write' },
+  })
+  assert.equal(measurements, 0)
+  assert.equal(result.points, 2)
+  assert.equal(result.state, 'pending')
+})
+
+test('does not fetch history when approvals already have two-point weights', async () => {
+  const result = await evaluateApproval({
+    event: pullRequestEvent(), policySource,
+    getOwnership: async () => { throw new Error('unexpected history fetch') },
+    api: async path => path.includes('/reviews?') ? [review('turtle1999', 'APPROVED')] : { permission: 'write' },
+  })
+  assert.equal(result.points, 2)
+})
+
+test('publishes error when production attribution fails', async () => {
+  const states = []
+  await assert.rejects(runApprovalCheck({
+    event: pullRequestEvent(), policySource, runUrl: 'https://github.example/run/1',
+    getOwnership: async () => { throw new Error('incomplete history') },
+    api: async (path, options) => {
+      if (path.includes('/reviews?')) return [review('writer', 'APPROVED')]
+      if (path.includes('/permission')) return { permission: 'write' }
+      states.push(options.body.state)
+      return {}
+    },
+  }), /incomplete history/u)
+  assert.deepEqual(states, ['pending', 'error'])
+})
+
+
+test('revokes a previous success before starting expensive attribution', async () => {
+  const states = []
+  await runApprovalCheck({
+    event: pullRequestEvent(), policySource, runUrl: 'https://github.example/run/1', write: () => {},
+    getOwnership: async () => {
+      assert.deepEqual(states, ['pending'])
+      return { totalLines: 100, reviewerLines: { writer: 25 } }
+    },
+    api: async (path, options) => {
+      if (path.includes('/reviews?')) return [review('writer', 'APPROVED')]
+      if (path.includes('/permission')) return { permission: 'write' }
+      states.push(options.body.state)
+      return {}
+    },
+  })
+  assert.deepEqual(states, ['pending', 'success'])
+})
+
+for (const reviewers of [['first', 'second'], ['turtle1999', 'first']]) {
+  test(`does not fetch ownership for sufficient approvals: ${reviewers}`, async () => {
+    const result = await evaluateApproval({
+      event: pullRequestEvent(), policySource,
+      getOwnership: async () => { throw new Error('unnecessary lookup') },
+      api: async path => path.includes('/reviews?')
+        ? reviewers.map(login => review(login, 'APPROVED')) : { permission: 'write' },
+    })
+    assert.equal(result.state, 'success')
+  })
+}
+
+test('uses policy endpoints and formats only the displayed score', async () => {
+  const result = await evaluateApproval({
+    event: pullRequestEvent(),
+    policySource: JSON.stringify({ requiredPoints: 5, defaultPoints: 2, reviewerPoints: {} }),
+    getOwnership: async () => ({ totalLines: 100, reviewerLines: { writer: 9 } }),
+    api: async path => path.includes('/reviews?') ? [review('writer', 'APPROVED')] : { permission: 'write' },
+  })
+  assert.equal(result.points, 3.08)
+  assert.equal(result.description, '3.08/5 approval points.')
+})
+
+test('publishes setup phases without evaluating or installing dependencies', async () => {
+  const states = []
+  const options = {
+    event: pullRequestEvent(), runUrl: 'https://github.example/run/1',
+    api: async (path, { body }) => { assert.match(path, /\/statuses\//u); states.push(body.state) },
+  }
+  await publishApprovalPhase({ ...options, phase: 'pending' })
+  await publishApprovalPhase({ ...options, phase: 'error' })
+  assert.deepEqual(states, ['pending', 'error'])
+  await assert.rejects(publishApprovalPhase({ ...options, phase: 'success' }), /invalid approval setup phase/u)
+})

+ 1 - 0
.github/review-ownership/requirements.txt

@@ -0,0 +1 @@
+Pygments==2.19.2

+ 221 - 0
.github/review-ownership/test_blame_production.py

@@ -0,0 +1,221 @@
+"""Exercise production-line attribution against isolated real Git histories."""
+
+import base64
+import importlib.util
+import json
+import os
+import shutil
+import sys
+from pathlib import Path
+import subprocess
+import tempfile
+import unittest
+
+spec = importlib.util.spec_from_file_location('blame_production', Path(__file__).with_name('blame-production.py'))
+blame = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(blame)
+
+
+class ProductionBlameTest(unittest.TestCase):
+    def setUp(self):
+        self.directory = tempfile.TemporaryDirectory(prefix='approval-blame-')
+        self.addCleanup(self.directory.cleanup)
+        self.root = Path(self.directory.name)
+        self.git('init', '-q')
+        self.git('config', 'user.name', 'Approval Fixture')
+        self.git('config', 'user.email', 'fixture@example.invalid')
+        self.git('config', 'commit.gpgsign', 'false')
+        self.git('config', 'core.hooksPath', str(self.root / 'no-hooks'))
+
+    def git(self, *args):
+        return subprocess.run(['git', '-C', str(self.root), *args], check=True,
+                              capture_output=True, text=True).stdout.strip()
+
+    def write(self, path, source):
+        file = self.root / path
+        file.parent.mkdir(parents=True, exist_ok=True)
+        file.write_text(source, encoding='utf-8')
+
+    def commit(self):
+        self.git('add', '.')
+        self.git('commit', '-qm', 'fixture')
+        return self.git('rev-parse', 'HEAD')
+
+    def test_code_tokens_keep_strings_and_mixed_lines(self):
+        cases = [
+            ('main.ts', '// comment\n/* block\n comment */\nconst url = "https://example.test" // tail\n\n', {4}),
+            ('main.html', '<!-- comment -->\n<div>content</div>\n', {2}),
+            ('main.tsx', 'const view = <div>{/* comment */}hello</div>\n', {1}),
+            ('main.css', '/* block\n comment */\n.x { color: red; /* mixed */ }\n', {3}),
+            ('main.py', '"""module docs\nmore docs"""\n# comment\nx = "# code"\n', {4}),
+            ('main.c', '#include <stdio.h>\n#define VALUE 1\n', {1, 2}),
+            ('main.c', '/* comment */\nint main() { return 0; } // tail\n', {2}),
+            ('main.ts', 'const text = `first\n// string content\nlast`\n', {1, 2, 3}),
+        ]
+        for path, source, expected in cases:
+            with self.subTest(path=path, source=source):
+                self.assertEqual(blame.code_lines(path, source), expected)
+
+    def test_excludes_nonproduction_paths_and_generated_headers(self):
+        for path in ('docs/a.ts', 'vendor/src/a.ts', 'packages/a/tests/a.ts',
+                     'packages/a/src/a.spec.ts', 'packages/support/a/src/a.ts',
+                     'packages/a/src/generated/a.ts', 'packages/a/src/a.d.ts',
+                     'packages/a/src/a.md', 'scripts/a.ts',
+                     'packages/core/tools/src/testing.ts',
+                     'packages/session/session-persistence-jsonl/src/testing/generation.ts'):
+            self.assertFalse(blame.production_path(path), path)
+        self.assertTrue(blame.production_path('packages/a/src/a.ts'))
+        for path in ('python/sdk-runtime/runtime-bootstrap.mjs', 'apps/desktop/renderer/startup.js',
+                     'apps/desktop/renderer/startup.html', 'packages/experimental/code-runtime-python/py/protocol.py',
+                     'packages/experimental/webworker-packer/bin.js'):
+            self.assertTrue(blame.production_path(path), path)
+        self.assertEqual(blame.code_lines('a.ts', '// Generated by schema\nconst a = 1\n'), set())
+
+    def test_attributes_only_changed_old_code_at_merge_base(self):
+        path = 'packages/a/src/a.ts'
+        self.write(path, '// first\nconst a = 1\nconst b = 2\n')
+        first = self.commit()
+        self.write(path, '// first\nconst a = 1\nconst b = 3\n')
+        base = self.commit()
+        self.write(path, '// different\nconst a = 4\nconst b = 5\nconst added = 6\n')
+        self.write('packages/a/src/new.ts', 'const entirelyNew = 1\n')
+        self.write('packages/a/tests/test.ts', 'const test = 1\n')
+        head = self.commit()
+        # Base advancement must not replace the common ancestor used for attribution.
+        self.git('checkout', '--detach', base)
+        self.write('packages/a/src/unrelated.ts', 'const unrelated = 1\n')
+        advanced_base = self.commit()
+        result = blame.measure(str(self.root), advanced_base, head)
+        self.assertEqual(result['mergeBase'], base)
+        self.assertEqual(result['totalLines'], 2)
+        self.assertEqual(result['commitLines'], {first: 1, base: 1})
+
+    def test_renames_deletions_and_unusual_paths(self):
+        path = 'packages/a/src/quoted " name.ts'
+        self.write(path, '// header\nconst a = 1\nconst b = 2\nconst c = 3\n')
+        self.write('packages/a/src/deleted.ts', 'const gone = 1\n// comment\n')
+        base = self.commit()
+        renamed = 'packages/a/src/renamed.ts'
+        self.git('mv', path, renamed)
+        self.write(renamed, '// header\nconst a = 1\nconst b = 2\nconst c = 4\n')
+        self.git('rm', 'packages/a/src/deleted.ts')
+        result = blame.measure(str(self.root), base, self.commit())
+        self.assertEqual(result['totalLines'], 2)
+        self.assertEqual(result['commitLines'], {base: 2})
+
+    def test_pure_additions_and_comment_only_changes_have_zero_denominator(self):
+        self.write('packages/a/src/a.ts', '// comment\nconst a = 1\n')
+        base = self.commit()
+        self.write('packages/a/src/a.ts', '// revised\nconst a = 1\nconst newLine = 2\n')
+        result = blame.measure(str(self.root), base, self.commit())
+        self.assertEqual(result['totalLines'], 0)
+        self.assertEqual(result['commitLines'], {})
+
+    def test_binary_marked_replacement_does_not_hide_old_production_lines(self):
+        path = 'packages/a/src/a.ts'
+        self.write(path, 'const old = 1\n')
+        base = self.commit()
+        self.write(path, '\0binary replacement\n')
+        self.assertEqual(blame.measure(str(self.root), base, self.commit())['totalLines'], 1)
+
+    def test_pure_rename_has_zero_denominator(self):
+        self.write('packages/a/src/a.ts', 'const a = 1\n')
+        base = self.commit()
+        self.git('mv', 'packages/a/src/a.ts', 'packages/a/src/b.ts')
+        self.assertEqual(blame.measure(str(self.root), base, self.commit())['totalLines'], 0)
+
+    def test_publisher_fetches_history_without_checking_out_pr_code(self):
+        self.write('packages/a/src/a.ts', 'const old = 1\n')
+        self.write('packages/a/src/base.ts', 'const base = 1\n')
+        base = self.commit()
+        self.write('packages/a/src/a.ts', 'throw new Error("PR code must not run")\n')
+        branch = self.commit()
+        self.git('checkout', '--detach', base)
+        self.write('packages/a/src/base.ts', 'const base = 2\n')
+        advanced = self.commit()
+        self.git('checkout', '--detach', branch)
+        self.git('merge', '--no-edit', advanced)
+        head = self.git('rev-parse', 'HEAD')
+        remote = self.root / 'remote' / 'owner' / 'repo.git'
+        remote.parent.mkdir(parents=True)
+        self.git('clone', '--bare', str(self.root), str(remote))
+        subprocess.run(['git', '-C', str(remote), 'update-ref', 'refs/heads/trusted', advanced], check=True)
+        subprocess.run(['git', '-C', str(remote), 'symbolic-ref', 'HEAD', 'refs/heads/trusted'], check=True)
+        subprocess.run(['git', '-C', str(remote), 'update-ref', 'refs/pull/42/head', advanced], check=True)
+        checkout = self.root / 'checkout'
+        self.git('clone', '--depth=1', remote.as_uri(), str(checkout))
+        wrappers = self.root / 'wrappers'
+        wrappers.mkdir()
+        trace = self.root / 'git-environment.json'
+        git_wrapper = wrappers / 'git'
+        git_wrapper.write_text(f"#!{sys.executable}\nimport json, os, sys\n"
+                               f"if 'fetch' in sys.argv: open({str(trace)!r}, 'w').write(json.dumps({{key: value for key, value in os.environ.items() if key.startswith('GIT_CONFIG_')}}))\n"
+                               f"os.execv({shutil.which('git')!r}, ['git', *sys.argv[1:]])\n")
+        git_wrapper.chmod(0o755)
+        module = Path(__file__).with_name('blame-ownership.mjs').resolve().as_uri()
+        program = f"""
+          import {{ productionOwnership }} from {json.dumps(module)};
+          const result = await productionOwnership({{repository:'owner/repo', number:42, headSha:{json.dumps(head)}}},
+            async path => path === '/graphql'
+              ? {{data:{{repository:{{c0:{{author:{{user:{{login:'writer'}}}}}}}}}}}}
+              : {{base:{{sha:{json.dumps(base)},ref:'trusted'}},head:{{sha:{json.dumps(head)}}}}});
+          console.log(JSON.stringify(result));
+        """
+        result = subprocess.run(['node', '--input-type=module', '-e', program], cwd=checkout,
+                                check=True, capture_output=True, text=True,
+                                env={**os.environ, 'GITHUB_TOKEN': 'fixture-token',
+                                     'GITHUB_SERVER_URL': (self.root / 'remote').as_uri(),
+                                     'PATH': str(wrappers) + os.pathsep + str(Path(sys.executable).parent) + os.pathsep + os.environ['PATH']})
+        self.assertEqual(json.loads(result.stdout), {'totalLines': 1, 'reviewerLines': {'writer': 1}})
+        self.assertEqual(subprocess.check_output(['git', '-C', str(checkout), 'rev-parse', 'HEAD'], text=True).strip(), advanced)
+        fetch_environment = json.loads(trace.read_text())
+        self.assertEqual(fetch_environment['GIT_CONFIG_COUNT'], '1')
+        self.assertEqual(fetch_environment['GIT_CONFIG_KEY_0'],
+                         f"http.{(self.root / 'remote').as_uri()}/.extraheader")
+        self.assertEqual(fetch_environment['GIT_CONFIG_VALUE_0'],
+                         'AUTHORIZATION: basic ' + base64.b64encode(b'x-access-token:fixture-token').decode())
+        config = (checkout / '.git' / 'config').read_text()
+        self.assertNotIn('fixture-token', config)
+        self.assertNotIn('AUTHORIZATION', config)
+
+    def test_merge_forward_excludes_base_only_edits(self):
+        path = 'packages/a/src/a.ts'
+        self.write(path, 'const a = 1\n')
+        self.write('packages/a/src/base.ts', 'const base = 1\n')
+        fork = self.commit()
+        self.write(path, 'const a = 2\n')
+        branch = self.commit()
+        self.git('checkout', '--detach', fork)
+        self.write('packages/a/src/base.ts', 'const base = 2\n')
+        advanced = self.commit()
+        self.git('checkout', '--detach', branch)
+        self.git('merge', '--no-edit', advanced)
+        result = blame.measure(str(self.root), advanced, self.git('rev-parse', 'HEAD'))
+        self.assertEqual(result['mergeBase'], advanced)
+        self.assertEqual(result['commitLines'], {fork: 1})
+
+    def test_non_utf8_blobs_and_commit_metadata_keep_old_lines(self):
+        path = 'packages/a/src/a.ts'
+        self.write(path, 'const a = "old"\n')
+        (self.root / path).write_bytes(b'const a = "\xff"\n')
+        self.git('add', '.')
+        subprocess.run(['git', '-C', str(self.root), '-c', 'i18n.commitEncoding=ISO-8859-1',
+                        'commit', '-q', '-F', '-'], input=b'metadata \xff', check=True, capture_output=True)
+        base = self.git('rev-parse', 'HEAD')
+        (self.root / path).write_bytes(b'\0new \xfe\n')
+        self.assertEqual(blame.measure(str(self.root), base, self.commit())['commitLines'], {base: 1})
+
+    def test_generated_words_in_code_do_not_exclude_handwritten_files(self):
+        self.assertEqual(blame.code_lines('a.ts', 'const text = "generated by an agent"\n'), {1})
+        self.assertEqual(blame.code_lines('a.ts', 'const a = 1\n// do not edit user data\n'), {1})
+
+    def test_rejects_shallow_history(self):
+        self.write('packages/a/src/a.ts', 'const a = 1\n')
+        base = self.commit()
+        (self.root / '.git' / 'shallow').write_text(base + '\n')
+        with self.assertRaisesRegex(ValueError, 'complete history'):
+            blame.measure(str(self.root), base, base)
+
+
+if __name__ == '__main__':
+    unittest.main()

+ 7 - 0
.github/workflows/ci.yml

@@ -468,6 +468,13 @@ jobs:
       - name: Install uv
         run: python -m pip install uv==0.11.23
 
+      - uses: actions/setup-node@v6
+        with:
+          node-version: 24
+
+      - name: Test production blame scoring
+        run: uv run --python 3.10 --with-requirements .github/review-ownership/requirements.txt python -m unittest discover -s .github/review-ownership -p 'test_*.py'
+
       - name: Run complete keyless Python suite
         run: uv run --python 3.10 --group test --project python/sdk pytest
 

+ 22 - 2
.github/workflows/weighted-approval.yml

@@ -2,7 +2,7 @@ name: weighted-approval
 
 on:
   pull_request_target:
-    types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
+    types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, edited]
   workflow_run:
     workflows: [weighted-approval-review-event]
     types: [completed]
@@ -13,7 +13,7 @@ permissions:
   statuses: write
 
 concurrency:
-  group: weighted-approval-${{ github.event.pull_request.number || github.event.workflow_run.head_sha }}
+  group: weighted-approval-${{ github.event.pull_request.number && format('weighted-approval-review-event:{0}', github.event.pull_request.number) || github.event.workflow_run.display_title }}
   cancel-in-progress: false
 
 jobs:
@@ -30,8 +30,28 @@ jobs:
         with:
           ref: ${{ github.event.repository.default_branch }}
           persist-credentials: false
+      - name: Revoke previous approval status
+        id: revoke
+        env:
+          GITHUB_TOKEN: ${{ github.token }}
+          GITHUB_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+        run: node .github/review-ownership/check-approval.mjs pending
+      # SECURITY: dependency setup runs in the status-writing job.
+      - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
+        with:
+          python-version: '3.10'
+          cache: pip
+          cache-dependency-path: .github/review-ownership/requirements.txt
+      - name: Install production lexer
+        run: python3 -m pip install -r .github/review-ownership/requirements.txt
       - name: Publish weighted approval status
         env:
           GITHUB_TOKEN: ${{ github.token }}
           GITHUB_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
         run: node .github/review-ownership/check-approval.mjs
+      - name: Publish approval setup failure
+        if: failure() && steps.revoke.outcome == 'success'
+        env:
+          GITHUB_TOKEN: ${{ github.token }}
+          GITHUB_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+        run: node .github/review-ownership/check-approval.mjs error

+ 1 - 1
package.json

@@ -58,7 +58,7 @@
     "test:bench:built": "vitest run --config vitest.bench.config.ts",
     "test:expected": "vitest run --config vitest.expected.config.ts",
     "test:expected:refresh": "DSH_SNAPSHOT=refresh vitest run --config vitest.expected.config.ts",
-    "test:approval-policy": "node --test .github/review-ownership/check-approval.test.mjs",
+    "test:approval-policy": "node --test .github/review-ownership/check-approval.test.mjs .github/review-ownership/blame-ownership.test.mjs",
     "test:issue-management": "node .github/issue-management/policy.test.mjs",
     "test:snapshot": "vitest run --config vitest.snapshot.config.ts",
     "test:snapshot:record": "DSH_SNAPSHOT=record vitest run --config vitest.snapshot.config.ts --update",

+ 20 - 2
scripts/ci-workflow.spec.ts

@@ -917,7 +917,7 @@ describe('Weighted approval workflow', () => {
 
     expect(publisher.name).toBe('weighted-approval')
     expect(Object.keys(publisher.on)).toEqual(['pull_request_target', 'workflow_run'])
-    expect(pullRequest.types).toEqual(['opened', 'synchronize', 'reopened', 'ready_for_review', 'converted_to_draft'])
+    expect(pullRequest.types).toEqual(['opened', 'synchronize', 'reopened', 'ready_for_review', 'converted_to_draft', 'edited'])
     expect(workflowRun).toEqual({ workflows: ['weighted-approval-review-event'], types: ['completed'] })
     expect(reviewEvent.name).toBe('weighted-approval-review-event')
     expect(reviewEvent['run-name']).toBe('weighted-approval-review-event:${{ github.event.pull_request.number }}')
@@ -930,7 +930,7 @@ describe('Weighted approval workflow', () => {
       statuses: 'write',
     })
     expect(publisher.concurrency).toEqual({
-      group: 'weighted-approval-${{ github.event.pull_request.number || github.event.workflow_run.head_sha }}',
+      group: "weighted-approval-${{ github.event.pull_request.number && format('weighted-approval-review-event:{0}', github.event.pull_request.number) || github.event.workflow_run.display_title }}",
       'cancel-in-progress': false,
     })
     expect(job).toMatchObject({
@@ -946,6 +946,24 @@ describe('Weighted approval workflow', () => {
         'persist-credentials': false,
       },
     })
+    const setupIndex = steps.findIndex(step => typeof step.uses === 'string' && step.uses.startsWith('actions/setup-python@'))
+    expect(steps[setupIndex]?.uses).toBe('actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1')
+    const revokeIndex = steps.findIndex(step => step.id === 'revoke')
+    expect(revokeIndex).toBeGreaterThan(steps.indexOf(checkout!))
+    expect(revokeIndex).toBeLessThan(setupIndex)
+    expect(steps[revokeIndex]?.run).toBe('node .github/review-ownership/check-approval.mjs pending')
+    expect(steps.at(-1)).toMatchObject({
+      if: "failure() && steps.revoke.outcome == 'success'",
+      run: 'node .github/review-ownership/check-approval.mjs error',
+    })
+    const pythonJob = workflowJob(loadWorkflow('.github/workflows/ci.yml'), 'python-sdk')
+    expect(pythonJob.steps).toContainEqual({
+      name: 'Test production blame scoring',
+      run: "uv run --python 3.10 --with-requirements .github/review-ownership/requirements.txt python -m unittest discover -s .github/review-ownership -p 'test_*.py'",
+    })
+    expect(steps.find(step => step.name === 'Install production lexer')).toMatchObject({
+      run: 'python3 -m pip install -r .github/review-ownership/requirements.txt',
+    })
     expect(publish).toMatchObject({
       env: {
         GITHUB_TOKEN: '${{ github.token }}',