You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@shardingsphere.apache.org by zh...@apache.org on 2026/08/13 13:13:27 UTC

(shardingsphere) branch master updated: Refine issue analysis and PR review skills (#39445)

This is an automated email from the ASF dual-hosted git repository.

terrymanu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shardingsphere.git


The following commit(s) were added to refs/heads/master by this push:
     new 541ac48fec5 Refine issue analysis and PR review skills (#39445)
541ac48fec5 is described below

commit 541ac48fec53491af554ac5f4aeff81d50e3d1c1
Author: Liang Zhang <zh...@apache.org>
AuthorDate: Thu Aug 13 21:13:19 2026 +0800

    Refine issue analysis and PR review skills (#39445)
    
    * Refine gen-ut quality gates
    
    - keep semantic decisions out of mechanical rule scans
    - harden coverage and task-scope verification
    - remove unsafe gate-result reuse and add regression tests
    
    * Refine gen-ut quality gates
    
    - keep semantic decisions out of mechanical rule scans
    - harden coverage and task-scope verification
    - remove unsafe gate-result reuse and add regression tests
    
    * Refine gen-ut quality gates
    
    - keep semantic decisions out of mechanical rule scans
    - harden coverage and task-scope verification
    - remove unsafe gate-result reuse and add regression tests
    
    * Refine gen-ut quality gates
    
    - keep semantic decisions out of mechanical rule scans
    - harden coverage and task-scope verification
    - remove unsafe gate-result reuse and add regression tests
    
    * Refactor MySQL COM_QUERY original SQL byte handling
    
    * Refine issue analysis and PR review skills
---
 .codex/skills/analyze-issue/SKILL.md               | 268 ++++-----------------
 .codex/skills/analyze-issue/agents/openai.yaml     |   2 +-
 .../analyze-issue/references/output-contract.md    | 206 ++++++++++++++++
 .codex/skills/review-pr/SKILL.md                   |   8 +
 .../skills/review-pr/references/evidence-access.md |  14 +-
 .codex/skills/review-pr/scripts/review_common.py   |   8 +-
 .codex/skills/review-pr/scripts/review_ledger.py   |  71 +++++-
 .../review-pr/tests/test_build_review_inventory.py |  54 +++++
 .../skills/review-pr/tests/test_review_common.py   |  62 +++++
 .../skills/review-pr/tests/test_review_ledger.py   | 154 ++++++++++++
 10 files changed, 597 insertions(+), 250 deletions(-)

diff --git a/.codex/skills/analyze-issue/SKILL.md b/.codex/skills/analyze-issue/SKILL.md
index b3edcbeeb4e..7c9bc65392a 100644
--- a/.codex/skills/analyze-issue/SKILL.md
+++ b/.codex/skills/analyze-issue/SKILL.md
@@ -2,9 +2,8 @@
 name: analyze-issue
 description: >-
   Used to analyze Apache ShardingSphere community issues. Emphasizes root-cause-first
-  and evidence-first classification before conclusions, and by default produces
-  copy-ready GitHub issue replies in the voice of an Apache ShardingSphere
-  community maintainer followed by reference analysis for traceability.
+  and evidence-first classification before conclusions, and produces copy-ready
+  GitHub issue replies in the voice of an Apache ShardingSphere community maintainer.
 ---
 
 # Analyze Issue
@@ -16,9 +15,9 @@ Prioritize identifying the real root cause and aligning with official ShardingSp
 
 ## Default Output Contract
 
-Default to a two-part, copy-ready GitHub issue comment:
-1. **Maintainer Reply:** A concise Apache ShardingSphere maintainer reply written directly to the issue author.
-2. **Reference Analysis:** A detailed evidence-based analysis appended after the reply for reviewers, maintainers, and follow-up contributors.
+Default to one concise, copy-ready GitHub issue comment written directly to the
+issue author. Complete the evidence analysis internally, but do not append the
+detailed Reference Analysis unless the user explicitly requests it.
 
 The default maintainer reply must:
 - Start by addressing the issue author when the author is known, for example `Hi @user, thanks for the question.`
@@ -27,13 +26,10 @@ The default maintainer reply must:
 - Give the next action: correct usage, label/close recommendation, required missing facts, or PR/design expectations.
 - Read like a community member helping in the issue thread, not an external analyst summarizing the issue.
 
-After the maintainer reply, add a short bridge sentence before the reference analysis:
-`The reply above is based on the analysis below; the detailed reasoning is kept here for reference and follow-up contributors.`
-
-The default reference analysis must:
-- Preserve the detailed four-/five-section structure in `Reference Analysis Output Structure`.
-- Include evidence IDs, `Issue Type`, `Recommended Labels`, and `Next Action`.
-- Make the maintainer reply traceable without making the opening reply feel like a report.
+When returning the reply in Codex chat, wrap only the copyable GitHub-facing
+body in one fenced `markdown` block and keep any copy instruction outside it.
+Tell the user to copy only the content inside the fenced block.
+When posting directly through an API or tool, submit only the inner body.
 
 ## Community Role
 
@@ -70,13 +66,18 @@ Do not use blogs, third-party tutorials, or forum posts as evidence.
 ## Output Mode Selection
 
 Choose output mode before drafting:
-- **Maintainer Reply + Reference Analysis (default):** Use for requests to reply to an issue, draft an issue comment, answer a community question,
+- **Maintainer Reply Only (default):** Use for requests to reply to an issue, draft an issue comment, answer a community question,
   classify an issue, or when the user gives only an issue URL.
-- **Maintainer Reply Only (explicit only):** Use only when the user asks for a concise reply/comment only, no detailed analysis, or no appendix.
+- **Maintainer Reply + Reference Analysis (explicit only):** Use only when the user asks for the reply plus detailed analysis,
+  evidence IDs, traceability appendix, or follow-up contributor notes.
 - **Reference Analysis Only (explicit only):** Use only when the user asks for detailed analysis only, evidence IDs only, triage report only,
   root-cause report only, or the fixed four-/five-section structure only.
 
-Internal evidence gathering is always required. In the default output, keep evidence IDs and report sections in the appended Reference Analysis, not in the opening maintainer reply.
+Internal evidence gathering is always required. Do not expose the evidence ledger
+in the default reply unless it improves clarity or the user explicitly requests it.
+Read [output-contract.md](references/output-contract.md) when the user requests
+Reference Analysis, a two-part response, a reusable template, or multi-line code
+whose Markdown fences require special handling.
 
 ## Fast Triage Gate
 
@@ -129,19 +130,10 @@ Use `Needs More Info` only when missing facts block the supportability decision
 
 1. Identify the issue number from user input.
 2. Use the canonical URL: `https://github.com/apache/shardingsphere/issues/${issueNO}`.
-3. Try normal browsing first.
-4. If normal access fails, use curl fallback:
-
-```bash
-curl -L -sS "https://github.com/apache/shardingsphere/issues/${issueNO}"
-```
-
-5. If structured data is needed, use the GitHub API:
-
-```bash
-curl -sS -H "Accept: application/vnd.github+json" \
-  "https://api.github.com/repos/apache/shardingsphere/issues/${issueNO}"
-```
+3. Follow `AGENTS.md` for GitHub authentication, route selection, pagination,
+   sensitive-data handling, and read-only boundaries.
+4. Fetch the issue body, all relevant comments, linked same-repository issues or
+   PRs, and any other pages required by the selected evidence checks.
 
 ## Minimum Evidence Package
 
@@ -282,195 +274,16 @@ Default to maintainer replies shaped by the issue type:
 - Use one concise consolidated list and set a 7-14 day follow-up window.
 - Recommend `status: need more info`.
 
-For the default appended Reference Analysis, and for explicit Reference Analysis Only mode, use the detailed four-/five-section structures below.
-
-## Maintainer Reply Templates
-
-Use these as compact shape guides, not rigid text:
-
-Question:
-```markdown
-Hi @user, thanks for the question.
-<Direct answer from current ShardingSphere behavior.>
-<Brief reason from docs/code.>
-Community members are also welcome to share related experience, examples, or documentation improvements.
-I suggest labeling this as `type: question` and closing it once the answer is clear.
-```
-
-Misunderstanding / Invalid Usage:
-```markdown
-Hi @user, thanks for the question.
-This configuration is not supported by the current <feature> rule model.
-<Explain the two or three project-level reasons, using `we` / `current ShardingSphere` language.>
-Please <correct usage>. I suggest closing this as invalid usage / question.
-```
-
-Bug:
-```markdown
-Hi @user, thanks for reporting this.
-This looks like a bug in <module/path> because <documented expected behavior> does not match <current code behavior>.
-The fix should cover <key classes/paths> and include <test scope>. Contributors are welcome to submit a PR with code and tests.
-```
-
-Duplicate:
-```markdown
-Hi @user, thanks for reporting this.
-This is a duplicate of #<issue-or-pr>, which already fixed or tracks the same root cause in <module/class>.
-Please verify with a version that includes #<fix-pr>. I suggest labeling this as `type: duplicate` and closing it as duplicate.
-```
-
-Enhancement:
-```markdown
-Hi @user, thanks for the suggestion.
-This is not supported today and should be handled as an enhancement rather than a bug.
-Before implementation, we need to define <semantic contract>, <compatibility impact>, and <test scope>. I suggest labeling this as `type: enhancement`.
-```
-
-Needs More Info:
-```markdown
-Hi @user, thanks for reporting this.
-We need a bit more information before we can classify this issue:
-- <blocking fact 1>
-- <blocking fact 2>
-Please provide these details within <7-14 days>. If there is no update, we may close this as inactive / invalid due to insufficient information.
-```
-
-## Reference Analysis Output Structure
-
-Use this section for the default appended Reference Analysis and for explicit Reference Analysis Only mode.
-
-### Reference Analysis Markdown Requirements
-
-- Format the reference analysis as GitHub-flavored Markdown that can be pasted directly into a GitHub issue comment after the maintainer reply and bridge sentence.
-- The GitHub-facing issue analysis body must not be wrapped in a code fence, blockquote, XML/HTML container, or plain-text transcript.
-- Use the same natural language as the user request for explanatory prose unless the user explicitly asks for another language.
-- Keep the mandatory Markdown structure unchanged regardless of output language.
-- Keep mandatory section titles and stable fields in English, such as `Problem Understanding`, `Root Cause`, `Problem Analysis`,
-  `Problem Conclusion`, `Evidence Confidence`, `Issue Type`, `Recommended Labels`, and `Next Action`.
-- Use Markdown headings (`### Problem Understanding`, `### Root Cause`, etc.) with a blank line before and after each heading.
-- Use short unordered bullets under each heading; use bold inline labels such as `Observation:`, `Inference:`, `Confidence:`, and `Action:`.
-- Use repo-relative paths with line numbers, for example `infra/.../Foo.java:123`; do not use local absolute file paths in GitHub-facing analysis text.
-- Keep evidence IDs, labels, severity values, topology values, commands, class names, method names, SQL, YAML, and Java snippets in their original English/code form.
-- Prefer bullets over tables. Use tables only for compact status summaries that remain readable in GitHub's issue comment pane.
-- Keep command evidence in inline code or short fenced blocks; avoid long raw JSON, full logs, or unrendered terminal transcripts.
-- Before final output, perform a formatting self-check on the inner GitHub-facing issue analysis body:
-    - The inner GitHub-facing issue analysis body is not wrapped in a code fence, blockquote, XML/HTML container, or transcript.
-    - The default body starts with a maintainer reply, includes the bridge sentence, and then contains the required `###` headings for the selected issue-type template.
-    - The reference analysis contains `Problem Conclusion` with the required conclusion fields.
-    - File references are repo-relative paths with line numbers, not local absolute paths.
-    - Stable section titles, evidence IDs, labels, severity values, and conclusion field labels remain in English/code form.
-
-### Codex Chat Delivery
-
-- When returning the default output in Codex chat, wrap the complete two-part GitHub issue comment in a fenced `markdown` code block.
-- When returning Maintainer Reply Only or Reference Analysis Only, wrap only the requested GitHub-facing body in a fenced `markdown` code block.
-- The fenced code block is only a chat delivery wrapper; it is not part of the GitHub-facing issue analysis body.
-- Tell the user to copy only the content inside the fenced block.
-- Keep any copy instruction outside the fenced block.
-- When posting directly to GitHub through an API or tool, submit only the inner GitHub-facing issue analysis body and do not include the outer fence.
-- Apply formatting self-checks to the inner GitHub-facing body, not to the chat delivery wrapper.
-
-### Markdown Fence Safety
-
-- Default to inline code for short SQL, commands, paths, and single-line examples inside GitHub-facing issue comments.
-- Avoid inner fenced code blocks inside the GitHub-facing body unless the snippet is multi-line or syntax highlighting materially improves readability.
-- If the GitHub-facing body contains any inner fenced code block, the outer Codex chat delivery fence must use more backticks than the longest inner fence.
-- Before final output, verify every inner fence has a matching closing fence, the outer delivery fence cannot be closed by an inner fence, and the copyable body starts after the outer opening fence and ends before the outer closing fence.
-- If fence safety is uncertain, remove the inner fenced block and use inline code or a plain bullet instead.
-
-In Reference Analysis mode, use this GitHub Markdown skeleton for Question, Misunderstanding / Invalid Usage, and Duplicate:
-
-```markdown
-### Problem Understanding
-
-- **Issue:** ...
-- **Topology:** ...
-- **Observed Evidence:** `OBS-1`, `OBS-2`
-
-### Root Cause
-
-- **Observation:** ...
-- **Inference:** ...
-- **Confidence:** High/Medium/Low
-
-### Problem Analysis
-
-- **Issue Type:** Question / Misunderstanding / Invalid Usage / Duplicate
-- **Evidence:** ...
-- **Label Recommendation:** ...
-
-### Problem Conclusion
-
-- **Evidence Confidence:** High/Medium/Low
-- **Impact Scope:** ...
-- **Topology:** JDBC/Proxy + Standalone/Cluster
-- **Issue Type:** ...
-- **Duplicate Of:** #issue-or-pr (Duplicate only)
-- **Fix PR:** #pr (Duplicate only)
-- **Merged In:** commit/milestone/version if known (Duplicate only)
-- **Recommended Labels:** ...
-- **Next Action:** ...
-```
-
-In Reference Analysis mode, use this GitHub Markdown skeleton for Bug and Enhancement:
-
-```markdown
-### Problem Understanding
-
-- **Issue:** ...
-- **Topology:** ...
-- **Observed Evidence:** `OBS-1`, `OBS-2`
-
-### Root Cause
-
-- **Observation:** ...
-- **Inference:** ...
-- **Confidence:** High/Medium/Low
-
-### Problem Analysis
-
-- **Issue Type:** Bug / Enhancement
-- **Evidence:** ...
-- **Compatibility Checklist:** Behavior / Config / API-SPI / SQL
-
-### Code-Level Design Suggestions
-
-- **Affected Modules:** ...
-- **Key Classes:** ...
-- **Required Test Scope:** ...
-- **Rollback Hint:** ...
-
-### Problem Conclusion
-
-- **Evidence Confidence:** High/Medium/Low
-- **Severity:** S0/S1/S2/S3
-- **Impact Scope:** ...
-- **Topology:** JDBC/Proxy + Standalone/Cluster
-- **Issue Type:** ...
-- **Recommended Labels:** ...
-- **Next Action:** ...
-- **Compatibility:** Behavior/Config/API-SPI/SQL
-- **Regression Scope:** ...
-```
-
-- Reference four-section structure for Question, Misunderstanding / Invalid Usage, and Duplicate: Problem Understanding, Root Cause, Problem Analysis, Problem Conclusion.
-- Reference five-section structure for Bug and Enhancement: Problem Understanding, Root Cause, Problem Analysis, Code-Level Design Suggestions, Problem Conclusion.
-
-At the end of `Problem Conclusion`, append:
-- `Evidence Confidence: High/Medium/Low`
-- `Severity: S0/S1/S2/S3` (Bug/Enhancement required)
-- `Impact Scope: ...`
-- `Topology: JDBC/Proxy + Standalone/Cluster`
-- `Issue Type: ...`
-- `Duplicate Of: #issue-or-pr` (Duplicate required)
-- `Fix PR: #pr` (Duplicate required when a fixing PR is found)
-- `Merged In: commit/milestone/version if known` (Duplicate required when known)
-- `Recommended Labels: ...`
-- `Next Action: ...`
+For explicit Maintainer Reply + Reference Analysis and Reference Analysis Only
+modes, use the detailed four-/five-section structures in the output reference.
+
+## Detailed Output Resources
 
-For Bug/Enhancement, also append:
-- `Compatibility: Behavior/Config/API-SPI/SQL`
-- `Regression Scope: ...`
+Read [output-contract.md](references/output-contract.md) only when its trigger in
+`Output Mode Selection` applies. It contains reusable maintainer-reply templates,
+the Reference Analysis schemas, Codex chat delivery rules, and Markdown fence
+safety checks. Its templates guide structure; they do not replace evidence-based
+wording for the current issue.
 
 ## Community Voice Guardrails
 
@@ -487,7 +300,8 @@ Before final output, run this self-check:
 - **Audience Check:** The reply addresses the issue author directly when the author is known.
 - **Decision Check:** The first paragraph states the supportability/classification decision.
 - **Reason Check:** The explanation is grounded in official docs, repository code/tests, or issue content.
-- **Traceability Check:** Default output includes the bridge sentence and appended Reference Analysis.
+- **Traceability Check:** The reply is supported by the internal evidence ledger;
+  explicit two-part output includes the bridge sentence and Reference Analysis.
 - **Action Check:** The reply gives a clear next action, label recommendation, close recommendation, or PR expectation.
 
 ## Missing Information Handling
@@ -525,21 +339,25 @@ Extended types are valid final classifications when evidence supports them:
 - Security (use responsible security disclosure workflow)
 
 Each must still include a clear maintainer reply by default, with labels and next action.
-Default output must also append Reference Analysis. If the user explicitly requests only one output mode, return only that requested mode.
+Append Reference Analysis only when the user explicitly requests a detailed or
+two-part output mode.
+
+For a suspected undisclosed vulnerability, do not reproduce or deepen sensitive
+details in a public issue reply. Direct the reporter to the responsible disclosure
+process in `docs/community/content/security/_index.en.md` and keep the public reply
+limited to that safe next action.
 
 ## Lightweight Lint Recommendation
 
-For default output, verify:
+For Maintainer Reply output, verify:
 - The reply addresses the issue author or community directly.
 - The first paragraph contains the decision.
 - The maintainer reply does not contain detailed report headings.
-- The bridge sentence appears before Reference Analysis.
-- The Reference Analysis contains required detailed sections and conclusion fields.
 - The reply includes a next action and label/close recommendation when appropriate.
 - For questions, the reply invites community participation when appropriate.
-- If the Codex chat delivery wrapper contains inner fenced code blocks, the outer fence is longer than every inner fence and all fences are closed.
 
-For Reference Analysis output, a local checker (script or CI step) may verify:
+For explicit Reference Analysis output, also verify:
+- The bridge sentence appears before Reference Analysis in two-part mode.
 - Required detailed sections exist.
 - Required conclusion fields exist.
 - Evidence IDs are present and referenced.
@@ -552,5 +370,5 @@ If lint fails, mark analysis as incomplete.
 - Do not recommend behavior that conflicts with official ShardingSphere conventions.
 - Do not provide certainty when evidence is insufficient.
 - Do not output a neutral machine-style report when the user asked for a reply to an issue author.
-- Do not omit the default Reference Analysis unless the user explicitly asks for maintainer reply only.
+- Do not append Reference Analysis unless the user explicitly requests it.
 - Source and workaround restrictions are governed by Source Policy and Response Strategy by Type.
diff --git a/.codex/skills/analyze-issue/agents/openai.yaml b/.codex/skills/analyze-issue/agents/openai.yaml
index 905bff26d16..e924e8dc555 100644
--- a/.codex/skills/analyze-issue/agents/openai.yaml
+++ b/.codex/skills/analyze-issue/agents/openai.yaml
@@ -18,4 +18,4 @@
 interface:
   display_name: "Analyze Issue"
   short_description: "Maintainer replies with traceable issue analysis"
-  default_prompt: "Use $analyze-issue to draft a maintainer reply plus reference analysis for this ShardingSphere issue (analysis only, no code changes)."
+  default_prompt: "Use $analyze-issue to analyze this ShardingSphere issue and draft a concise maintainer reply (analysis only, no code changes)."
diff --git a/.codex/skills/analyze-issue/references/output-contract.md b/.codex/skills/analyze-issue/references/output-contract.md
new file mode 100644
index 00000000000..8b370ccb073
--- /dev/null
+++ b/.codex/skills/analyze-issue/references/output-contract.md
@@ -0,0 +1,206 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+# Analyze Issue Output Contract
+
+Read this reference when the user explicitly requests Reference Analysis, a
+two-part response, a reusable reply template, or multi-line code whose Markdown
+fences require special handling. Complete the main Skill's evidence workflow
+before applying these output shapes.
+
+## Maintainer Reply Templates
+
+Use these as compact shape guides, not rigid text.
+
+### Question
+
+```markdown
+Hi @user, thanks for the question.
+<Direct answer from current ShardingSphere behavior.>
+<Brief reason from docs/code.>
+Community members are also welcome to share related experience, examples, or documentation improvements.
+I suggest labeling this as `type: question` and closing it once the answer is clear.
+```
+
+### Misunderstanding / Invalid Usage
+
+```markdown
+Hi @user, thanks for the question.
+This configuration is not supported by the current <feature> rule model.
+<Explain the two or three project-level reasons, using `we` / `current ShardingSphere` language.>
+Please <correct usage>. I suggest closing this as invalid usage / question.
+```
+
+### Bug
+
+```markdown
+Hi @user, thanks for reporting this.
+This looks like a bug in <module/path> because <documented expected behavior> does not match <current code behavior>.
+The fix should cover <key classes/paths> and include <test scope>. Contributors are welcome to submit a PR with code and tests.
+```
+
+### Duplicate
+
+```markdown
+Hi @user, thanks for reporting this.
+This is a duplicate of #<issue-or-pr>, which already fixed or tracks the same root cause in <module/class>.
+Please verify with a version that includes #<fix-pr>. I suggest labeling this as `type: duplicate` and closing it as duplicate.
+```
+
+### Enhancement
+
+```markdown
+Hi @user, thanks for the suggestion.
+This is not supported today and should be handled as an enhancement rather than a bug.
+Before implementation, we need to define <semantic contract>, <compatibility impact>, and <test scope>. I suggest labeling this as `type: enhancement`.
+```
+
+### Needs More Info
+
+```markdown
+Hi @user, thanks for reporting this.
+We need a bit more information before we can classify this issue:
+- <blocking fact 1>
+- <blocking fact 2>
+Please provide these details within <7-14 days>. If there is no update, we may close this as inactive / invalid due to insufficient information.
+```
+
+## Reference Analysis Requirements
+
+- In two-part mode, put the maintainer reply first, then add this bridge sentence:
+  `The reply above is based on the analysis below; the detailed reasoning is kept here for reference and follow-up contributors.`
+- Use the same natural language as the user request unless the user asks for
+  another language.
+- Keep stable headings and fields in English: `Problem Understanding`, `Root
+  Cause`, `Problem Analysis`, `Code-Level Design Suggestions`, `Problem
+  Conclusion`, `Evidence Confidence`, `Issue Type`, `Recommended Labels`, and
+  `Next Action`.
+- Use short bullets, evidence IDs, and repo-relative paths with line numbers.
+  Use bold inline labels such as `Observation:`, `Inference:`, `Confidence:`,
+  and `Action:`. Do not expose local absolute paths.
+- Prefer bullets over tables and concise evidence over raw JSON, full logs, or
+  terminal transcripts. Keep command evidence in inline code or short fenced
+  blocks.
+
+Use this four-section shape for Question, Misunderstanding / Invalid Usage, and
+Duplicate:
+
+```markdown
+### Problem Understanding
+
+- **Issue:** ...
+- **Topology:** ...
+- **Observed Evidence:** `OBS-1`, `OBS-2`
+
+### Root Cause
+
+- **Observation:** ...
+- **Inference:** ...
+- **Confidence:** High/Medium/Low
+
+### Problem Analysis
+
+- **Issue Type:** Question / Misunderstanding / Invalid Usage / Duplicate
+- **Evidence:** ...
+- **Label Recommendation:** ...
+
+### Problem Conclusion
+
+- **Evidence Confidence:** High/Medium/Low
+- **Impact Scope:** ...
+- **Topology:** JDBC/Proxy + Standalone/Cluster
+- **Issue Type:** ...
+- **Duplicate Of:** #issue-or-pr (Duplicate only)
+- **Fix PR:** #pr (Duplicate only)
+- **Merged In:** commit/milestone/version if known (Duplicate only)
+- **Recommended Labels:** ...
+- **Next Action:** ...
+```
+
+Use this five-section shape for Bug and Enhancement:
+
+```markdown
+### Problem Understanding
+
+- **Issue:** ...
+- **Topology:** ...
+- **Observed Evidence:** `OBS-1`, `OBS-2`
+
+### Root Cause
+
+- **Observation:** ...
+- **Inference:** ...
+- **Confidence:** High/Medium/Low
+
+### Problem Analysis
+
+- **Issue Type:** Bug / Enhancement
+- **Evidence:** ...
+- **Compatibility Checklist:** Behavior / Config / API-SPI / SQL
+
+### Code-Level Design Suggestions
+
+- **Affected Modules:** ...
+- **Key Classes:** ...
+- **Required Test Scope:** ...
+- **Rollback Hint:** ...
+
+### Problem Conclusion
+
+- **Evidence Confidence:** High/Medium/Low
+- **Severity:** S0/S1/S2/S3
+- **Impact Scope:** ...
+- **Topology:** JDBC/Proxy + Standalone/Cluster
+- **Issue Type:** ...
+- **Recommended Labels:** ...
+- **Next Action:** ...
+- **Compatibility:** Behavior/Config/API-SPI/SQL
+- **Regression Scope:** ...
+```
+
+## Codex Chat Delivery
+
+- Wrap the requested GitHub-facing body in exactly one outer fenced `markdown`
+  block and keep any copy instruction outside it.
+- The outer fence is a chat delivery wrapper, not part of the GitHub comment.
+  When posting through an API or tool, submit only the inner body.
+- Default to inline code inside the body. Use an inner fence only for multi-line
+  content that materially benefits from it.
+- If the body contains an inner fence, use more backticks for the outer fence
+  than for every inner fence. Verify that all fences close and that the copyable
+  body begins and ends inside the outer fence.
+- If fence safety is uncertain, replace the inner fence with inline code or a
+  plain bullet.
+
+## Output Self-Check
+
+- The opening reply addresses the author when known and states the decision in
+  its first paragraph.
+- A two-part response contains the exact bridge sentence and the required four-
+  or five-section Reference Analysis.
+- Every conclusion cites at least one `OBS-*` or `INF-*` evidence ID, and every
+  `INF-*` derives from one or more observations.
+- `Problem Conclusion` includes all fields required by the selected shape.
+- Documentation evidence has concrete URLs; code evidence has repository paths
+  or class names.
+- Labels, severity, topology, commands, class names, method names, SQL, YAML,
+  and Java snippets retain their original English or code form.
+- The inner GitHub-facing body is not itself wrapped in a blockquote, HTML/XML
+  container, or transcript.
+- A local checker or CI step may enforce the required sections, conclusion
+  fields, evidence references, and type-label consistency. Treat a failed check
+  as incomplete output.
diff --git a/.codex/skills/review-pr/SKILL.md b/.codex/skills/review-pr/SKILL.md
index 6aceef3c25e..9a3f8cad167 100644
--- a/.codex/skills/review-pr/SKILL.md
+++ b/.codex/skills/review-pr/SKILL.md
@@ -115,6 +115,14 @@ readiness conclusion.
 Read [evidence-access.md](references/evidence-access.md) whenever current GitHub,
 CI, Actions, or third-party behavior evidence is required.
 
+AI-assistance disclosure is a mergeability concern, not a code-correctness
+signal. In Mergeability Review or an explicit policy-compliance review, apply
+`AI_POLICY.md` only when public PR evidence explicitly establishes material AI
+assistance. Verify that the PR description names the tool and affected files or
+scope. Never infer AI use from code, prose style, metadata, or an automated
+classifier; without explicit public evidence, missing disclosure is neither a
+finding nor an evidence gap.
+
 ## Finding Proof Gate
 
 A candidate may become a blocking issue only when all five conditions hold:
diff --git a/.codex/skills/review-pr/references/evidence-access.md b/.codex/skills/review-pr/references/evidence-access.md
index 2799acb34e4..87293c61bc6 100644
--- a/.codex/skills/review-pr/references/evidence-access.md
+++ b/.codex/skills/review-pr/references/evidence-access.md
@@ -36,12 +36,12 @@ the selected review focus requires CI or Actions evidence.
 
 ## GitHub Read Strategy
 
-Use the first available read-only route that can obtain the required endpoint:
-
-1. Authenticated GitHub connector or app.
-2. Authenticated GitHub CLI.
-3. Token-backed GitHub REST or GraphQL.
-4. Anonymous API or HTML.
+Follow the GitHub access contract in `AGENTS.md` before choosing a read route.
+In particular, use the first configured token in its required order and call
+the GitHub REST or GraphQL API directly when a token is available. Do not search
+for, inspect, or invoke `gh` in that case. When neither token is configured, use
+an authenticated read-only connector or app when it can obtain the required
+endpoint, then `gh` or anonymous API/HTML as needed.
 
 Fetch every page needed for authoritative PR files, commits, comments, reviews,
 review threads, and focus-required checks. Classify availability per endpoint;
@@ -84,7 +84,7 @@ When CI is in scope:
   conclusion incomplete.
 - Passing CI supports only the behavior it actually exercises; it never
   replaces code, root-cause, scope, compatibility, or test-validity review.
-- If Actions logs are needed, try authenticated CLI or API access before
+- If Actions logs are needed, follow the GitHub read strategy above before
   treating anonymous failures as missing evidence.
 - Download large logs to a system temporary file and inspect only focused,
   sanitized excerpts. Do not copy raw logs into the review.
diff --git a/.codex/skills/review-pr/scripts/review_common.py b/.codex/skills/review-pr/scripts/review_common.py
index 2c4adafe8f5..8159afd1ef5 100644
--- a/.codex/skills/review-pr/scripts/review_common.py
+++ b/.codex/skills/review-pr/scripts/review_common.py
@@ -68,14 +68,14 @@ def categorize(path: str) -> str:
         return "release-notes"
     if "distribution" in path:
         return "distribution"
-    if "/src/main/java/" in path:
+    if path.startswith("src/main/java/") or "/src/main/java/" in path:
         return "production-java"
-    if "/src/test/" in path:
+    if path.startswith("src/test/") or "/src/test/" in path:
         return "tests"
-    if path.startswith("docs/") or path.endswith((".md", ".adoc")):
-        return "docs"
     if path.startswith(".github/") or path.endswith((".xml", ".properties", ".yml", ".yaml", ".toml", ".gradle")):
         return "build-config"
+    if path.startswith("docs/") or path.endswith((".md", ".adoc")):
+        return "docs"
     if "target/" in path or "/generated/" in path:
         return "generated"
     return "other"
diff --git a/.codex/skills/review-pr/scripts/review_ledger.py b/.codex/skills/review-pr/scripts/review_ledger.py
index 246384f9f6b..0441a79f63b 100755
--- a/.codex/skills/review-pr/scripts/review_ledger.py
+++ b/.codex/skills/review-pr/scripts/review_ledger.py
@@ -80,9 +80,44 @@ def ensure_safe_ledger_file(ledger_file: Path) -> None:
         raise RuntimeError(f"Ledger is outside the private review ledger root: {target}")
 
 
-def validate_schema(ledger: dict[str, Any]) -> None:
+def validate_schema(ledger: object) -> None:
+    if not isinstance(ledger, dict):
+        raise RuntimeError("Malformed review coverage ledger")
     if LEDGER_KIND != ledger.get("kind") or LEDGER_VERSION != ledger.get("version"):
         raise RuntimeError("Unsupported or invalid review coverage ledger")
+    required_types = {
+        "scope": dict,
+        "files": list,
+        "findings": list,
+        "passes": list,
+        "review_revision": int,
+    }
+    if any(not isinstance(ledger.get(key), expected_type) for key, expected_type in required_types.items()):
+        raise RuntimeError("Malformed review coverage ledger")
+    if not isinstance(ledger["scope"].get("github_files", {}), dict):
+        raise RuntimeError("Malformed review coverage ledger scope")
+    entry_schemas = (
+        ("file", ledger["files"], {
+            "path": str, "status": str, "clusters": list, "risk_axes": list, "findings": list,
+        }),
+        ("finding", ledger["findings"], {
+            "id": str, "status": str, "origin": str, "fix_boundary": str, "evidence": list, "full_path": list,
+            "counter_evidence": list, "necessity": str, "scope_proof": str, "files": list, "reason": str,
+        }),
+        ("pass", ledger["passes"], {
+            "focus": str, "new_findings": int, "review_revision": int,
+        }),
+    )
+    for entry_name, entries, schema in entry_schemas:
+        for entry in entries:
+            if not isinstance(entry, dict) or any(not isinstance(entry.get(key), expected_type) for key, expected_type in schema.items()):
+                raise RuntimeError(f"Malformed review coverage ledger {entry_name} entry")
+    list_fields = ((ledger["files"], ("clusters", "risk_axes", "findings")),
+                   (ledger["findings"], ("evidence", "full_path", "counter_evidence", "files")))
+    for entries, fields in list_fields:
+        for entry in entries:
+            if any(not all(isinstance(value, str) for value in entry[field]) for field in fields):
+                raise RuntimeError("Malformed review coverage ledger list entry")
 
 
 def read_ledger(value: str | Path) -> dict[str, Any]:
@@ -116,6 +151,15 @@ def unique_extend(values: list[str], additions: Iterable[str]) -> None:
             values.append(each)
 
 
+def has_text(value: object) -> bool:
+    return isinstance(value, str) and bool(value.strip())
+
+
+def has_text_entries(values: Iterable[object]) -> bool:
+    entries = list(values)
+    return bool(entries) and all(has_text(each) for each in entries)
+
+
 def sync_file_findings(ledger: dict[str, Any]) -> None:
     for each in ledger["files"]:
         each["findings"] = []
@@ -230,6 +274,7 @@ def cmd_add_pass(args: argparse.Namespace) -> int:
 
 
 def validate_ledger(ledger: dict[str, Any]) -> list[str]:
+    validate_schema(ledger)
     result: list[str] = []
     github_files = ledger["scope"].get("github_files", {})
     if github_files.get("provided") and not github_files.get("matched"):
@@ -249,16 +294,16 @@ def validate_ledger(ledger: dict[str, Any]) -> list[str]:
         result.append(f"Blocked files require more evidence or an incomplete result: {len(blocked_files)}")
     missing_clusters = [each["path"] for each in ledger["files"]
                         if each["status"] in SUBSTANTIVE_FILE_STATUSES
-                        and (not each.get("clusters") or not all(each["clusters"]))]
+                        and not has_text_entries(each["clusters"])]
     if missing_clusters:
         result.append(f"Substantive files missing behavior clusters: {len(missing_clusters)}")
     missing_risk_axes = [each["path"] for each in ledger["files"]
                          if each["status"] in SUBSTANTIVE_FILE_STATUSES
-                         and (not each.get("risk_axes") or not all(each["risk_axes"]))]
+                         and not has_text_entries(each["risk_axes"])]
     if missing_risk_axes:
         result.append(f"Substantive files missing reviewed risk axes: {len(missing_risk_axes)}")
     unexplained_files = [each["path"] for each in ledger["files"]
-                         if each["status"] in EXPLAINED_FILE_STATUSES and not each.get("reason")]
+                         if each["status"] in EXPLAINED_FILE_STATUSES and not has_text(each.get("reason"))]
     if unexplained_files:
         result.append(f"Non-substantive file classifications missing reasons: {len(unexplained_files)}")
     finding_counts = Counter(each["id"] for each in ledger["findings"])
@@ -285,24 +330,24 @@ def validate_ledger(ledger: dict[str, Any]) -> list[str]:
         if unknown_files:
             result.append(f"{each['id']} references files outside scope: {', '.join(unknown_files)}")
         if "confirmed" == each["status"]:
-            if not each["origin"]:
+            if not has_text(each["origin"]):
                 result.append(f"{each['id']} confirmed finding is missing origin")
-            if not each["fix_boundary"]:
+            if not has_text(each["fix_boundary"]):
                 result.append(f"{each['id']} confirmed finding is missing fix boundary")
-            if not each["evidence"]:
+            if not has_text_entries(each["evidence"]):
                 result.append(f"{each['id']} confirmed finding is missing evidence")
-            if not each.get("full_path"):
+            if not has_text_entries(each["full_path"]):
                 result.append(f"{each['id']} confirmed finding is missing full-path review")
-            if not each.get("counter_evidence"):
+            if not has_text_entries(each["counter_evidence"]):
                 result.append(f"{each['id']} confirmed finding is missing counter-evidence review")
-            if not each.get("necessity"):
+            if not has_text(each["necessity"]):
                 result.append(f"{each['id']} confirmed finding is missing necessity")
-            if not each.get("scope_proof"):
+            if not has_text(each["scope_proof"]):
                 result.append(f"{each['id']} confirmed finding is missing scope proof")
-            if not each["files"]:
+            if not has_text_entries(each["files"]):
                 result.append(f"{each['id']} confirmed finding is missing scope files")
         if "review-incomplete-gap" == each["status"]:
-            if not each["reason"]:
+            if not has_text(each["reason"]):
                 result.append(f"{each['id']} incomplete gap is missing a reason")
             result.append(f"{each['id']} requires a Review Incomplete result")
     pass_foci = {each.get("focus") for each in ledger["passes"]}
diff --git a/.codex/skills/review-pr/tests/test_build_review_inventory.py b/.codex/skills/review-pr/tests/test_build_review_inventory.py
new file mode 100644
index 00000000000..347a6ebfa12
--- /dev/null
+++ b/.codex/skills/review-pr/tests/test_build_review_inventory.py
@@ -0,0 +1,54 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import sys
+import unittest
+from argparse import Namespace
+from pathlib import Path
+from unittest.mock import patch
+
+
+SCRIPTS_DIR = Path(__file__).resolve().parents[1] / "scripts"
+if str(SCRIPTS_DIR) not in sys.path:
+    sys.path.insert(0, str(SCRIPTS_DIR))
+
+import build_review_inventory as inventory
+
+
+class BuildReviewInventoryTest(unittest.TestCase):
+
+    @patch.object(inventory, "run_git")
+    @patch.object(inventory, "get_repo_root", return_value=Path("/repo"))
+    def test_build_inventory_uses_triple_dot_scope(self, _, run_git):
+        run_git.side_effect = [
+            "base-sha\n",
+            "head-sha\n",
+            "merge-base\n",
+            "M\tsrc/main/java/Foo.java\nA\tdocs/guide.md\n",
+            " M unrelated.java\n",
+        ]
+        args = Namespace(base_ref="base", head_ref="head", previous_head=None, github_files=None)
+        actual = inventory.build_inventory(args)
+        self.assertEqual("merge-base", actual["scope"]["merge_base"])
+        self.assertEqual(2, actual["scope"]["changed_file_count"])
+        self.assertEqual(1, actual["dirty_worktree"]["count"])
+        self.assertEqual("src/main/java/Foo.java", actual["changed_files_by_category"]["production-java"][0]["path"])
+        run_git.assert_any_call(["diff", "--name-status", "merge-base..head"], Path("/repo"))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/.codex/skills/review-pr/tests/test_review_common.py b/.codex/skills/review-pr/tests/test_review_common.py
new file mode 100644
index 00000000000..ce399b9a915
--- /dev/null
+++ b/.codex/skills/review-pr/tests/test_review_common.py
@@ -0,0 +1,62 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+
+SCRIPTS_DIR = Path(__file__).resolve().parents[1] / "scripts"
+if str(SCRIPTS_DIR) not in sys.path:
+    sys.path.insert(0, str(SCRIPTS_DIR))
+
+from review_common import ChangedFile, categorize, compare_github_files, parse_name_status
+
+
+class ReviewCommonTest(unittest.TestCase):
+
+    def test_parse_name_status(self):
+        actual = parse_name_status("M\tmodule/Foo.java\nR100\told.md\tnew.md\nC75\tsource.yml\tcopy.yml\n")
+        self.assertEqual([
+            ChangedFile(status="M", path="module/Foo.java"),
+            ChangedFile(status="R100", path="new.md", old_path="old.md"),
+            ChangedFile(status="C75", path="copy.yml", old_path="source.yml"),
+        ], actual)
+
+    def test_categorize_root_and_module_paths(self):
+        self.assertEqual("production-java", categorize("src/main/java/Foo.java"))
+        self.assertEqual("production-java", categorize("module/src/main/java/Foo.java"))
+        self.assertEqual("tests", categorize("src/test/java/FooTest.java"))
+        self.assertEqual("tests", categorize("module/src/test/resources/case.yaml"))
+        self.assertEqual("build-config", categorize(".github/README.md"))
+        self.assertEqual("docs", categorize("docs/community/index.md"))
+
+    def test_compare_github_files_reports_bounded_differences(self):
+        with tempfile.TemporaryDirectory() as temp_dir:
+            github_files = Path(temp_dir) / "github-files.txt"
+            github_files.write_text("a.md\nb.md\nc.md\n", encoding="utf-8")
+            actual = compare_github_files(["a.md", "d.md", "e.md"], str(github_files), limit=1)
+        self.assertFalse(actual["matched"])
+        self.assertEqual(3, actual["github_count"])
+        self.assertEqual(3, actual["local_count"])
+        self.assertEqual(["b.md"], actual["only_in_github"])
+        self.assertEqual(["d.md"], actual["only_in_local"])
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/.codex/skills/review-pr/tests/test_review_ledger.py b/.codex/skills/review-pr/tests/test_review_ledger.py
new file mode 100644
index 00000000000..e07d19e154c
--- /dev/null
+++ b/.codex/skills/review-pr/tests/test_review_ledger.py
@@ -0,0 +1,154 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import sys
+import tempfile
+import unittest
+from argparse import Namespace
+from contextlib import redirect_stdout
+from io import StringIO
+from pathlib import Path
+from unittest.mock import patch
+
+
+SCRIPTS_DIR = Path(__file__).resolve().parents[1] / "scripts"
+if str(SCRIPTS_DIR) not in sys.path:
+    sys.path.insert(0, str(SCRIPTS_DIR))
+
+import review_ledger as ledger
+
+
+class ReviewLedgerTest(unittest.TestCase):
+
+    def test_complete_ledger_is_valid(self):
+        self.assertEqual([], ledger.validate_ledger(self.create_valid_ledger()))
+
+    def test_confirmed_finding_requires_non_blank_proof(self):
+        actual_ledger = self.create_valid_ledger()
+        actual_ledger["findings"] = [{
+            "id": "F1",
+            "status": "confirmed",
+            "origin": " ",
+            "fix_boundary": "boundary",
+            "evidence": [""],
+            "full_path": [" "],
+            "counter_evidence": [],
+            "necessity": " ",
+            "scope_proof": " ",
+            "files": [""],
+            "reason": "",
+        }]
+        errors = ledger.validate_ledger(actual_ledger)
+        self.assertIn("F1 confirmed finding is missing origin", errors)
+        self.assertIn("F1 confirmed finding is missing evidence", errors)
+        self.assertIn("F1 confirmed finding is missing full-path review", errors)
+        self.assertIn("F1 confirmed finding is missing counter-evidence review", errors)
+        self.assertIn("F1 confirmed finding is missing necessity", errors)
+        self.assertIn("F1 confirmed finding is missing scope proof", errors)
+        self.assertIn("F1 confirmed finding is missing scope files", errors)
+
+    def test_malformed_ledger_is_rejected(self):
+        actual_ledger = self.create_valid_ledger()
+        del actual_ledger["files"]
+        with self.assertRaisesRegex(RuntimeError, "Malformed review coverage ledger"):
+            ledger.validate_ledger(actual_ledger)
+
+    def test_non_mapping_ledger_is_rejected(self):
+        with self.assertRaisesRegex(RuntimeError, "Malformed review coverage ledger"):
+            ledger.validate_schema([])
+
+    def test_malformed_nested_ledger_value_is_rejected(self):
+        actual_ledger = self.create_valid_ledger()
+        actual_ledger["files"][0]["clusters"] = [{}]
+        with self.assertRaisesRegex(RuntimeError, "Malformed review coverage ledger list entry"):
+            ledger.validate_ledger(actual_ledger)
+
+    def test_convergence_pass_must_follow_latest_review_change(self):
+        actual_ledger = self.create_valid_ledger()
+        actual_ledger["review_revision"] = 2
+        errors = ledger.validate_ledger(actual_ledger)
+        self.assertIn("Latest convergence pass predates a file or finding change", errors)
+
+    def test_github_file_list_must_match_local_scope(self):
+        actual_ledger = self.create_valid_ledger()
+        actual_ledger["scope"]["github_files"] = {"provided": True, "matched": False}
+        errors = ledger.validate_ledger(actual_ledger)
+        self.assertIn("GitHub file list does not match local triple-dot scope", errors)
+
+    def test_confirmed_findings_must_have_distinct_fix_boundaries(self):
+        actual_ledger = self.create_valid_ledger()
+        actual_ledger["findings"] = [self.create_confirmed_finding("F1"), self.create_confirmed_finding("F2")]
+        errors = ledger.validate_ledger(actual_ledger)
+        self.assertIn("Confirmed findings share fix boundaries: 1", errors)
+
+    def test_cleanup_removes_only_validated_ledger_directory(self):
+        with tempfile.TemporaryDirectory() as temp_dir, patch.object(ledger.tempfile, "gettempdir", return_value=temp_dir):
+            ledger_dir = ledger.ledger_root() / "candidate"
+            ledger_dir.mkdir()
+            ledger_file = ledger_dir / ledger.LEDGER_FILE_NAME
+            ledger.write_ledger(ledger_file, self.create_valid_ledger())
+            with redirect_stdout(StringIO()):
+                self.assertEqual(0, ledger.cmd_cleanup(Namespace(ledger=str(ledger_file))))
+            self.assertFalse(ledger_dir.exists())
+            outside_file = Path(temp_dir) / ledger.LEDGER_FILE_NAME
+            outside_file.write_text("{}", encoding="utf-8")
+            with self.assertRaisesRegex(RuntimeError, "outside the private review ledger root"):
+                ledger.ensure_safe_ledger_file(outside_file)
+
+    @staticmethod
+    def create_valid_ledger():
+        review_passes = [{
+            "focus": focus,
+            "new_findings": 0,
+            "review_revision": 1,
+        } for focus in ledger.PASS_FOCUSES]
+        return {
+            "kind": ledger.LEDGER_KIND,
+            "version": ledger.LEDGER_VERSION,
+            "scope": {"github_files": {"provided": False}},
+            "files": [{
+                "path": "src/main/java/Foo.java",
+                "status": "reviewed",
+                "clusters": ["core"],
+                "risk_axes": ["correctness"],
+                "findings": [],
+                "reason": "",
+            }],
+            "findings": [],
+            "passes": review_passes,
+            "review_revision": 1,
+        }
+
+    @staticmethod
+    def create_confirmed_finding(finding_id):
+        return {
+            "id": finding_id,
+            "status": "confirmed",
+            "origin": "current head",
+            "fix_boundary": "shared boundary",
+            "evidence": ["public evidence"],
+            "full_path": ["entry to result"],
+            "counter_evidence": ["strongest counterexample"],
+            "necessity": "required for correctness",
+            "scope_proof": "introduced by reviewed change",
+            "files": ["src/main/java/Foo.java"],
+            "reason": "",
+        }
+
+
+if __name__ == "__main__":
+    unittest.main()