From 99623f78d42e329f8db1b15dc887927f839c654e Mon Sep 17 00:00:00 2001 From: t Date: Sun, 13 Sep 2026 05:44:57 +0530 Subject: [PATCH 01/27] Rectify incomplete merge preflight evidence Carry forward the reviewed implementation from PR #321. The original PR preserves its review and authorship record. Assemble synthetic grouped-number probes without embedding complete identifier-shaped literals in source. --- .github/workflows/ci.yml | 1 + .../compatibility/compatibility-matrix.json | 2 +- .../compatibility/compatibility-surface.json | 4 +- scripts/merge-gate.sh | 1303 +++++++++++++++++ scripts/merge-gate.test.py | 1155 +++++++++++++++ scripts/merge_gate_diff.py | 205 +++ 6 files changed, 2667 insertions(+), 3 deletions(-) create mode 100755 scripts/merge-gate.sh create mode 100644 scripts/merge-gate.test.py create mode 100755 scripts/merge_gate_diff.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b88718ae..801ddfe7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,6 +138,7 @@ jobs: - run: python3 scripts/retain-macos-test-binaries.test.py - run: python3 scripts/bank_statement_import.test.py - run: python3 scripts/sanitise-bbox-capture.test.py + - run: python3 scripts/merge-gate.test.py tally-portable: name: Tally portable core diff --git a/docs/tally/compatibility/compatibility-matrix.json b/docs/tally/compatibility/compatibility-matrix.json index 10986db86..d3374f017 100644 --- a/docs/tally/compatibility/compatibility-matrix.json +++ b/docs/tally/compatibility/compatibility-matrix.json @@ -1,7 +1,7 @@ { "schema_version": 1, "bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e", - "compatibility_surface_sha256": "74ac4427cf03860e7d4a7005682cccaa3d2544366e87f50fbe53ee79584a076b", + "compatibility_surface_sha256": "bef1632064166f9b80ff3beef9d0196b92667e4670d0834cdd183651a19a062a", "claims": [ { "claim_id": "erp9-6-6-3-windows-education-xml-one-company", diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json index ecad92293..c25468c4a 100644 --- a/docs/tally/compatibility/compatibility-surface.json +++ b/docs/tally/compatibility/compatibility-surface.json @@ -3,7 +3,7 @@ "files": [ { "path": ".github/workflows/ci.yml", - "sha256": "2d2220d0dc1942eab04034dbfd07f5ecf693cc7d5f47b1e31615fe17e4b2f140" + "sha256": "f1d96290429f3d7d9821440ad989fc68b41d8ee3c99106d0c93151c95b03b6d1" }, { "path": ".github/workflows/dependency-security.yml", @@ -850,5 +850,5 @@ "sha256": "a8ac2714fecf51947f2822c8c46d7ce2e8602c732780ff60566a7771f0836f9a" } ], - "manifest_sha256": "74ac4427cf03860e7d4a7005682cccaa3d2544366e87f50fbe53ee79584a076b" + "manifest_sha256": "bef1632064166f9b80ff3beef9d0196b92667e4670d0834cdd183651a19a062a" } \ No newline at end of file diff --git a/scripts/merge-gate.sh b/scripts/merge-gate.sh new file mode 100755 index 000000000..4283d904f --- /dev/null +++ b/scripts/merge-gate.sh @@ -0,0 +1,1303 @@ +#!/usr/bin/env bash +# Decide whether a pull request may be merged, and say why not when it may not. +# +# Usage: scripts/merge-gate.sh [--repo OWNER/NAME] +# [--independent-review-sha FULL_SHA] (explicit manual review attestation) +# DSC/credential review record format (review or PR comment by another reviewer): +# Security review: FULL_SHA +# Result: accepted +# Include the reviewed scope and reasoning in that record. +# Exit: 0 may merge, 1 must not, 2 could not determine. +# +# Every positive result is bound to one server-observed head, base tip, complete +# check set, provider review commit, review-thread set, changed-file set, and +# readable compatibility surface. Unknown or incomplete evidence is never +# converted into an empty successful set. + +set -uo pipefail + +PR="" +REPO="" +INDEPENDENT_REVIEW_SHA="" +while [ $# -gt 0 ]; do + case "$1" in + --repo) + REPO="${2:-}" + [ -n "$REPO" ] || { echo "--repo needs OWNER/NAME" >&2; exit 2; } + shift 2 + ;; + --repo=*) + REPO="${1#--repo=}" + [ -n "$REPO" ] || { echo "--repo= needs OWNER/NAME" >&2; exit 2; } + shift + ;; + --independent-review-sha) + INDEPENDENT_REVIEW_SHA="${2:-}" + [ -n "$INDEPENDENT_REVIEW_SHA" ] || { echo "--independent-review-sha needs a full commit SHA" >&2; exit 2; } + shift 2 + ;; + --independent-review-sha=*) + INDEPENDENT_REVIEW_SHA="${1#*=}" + [ -n "$INDEPENDENT_REVIEW_SHA" ] || { echo "--independent-review-sha= needs a full commit SHA" >&2; exit 2; } + shift + ;; + -h|--help) + sed -n '2,17p' "$0" + exit 0 + ;; + -*) + echo "unknown option: $1" >&2 + exit 2 + ;; + *) + if [ -z "$PR" ]; then + PR="$1" + else + echo "unexpected argument: $1" >&2 + exit 2 + fi + shift + ;; + esac +done +[ -n "$PR" ] || { echo "usage: $0 [--repo OWNER/NAME]" >&2; exit 2; } +[[ "$PR" =~ ^[0-9]+$ ]] || { echo "PR selector must be numeric" >&2; exit 2; } + +# Resolve the repository once. An explicit target must never fall back to the +# current checkout if one later API call fails. +if [ -z "$REPO" ]; then + if ! REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null); then + echo "could not determine repository; pass --repo OWNER/NAME" >&2 + exit 2 + fi +fi +OWNER="${REPO%%/*}" +NAME="${REPO##*/}" +if [ -z "$OWNER" ] || [ -z "$NAME" ] || [ "$OWNER" = "$REPO" ] || [[ "$REPO" == */*/* ]] \ + || ! [[ "$OWNER" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] \ + || ! [[ "$NAME" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then + echo "--repo must be OWNER/NAME, got '$REPO'" >&2 + exit 2 +fi + +# This command encodes Bridge-specific master workflow and surface policy. +# An arbitrary repository's passing checks cannot qualify that contract. +if [ "$REPO" != "lamemustafa/bridge" ]; then + echo "unsupported repository: this gate implements lamemustafa/bridge policy" >&2 + exit 2 +fi + +fail=0 +uncertain=0 +tmpdir=$(mktemp -d) +errfile="$tmpdir/error" +trap 'rm -rf "$tmpdir"' EXIT +say() { printf ' %-13s %s\n' "$1" "$2"; } +bad() { say "BLOCK" "$1"; fail=1; } +unknown() { say "INDETERMINATE" "$1"; uncertain=1; } +die() { echo "$1" >&2; exit 2; } + +# A malformed response is different from a valid empty result. Validate the +# outer shape before extracting fields so jq errors cannot become empty values. +# Independent acceptance decides whether a change is a production regression, +# whether its branch is dedicated, and whether it carries the required type:rectify label. +: >"$errfile" +if ! meta=$(gh pr view "$PR" --repo "$REPO" \ + --json headRefOid,baseRefOid,baseRefName,mergeable,mergeStateStatus,isDraft,state,title,body,changedFiles 2>"$errfile"); then + die "could not read PR #$PR in $REPO" +fi +if ! jq -e ' + type == "object" and + (.headRefOid | type == "string" and test("^[0-9a-fA-F]{40}$")) and + (.baseRefOid | type == "string" and test("^[0-9a-fA-F]{40}$")) and + (.baseRefName | type == "string" and length > 0) and + (.mergeable | type == "string") and + (.mergeStateStatus | type == "string") and + (.isDraft | type == "boolean") and + (.state | type == "string") and + (.title | type == "string") and + (.changedFiles | type == "number" and floor == . and . >= 0) +' <<<"$meta" >/dev/null 2>&1; then + die "PR metadata was not a valid complete JSON object" +fi +head=$(jq -r '.headRefOid' <<<"$meta") +base_ref_oid=$(jq -r '.baseRefOid' <<<"$meta") +base=$(jq -r '.baseRefName' <<<"$meta") +mergeable=$(jq -r '.mergeable' <<<"$meta") +mstate=$(jq -r '.mergeStateStatus' <<<"$meta") +draft=$(jq -r '.isDraft' <<<"$meta") +pstate=$(jq -r '.state' <<<"$meta") +changed_files_expected=$(jq -r '.changedFiles' <<<"$meta") +short=${head:0:7} +title=$(jq -r '.title' <<<"$meta") +raw_prbody=$(jq -r '.body // ""' <<<"$meta") +prbody="$raw_prbody" +visible_body_status=0 +prbody=$(python3 -c 'import re, sys +text = sys.stdin.read() +print(re.sub(r"|\Z)", "", text, flags=re.S), end="")' <<<"$prbody") || visible_body_status=$? +if [ "$visible_body_status" -ne 0 ]; then + unknown "could not extract visible PR description content" + prbody="" +fi + +if [ -n "$INDEPENDENT_REVIEW_SHA" ] && ! [[ "$INDEPENDENT_REVIEW_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then + bad "independent review attestation must be a full 40-hex commit SHA" +elif [ -n "$INDEPENDENT_REVIEW_SHA" ] && [ "$INDEPENDENT_REVIEW_SHA" != "$head" ]; then + bad "independent review attestation names a different commit than the PR head" +fi + +echo "PR #$PR ($REPO) head=$short base=$base $mergeable/$mstate" +[ "$pstate" = "OPEN" ] || bad "PR is $pstate, not OPEN" +[ "$draft" = "false" ] || bad "draft" + +case "$mergeable" in + MERGEABLE) say "ok" "no conflicts" ;; + CONFLICTING) bad "conflicts with $base — rebase first" ;; + *) unknown "mergeability is '$mergeable'; re-run when GitHub has determined it" ;; +esac +case "$mstate" in + BEHIND) bad "head is BEHIND $base — its checks and review describe a stale tree; rebase" ;; + DIRTY) bad "merge state DIRTY — conflicts" ;; + UNKNOWN) unknown "merge state UNKNOWN; re-run in a moment" ;; + BLOCKED) bad "merge state BLOCKED — GitHub is refusing this merge" ;; + CLEAN|HAS_HOOKS) say "ok" "merge state $mstate is not stale" ;; + UNSTABLE) bad "merge state UNSTABLE — GitHub has not established a mergeable result" ;; + DRAFT) bad "merge state DRAFT — the PR is not ready for merge" ;; + *) unknown "unrecognised merge state '$mstate'" ;; +esac + +if [ "$base" = "master" ]; then + say "ok" "based on master (CI actually runs)" +else + bad "based on '$base', not master — ci.yml does not fire, so checks here prove nothing" +fi + +# Capture the base tip independently. A PR can retain the same base name while +# the branch advances during this run. +: >"$errfile" +base_tip_status=0 +base_tip=$(gh api "repos/$REPO/branches/$base" --jq '.commit.sha' 2>"$errfile") || base_tip_status=$? +if [ "$base_tip_status" -ne 0 ] || ! [[ "$base_tip" =~ ^[0-9a-fA-F]{40}$ ]]; then + unknown "could not read the full current tip of base '$base'" + base_tip="" +else + say "ok" "captured base tip ${base_tip:0:7}" +fi +if [ -n "$base_tip" ] && [ "$base_ref_oid" != "$base_tip" ]; then + unknown "PR base OID $base_ref_oid differs from the current '$base' tip $base_tip" +fi + +# Bind the reviewed head to the actual base lineage returned by GitHub. The +# compare API is queried with the captured OIDs and must say that base is an +# ancestor of head; mergeability alone does not establish that relationship. +if [ -n "$base_tip" ] && [ "$base_ref_oid" = "$base_tip" ]; then + : >"$errfile" + compare_status=0 + comparison=$(gh api "repos/$REPO/compare/${base_tip}...${head}" 2>"$errfile") || compare_status=$? + if [ "$compare_status" -ne 0 ] || ! jq -e --arg base "$base_tip" ' + type == "object" and + (.status | type == "string" and (. == "ahead" or . == "identical")) and + (.behind_by | type == "number" and floor == . and . == 0) and + (.merge_base_commit | type == "object") and + (.merge_base_commit.sha | type == "string" and test("^[0-9a-fA-F]{40}$") and . == $base) + ' <<<"$comparison" >/dev/null 2>&1; then + unknown "base/head compare did not prove that the captured base tip is an ancestor" + else + say "ok" "compare API binds base tip ${base_tip:0:7} as head's merge base" + fi +fi + +# Branch protection is the source of required check contexts. A pass list with +# an omitted required context is not a complete check result. +: >"$errfile" +protection_status=0 +required_contexts="" +protection=$(gh api "repos/$REPO/branches/$base/protection/required_status_checks" 2>"$errfile") || protection_status=$? +if [ "$protection_status" -ne 0 ]; then + unknown "could not read required status-check contexts for $base" + required_contexts="" +elif ! jq -e ' + type == "object" and + (.strict | type == "boolean" and . == true) and + ((.contexts // []) | type == "array" and all(.[]; type == "string" and length > 0)) and + ((.checks // []) | type == "array" and all(.[]; type == "object" and (.context | type == "string" and length > 0))) +' <<<"$protection" >/dev/null 2>&1; then + unknown "required status-check response was malformed" + required_contexts="" +else + required_contexts=$(jq -r '((.contexts // []) + ([.checks // [] | .[]? | .context] | map(select(type == "string" and length > 0))) | unique)[]' <<<"$protection") + documented_contexts=$'Dependency security\nFrontend build\nGitGuardian Security Checks\nRequired checks\nRust format' + if [ -z "$required_contexts" ]; then + unknown "branch protection returned no required status-check contexts" + elif missing_documented=$(comm -23 <(sort -u <<<"$documented_contexts") <(sort -u <<<"$required_contexts")) && [ -n "$missing_documented" ]; then + bad "branch protection omits $(wc -l <<<"$missing_documented" | tr -d ' ') documented required check context(s)" + elif unexpected_documented=$(comm -13 <(sort -u <<<"$documented_contexts") <(sort -u <<<"$required_contexts")) && [ -n "$unexpected_documented" ]; then + bad "branch protection includes $(wc -l <<<"$unexpected_documented" | tr -d ' ') undocumented required check context(s)" + else + say "ok" "loaded $(wc -l <<<"$required_contexts" | tr -d ' ') required check context(s)" + fi +fi +printf '%s\n' "$required_contexts" >"$tmpdir/required-contexts" + +# gh uses pass/fail/pending/skipping/cancel buckets. Preserve command status, +# then parse JSON and require each protected context individually. +: >"$errfile" +check_status=0 +buckets=$(gh pr checks "$PR" --repo "$REPO" --json bucket,name 2>"$errfile") || check_status=$? +if [ -z "$buckets" ]; then + unknown "checks query returned no JSON" +elif ! jq -e 'type == "array" and all(.[]; type == "object" and (.name | type == "string" and length > 0) and (.bucket | type == "string"))' <<<"$buckets" >/dev/null 2>&1; then + unknown "checks query returned malformed JSON" +else + # The first validation accepts only the documented buckets; keep this second + # check explicit because jq's precedence is easy to misread in a gate. + if ! jq -e 'all(.[]; (.bucket == "pass" or .bucket == "fail" or .bucket == "pending" or .bucket == "skipping" or .bucket == "cancel"))' <<<"$buckets" >/dev/null 2>&1; then + unknown "checks query contained an unknown bucket" + elif [ "$check_status" -ne 0 ] && [ "$(jq '[.[] | select(.bucket == "fail" or .bucket == "cancel" or .bucket == "pending")] | length' <<<"$buckets")" -eq 0 ]; then + unknown "checks command failed even though no failing or pending result was returned" + elif [ "$(jq 'length' <<<"$buckets")" -eq 0 ]; then + bad "no checks reported for this PR" + else + printf '%s' "$buckets" >"$tmpdir/check-buckets.json" + context_report_status=0 + context_report_valid=false + context_report=$(jq --rawfile contexts "$tmpdir/required-contexts" ' + . as $buckets | ($contexts | split("\n") | map(select(length > 0))) | + map(. as $context | [$buckets[] | select(.name == $context)] | + {name: $context, state: (if length == 0 then "missing" + elif all(.[]; .bucket == "pass") then "pass" + else map(.bucket) | unique | join(",") end)}) | + {total: length, failed: ([.[] | select(.state != "pass")] | length), + examples: ([.[] | select(.state != "pass")][0:8] | map( + "required check \u0027" + (.name | gsub("[[:cntrl:]]"; "?") | .[0:80]) + "\u0027 " + + (if .state == "missing" then "was not reported" else "is not passing (" + .state + ")" end)))} + ' <"$tmpdir/check-buckets.json") || context_report_status=$? + if [ "$context_report_status" -ne 0 ] || ! jq -e '. as $report | type == "object" and ($report.total | type == "number" and floor == . and . >= 0) and ($report.failed | type == "number" and floor == . and . >= 0 and . <= $report.total) and ($report.examples | type == "array" and length <= 8 and all(.[]; type == "string"))' <<<"$context_report" >/dev/null 2>&1; then + unknown "could not compute bounded required-check diagnostics" + else + context_report_valid=true + check_bad=$(jq -r '.failed' <<<"$context_report") + context_count=$(jq -r '.total' <<<"$context_report") + if [ "$check_bad" -gt 0 ]; then + bad "$check_bad of $context_count required check contexts are not passing; up to 8 bounded examples: $(jq -r '.examples | join("; ")' <<<"$context_report")" + else + say "ok" "all $context_count required check contexts passed" + fi + fi + all_bad=$(jq '[.[] | select(.bucket == "fail" or .bucket == "cancel" or .bucket == "pending")] | length' <<<"$buckets") + skipped=$(jq '[.[] | select(.bucket == "skipping")] | length' <<<"$buckets") + [ "$all_bad" -eq 0 ] || bad "$all_bad reported check(s) are failing, cancelled, or pending" + [ "$skipped" -eq 0 ] || say "note" "$skipped optional check(s) are skipped; required skipped contexts remain blocking" + [ "$context_report_valid" = true ] && [ "$check_bad" -eq 0 ] && [ "$all_bad" -eq 0 ] && say "ok" "all reported checks concluded successfully" + fi +fi + +# The checks rollup is head-bound by its PR endpoint, but it does not expose a +# check-run SHA in `gh pr checks`. Verify the complete provider check-run pages +# and commit-status response independently so a malformed or mixed response +# cannot become positive evidence. The required-context decision above remains +# authoritative for branch protection, including status-only contexts. +: >"$errfile" +check_runs_status=0 +check_runs=$(gh api --paginate --slurp "repos/$REPO/commits/$head/check-runs?per_page=100" 2>"$errfile") || check_runs_status=$? +if [ "$check_runs_status" -ne 0 ] || ! jq -e --arg head "$head" ' + type == "array" and length > 0 and + all(.[]; type == "object" and + (.total_count | type == "number" and floor == . and . >= 0) and + (.check_runs | type == "array" and all(.[]; + type == "object" and + (.id | type == "number" and floor == . and . >= 0) and + (.name | type == "string" and length > 0) and + (.status == "completed") and + (.conclusion | type == "string" and (. == "success" or . == "skipped" or . == "neutral" or . == "failure" or . == "cancelled" or . == "timed_out" or . == "action_required" or . == "stale")) and + (.head_sha | type == "string" and test("^[0-9a-fA-F]{40}$") and . == $head) + ))) and + ((map(.total_count) | unique | length) == 1) and + ((map(.check_runs | length) | add) == .[0].total_count) and + ((map(.check_runs) | add | map(.id) | unique | length) == .[0].total_count) +' <<<"$check_runs" >/dev/null 2>&1; then + unknown "could not validate complete head-bound check-run evidence" +else + failed_runs_status=0 + failed_runs=$(jq --rawfile contexts "$tmpdir/required-contexts" ' + ($contexts | split("\n")) as $required | + [.[] | .check_runs[] | . as $run | + select((.conclusion != "success" and .conclusion != "neutral" and .conclusion != "skipped") or + (.conclusion != "success" and ($required | index($run.name)) != null))] | length + ' <(printf '%s' "$check_runs")) || failed_runs_status=$? + if [ "$failed_runs_status" -ne 0 ] || ! [[ "$failed_runs" =~ ^[0-9]+$ ]]; then + unknown "could not evaluate refreshed check-run conclusions" + elif [ "$failed_runs" -gt 0 ]; then + bad "$failed_runs refreshed check run(s) are failed or required-but-not-successful" + else + say "ok" "completed check-run pages are successful and bound to head $short" + fi +fi +: >"$errfile" +statuses_status=0 +statuses=$(gh api --paginate --slurp "repos/$REPO/commits/$head/status?per_page=100" 2>"$errfile") || statuses_status=$? +printf '%s' "$statuses" >"$tmpdir/combined-status-pages.json" +if [ "$statuses_status" -ne 0 ] || ! jq -e --arg head "$head" ' + type == "array" and length > 0 and + all(.[]; type == "object" and + (.sha | type == "string" and test("^[0-9a-fA-F]{40}$") and . == $head) and + (.total_count | type == "number" and floor == . and . >= 0) and + (.state | type == "string" and (. == "success" or . == "pending")) and + (.statuses | type == "array" and all(.[]; + type == "object" and + (.id | type == "number" and floor == . and . >= 0) and + (.context | type == "string" and length > 0) and + (.state == "success") + )) + ) and + ((map(.total_count) | unique | length) == 1) and + ((map(.statuses | length) | add) == .[0].total_count) and + ((map(.statuses) | add | map(.id) | unique | length) == .[0].total_count) and + ((map(.statuses) | add | map(.context) | unique | length) == .[0].total_count) and + (if .[0].state == "pending" then .[0].total_count == 0 and (map(.statuses | length) | add) == 0 else all(.[]; .state == "success") end) +' <"$tmpdir/combined-status-pages.json" >/dev/null 2>&1; then + unknown "could not validate complete head-bound commit-status evidence" +else + say "ok" "complete commit-status pages are bound to head $short" +fi + +# Provider review objects carry an immutable full commit_id even when the +# human-readable summary is abbreviated. The summary is required as a separate +# completed-run receipt: an exact-head COMMENTED object can remain after a run +# later fails. A clean summary without a full provider OID has an explicit +# operator-attestation path, but a seven-character row never becomes a full-SHA +# claim by inference. +: >"$errfile" +review_status=0 +provider_review="" +reviews=$(gh api --paginate --slurp "repos/$REPO/pulls/$PR/reviews" 2>"$errfile") || review_status=$? +if [ "$review_status" -ne 0 ]; then + unknown "could not read provider review records" +elif ! jq -e 'type == "array" and (all(.[]; type == "array") or all(.[]; type == "object"))' <<<"$reviews" >/dev/null 2>&1; then + unknown "provider review response was malformed" +else + provider_review=$(jq -r --arg head "$head" ' + (if all(.[]; type == "array") then flatten else . end) | + map(select(.user.login == "chatgpt-codex-connector[bot]" and .user.type == "Bot" and + .state == "COMMENTED" and .commit_id == $head)) | + if length > 0 then "matched" else "" end + ' <<<"$reviews") +fi + +: >"$errfile" +comment_status=0 +comments=$(gh api --paginate --slurp "repos/$REPO/issues/$PR/comments" 2>"$errfile") || comment_status=$? +summary_good=0 +if [ "$comment_status" -ne 0 ]; then + unknown "could not read provider review summaries" +elif ! jq -e 'type == "array" and (all(.[]; type == "array") or all(.[]; type == "object"))' <<<"$comments" >/dev/null 2>&1; then + unknown "provider review-summary response was malformed" +else + summary_rows=$(jq -r ' + (if all(.[]; type == "array") then flatten else . end)[] | + select(.user.login == "chatgpt-codex-connector[bot]" and .user.type == "Bot") | + select((.body // "") | contains("codex-pull-request-review-summary")) | + .body + ' <<<"$comments" | grep -E '^\| (📝|🔍)' | tail -1) + if [ -z "$summary_rows" ]; then + bad "no provider review summary row" + elif ! grep -Fq "\`$short\`" <<<"$summary_rows"; then + bad "latest provider summary names a different head than $short" + elif ! grep -Fq 'Completed' <<<"$summary_rows"; then + bad "provider review run for $short is not completed" + else + summary_good=1 + say "ok" "provider review summary is completed on $short" + fi +fi + +if [ "$provider_review" = "matched" ] && [ "$summary_good" -eq 1 ]; then + say "ok" "provider review records the full current head $short" +elif [ "$provider_review" != "matched" ] && [ "$summary_good" -eq 1 ]; then + if [ -n "$INDEPENDENT_REVIEW_SHA" ] && [ "$INDEPENDENT_REVIEW_SHA" = "$head" ]; then + say "ok" "manual independent review attestation names full current head $short" + elif [ -n "$INDEPENDENT_REVIEW_SHA" ]; then + bad "manual independent review attestation does not match the current head" + else + unknown "provider summary exposes only an abbreviated head; pass --independent-review-sha with an exact manual review attestation" + fi +fi + +# Scan all published PR metadata. Commit messages are paginated because they +# can become squash subjects or release evidence independently of the patch. +: >"$errfile" +metadata_pr_status=0 +metadata_pr=$(gh api "repos/$REPO/pulls/$PR" 2>"$errfile") || metadata_pr_status=$? +if [ "$metadata_pr_status" -ne 0 ] || ! jq -e --arg head "$head" ' + type == "object" and + (.commits | type == "number" and floor == . and . > 0 and . <= 250) and + (.head | type == "object" and + (.sha | type == "string" and test("^[0-9a-fA-F]{40}$") and . == $head)) +' <<<"$metadata_pr" >/dev/null 2>&1; then + unknown "could not prove complete head-bound PR commit metadata for the privacy scan" + privacy_metadata="" +else + metadata_commit_total=$(jq -r '.commits' <<<"$metadata_pr") +fi + +: >"$errfile" +metadata_status=0 +metadata_commits=$(gh api --paginate --slurp "repos/$REPO/pulls/$PR/commits?per_page=100" 2>"$errfile") || metadata_status=$? +if [ -z "${metadata_commit_total:-}" ] || [ "$metadata_status" -ne 0 ] || ! jq -e --argjson expected "$metadata_commit_total" --arg head "$head" ' + type == "array" and (all(.[]; type == "array") or all(.[]; type == "object")) and + ((if all(.[]; type == "array") then flatten else . end) | + length == $expected and + ([.[].sha] | unique | length) == $expected and + any(.[]; .sha == $head) and + all(.[]; type == "object" and + (.sha | type == "string" and test("^[0-9a-fA-F]{40}$")) and + (.commit | type == "object") and + (.commit.message | type == "string") and + (.commit.author | type == "object" and + (.name | type == "string") and (.email | type == "string")) and + (.commit.committer | type == "object" and + (.name | type == "string") and (.email | type == "string")) and + ((.author == null) or (.author | type == "object" and (.login | type == "string"))) and + ((.committer == null) or (.committer | type == "object" and (.login | type == "string"))))) +' <<<"$metadata_commits" >/dev/null 2>&1; then + unknown "could not prove complete head-bound PR commit metadata for the privacy scan" + privacy_metadata="" +else + # These are Git's standard author/committer and linked-account fields. The + # privacy scanner checks identifier/path shapes in their literal values; it + # does not claim that ordinary names or email addresses are private data. + commit_messages=$(jq -r '(if all(.[]; type == "array") then flatten else . end)[] | + [.commit.message, .commit.author.name, .commit.author.email, + .commit.committer.name, .commit.committer.email, + (.author.login? // null), (.committer.login? // null)] | + map(select(. != null))[]' <<<"$metadata_commits") + privacy_metadata="$title +$raw_prbody +$commit_messages" +fi + +# Paginate review threads and count unresolved nodes over every page. +cursor="" +open_threads=0 +total_threads=-1 +fetched_threads=0 +thread_ids="$tmpdir/review-thread-ids" +: >"$thread_ids" +thread_ok=1 +while :; do + : >"$errfile" + page_status=0 + if [ -z "$cursor" ]; then + page=$(gh api graphql -f owner="$OWNER" -f name="$NAME" -F pr="$PR" -f cursor="" -f query=' + query($owner:String!,$name:String!,$pr:Int!,$cursor:String){ + repository(owner:$owner,name:$name){ + pullRequest(number:$pr){ reviewThreads(first:100,after:$cursor){ + totalCount pageInfo{hasNextPage endCursor} nodes{id isResolved} + }} + } + }' 2>"$errfile") || page_status=$? + else + page=$(gh api graphql -f owner="$OWNER" -f name="$NAME" -F pr="$PR" -f cursor="$cursor" -f query=' + query($owner:String!,$name:String!,$pr:Int!,$cursor:String){ + repository(owner:$owner,name:$name){ + pullRequest(number:$pr){ reviewThreads(first:100,after:$cursor){ + totalCount pageInfo{hasNextPage endCursor} nodes{id isResolved} + }} + } + }' 2>"$errfile") || page_status=$? + fi + if [ "$page_status" -ne 0 ] || ! jq -e '.data.repository.pullRequest.reviewThreads | type == "object" and (.totalCount | type == "number" and floor == . and . >= 0) and (.pageInfo.hasNextPage | type == "boolean") and (.nodes | type == "array" and all(.[]; (.id | type == "string" and length > 0) and (.isResolved | type == "boolean")))' <<<"$page" >/dev/null 2>&1; then + unknown "could not read review threads for $REPO#$PR" + thread_ok=0 + break + fi + page_total=$(jq -r '.data.repository.pullRequest.reviewThreads.totalCount' <<<"$page") + if [ "$total_threads" -eq -1 ]; then + total_threads="$page_total" + elif [ "$page_total" -ne "$total_threads" ]; then + unknown "review-thread totalCount changed during pagination" + thread_ok=0 + break + fi + page_nodes=$(jq '.data.repository.pullRequest.reviewThreads.nodes | length' <<<"$page") + jq -r '.data.repository.pullRequest.reviewThreads.nodes[].id' <<<"$page" >>"$thread_ids" + fetched_threads=$((fetched_threads + page_nodes)) + if [ "$fetched_threads" -gt "$total_threads" ]; then + unknown "review-thread pagination exceeded totalCount" + thread_ok=0 + break + fi + page_open=$(jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length' <<<"$page") + open_threads=$((open_threads + page_open)) + has_next=$(jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage' <<<"$page") + [ "$has_next" = "true" ] || break + if [ "$page_nodes" -eq 0 ]; then + unknown "review-thread pagination returned no nodes while claiming another page" + thread_ok=0 + break + fi + next_cursor=$(jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor // empty' <<<"$page") + if [ -z "$next_cursor" ] || [ "$next_cursor" = "$cursor" ]; then + unknown "review-thread pagination returned no advancing cursor" + thread_ok=0 + break + fi + cursor="$next_cursor" +done +if [ "$thread_ok" -eq 1 ]; then + unique_thread_count=$(sort -u "$thread_ids" | wc -l | tr -d ' ') + if [ "$unique_thread_count" -ne "$fetched_threads" ]; then + unknown "review-thread pagination repeated thread IDs" + elif [ "$fetched_threads" -ne "$total_threads" ]; then + unknown "review-thread pagination returned $fetched_threads of $total_threads nodes" + elif [ "$open_threads" -eq 0 ]; then + say "ok" "0 of $total_threads review threads unresolved" + else + bad "$open_threads of $total_threads review threads unresolved" + fi +fi + +# Require a completed checklist item and a same-repository line permalink. +# The repository template puts the permalink on the item's indented +# continuation, so accept it there as well as in an inline Markdown link. +# A filename in prose, a foreign link, or an anchor outside the current +# checklist file is not completion evidence. +read_review_checklist() { + local response content decoded decode_status + : >"$errfile" + response=$(gh api "repos/$REPO/contents/review-checklist.md?ref=$head" 2>"$errfile") || return 1 + content=$(jq -er 'select(.encoding == "base64") | .content | strings' <<<"$response") || return 1 + decode_status=0 + decoded=$(printf '%s' "${content//$'\n'/}" | base64 --decode 2>"$errfile") || decode_status=$? + if [ "$decode_status" -ne 0 ]; then + decode_status=0 + decoded=$(printf '%s' "${content//$'\n'/}" | base64 -D 2>"$errfile") || decode_status=$? + fi + [ "$decode_status" -eq 0 ] && [ -n "$decoded" ] || return 1 + review_checklist="$decoded" +} +checklist_link_ok() { + local body="$1" checklist="$2" line awaiting_permalink=0 link anchor + local checked='^[[:space:]]*-[[:space:]]*\[[xX]\][[:space:]]+' + local permalink="https://github\.com/${OWNER}/${NAME}/blob/${head}/review-checklist\.md#L[0-9]+" + local permalink_boundary="${permalink}"'([[:space:]]|\)|$)' + while IFS= read -r line; do + if printf '%s\n' "$line" | grep -Eq "$checked"; then + if printf '%s\n' "$line" | grep -Eiq "$permalink_boundary"; then + awaiting_permalink=2 + elif printf '%s\n' "$line" | grep -Eiq 'review-checklist\.md'; then + awaiting_permalink=1 + else + awaiting_permalink=0 + fi + elif [ "$awaiting_permalink" -eq 1 ] && printf '%s\n' "$line" | grep -Eq '^[[:space:]]+'; then + if printf '%s\n' "$line" | grep -Eiq "$permalink_boundary"; then + awaiting_permalink=2 + fi + elif [ "$awaiting_permalink" -ne 2 ]; then + awaiting_permalink=0 + fi + if [ "$awaiting_permalink" -eq 2 ]; then + while IFS= read -r link; do + anchor=${link##*#L} + anchor=${anchor%%[^0-9]*} + if sed -n "${anchor}p" <<<"$checklist" | grep -Eq '^[[:space:]]*-[[:space:]]*\[[ xX]\][[:space:]]+[^[:space:]]'; then + return 0 + fi + done < <(printf '%s\n' "$line" | grep -Eio "$permalink_boundary") + awaiting_permalink=0 + fi + done <<<"$body" + return 1 +} +review_checklist="" +checklist_status=0 +read_review_checklist || checklist_status=$? +if [ "$checklist_status" -ne 0 ]; then + unknown "could not read review-checklist content for link validation" +elif ! checklist_link_ok "$prbody" "$review_checklist"; then + bad "description lacks a completed same-repository review-checklist link to an existing line" +else + say "ok" "description links a completed review-checklist item" +fi + +body_section_has_content() { + local body="$1" labels="$2" allow_placeholders="${3:-false}" + awk -v labels="$labels" -v allow_placeholders="$allow_placeholders" ' + function heading(line, lower) { + lower = tolower(line) + sub(/^[[:space:]]*#+[[:space:]]*/, "", lower) + return lower ~ ("^(" labels ")[[:space:]]*:[[:space:]]*[^[:space:]]") || + lower ~ ("^(" labels ")[[:space:]]*:?[[:space:]]*$") + } + function template_prompt(line, lower) { + lower = tolower(line) + return lower == "what concrete user or maintainer workflow changes, and why now?" || + lower ~ /^-[[:space:]]*\[[xX]\][[:space:]]/ || + lower ~ /^-[[:space:]]*exact candidate sha:/ || + lower ~ /^-[[:space:]]*commands and results[[:space:]]*\(.*\):[[:space:]]*$/ || + lower ~ /^-[[:space:]]*captured\/fixture\/live scope and known limitations:[[:space:]]*$/ || + lower ~ /^-[[:space:]]*manual\/ui evidence[[:space:]]*\(.*\):[[:space:]]*$/ + } + function after_colon(line, value) { + value = line + sub(/^[^:]*:[[:space:]]*/, "", value) + return value + } + function meaningful(value, lower) { + gsub(//, "", value) + lower = tolower(value) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", lower) + if (lower == "") return 0 + if (allow_placeholders == "true" && lower ~ /^(n\/a|none|no impact)$/) return 1 + return lower !~ /^(n\/a|none|pending|todo|tbd|not applicable|unaffected|not affected|not impacted|no impact)$/ + } + { + # The canonical PR template uses labelled list fields as well as + # headings. Accept a filled field for the requested policy label, but + # never the untouched label or a checkbox by itself. + lower = tolower($0) + gsub(//, "", lower) + if (pending_list) { + if (lower ~ /^[[:space:]]*$/) { + pending_list = 0 + } else if (lower ~ /^[[:space:]]*[-#]/) { + pending_list = 0 + } else if (lower !~ /^[[:space:]]+/) { + pending_list = 0 + } else if (lower ~ /:[[:space:]]*[^[:space:]]/ && meaningful(after_colon(lower))) { + found = 1 + exit + } else if (lower ~ /:[[:space:]]*$/) { + pending_list = 2 + } else if (pending_list == 2 && lower ~ /[^[:space:]]/) { + if (!template_prompt($0) && lower !~ /^[[:space:]]*", "", sys.stdin.read(), flags=re.S) +active = fenced = False +for line in text.splitlines(): + heading = re.match(r"^\s*#{1,6}\s+(.+?)\s*$", line) + if heading: + active = bool(re.fullmatch(r"(?:test or reproduction command|commands and results|validation and evidence):?", heading[1], re.I)) + fenced = False + continue + if not active: + continue + if re.match(r"^\s*```", line): + fenced = not fenced + continue + candidates = [(line.strip()[2:] if line.strip().startswith("$ ") else line.strip())] if fenced else re.findall(r"`([^`]+)`", line) + for command in candidates: + if re.search(r"\.\.\.|…|<[^>]+>", command): + continue + try: + words = shlex.split(command) + except ValueError: + continue + if words and words[0] == "corepack": + words = words[1:] + if len(words) >= 2 and (words[0] in {"python", "python3", "pytest", "pnpm", "npm", "cargo", "make", "bash", "sh", "gh"} or words[0].startswith("scripts/")): + sys.exit(0) +sys.exit(1) +' <<<"$1" +} +if ! body_has_validation_command "$prbody"; then + bad "description lacks an actual test or reproduction command" +fi + +# P4 is a three-part design record, not a generic scope paragraph. When a +# patch adds implementation code, each question must have an answer that is +# more than the untouched template label. +body_has_p4_answers() { + local body="$1" + body_section_has_content "$body" 'existing component reused' && + body_section_has_content "$body" 'what is deleted' && + body_section_has_content "$body" 'what breaks if this is not built' +} + +body_has_platform_evidence() { + local body="$1" host="$2" + # A bare checked template box has no host, command, or rationale. Require + # a filled heading/list field that names the host and either evidence or a + # justified unaffected statement. + python3 -c ' +import re, sys +host = sys.argv[1].lower() +for line in sys.stdin.read().splitlines(): + lower = line.strip().lower() + lower = re.sub(r"", "", lower).strip() + if host not in lower: + continue + if re.search(r"(validation|evidence|test|check|unaffected|not applicable|not affected|not impact)", lower): + value = lower.split(":", 1)[1].strip() if ":" in lower else "" + placeholders = { + "none", "n/a", "not applicable", "unaffected", "not affected", + "not impacted", "no impact", "pending", "todo", "tbd", + } + if value and value not in placeholders: + raise SystemExit(0) +raise SystemExit(1) +' "$host" <<<"$body" +} + +# Paginate changed files through the REST endpoint; gh pr view hard-codes a +# first:100 GraphQL fragment in some versions. Retain the line counts as well: +# the privacy scan can only be complete when the textual diff describes every +# non-removed destination with the byte count GitHub reported. +: >"$errfile" +files_status=0 +files=$(gh api --paginate --slurp "repos/$REPO/pulls/$PR/files?per_page=100" 2>"$errfile") || files_status=$? +changed_records="$tmpdir/changed-files.tsv" +if [ "$files_status" -ne 0 ] || ! jq -e ' + type == "array" and + (all(.[]; type == "array" and all(.[]; + type == "object" and + ((.filename | type) == "string") and (.filename | length > 0) and (.filename | test("[\u0000-\u001F\u007F]") | not) and + ((.status | type) == "string") and (.status | length > 0) and + (((.previous_filename? == null) or (((.previous_filename | type) == "string") and ((.previous_filename | length) > 0) and ((.previous_filename | test("[\u0000-\u001F\u007F]")) | not))) and ((.status != "renamed") or (((.previous_filename | type) == "string") and ((.previous_filename | length) > 0) and ((.previous_filename | test("[\u0000-\u001F\u007F]")) | not)))) and + ((.additions | type) == "number") and (.additions | floor == . and . >= 0) and + ((.deletions | type) == "number") and (.deletions | floor == . and . >= 0))) or + all(.[]; type == "object" and + ((.filename | type) == "string") and (.filename | length > 0) and (.filename | test("[\u0000-\u001F\u007F]") | not) and + ((.status | type) == "string") and (.status | length > 0) and + (((.previous_filename? == null) or (((.previous_filename | type) == "string") and ((.previous_filename | length) > 0) and ((.previous_filename | test("[\u0000-\u001F\u007F]")) | not))) and ((.status != "renamed") or (((.previous_filename | type) == "string") and ((.previous_filename | length) > 0) and ((.previous_filename | test("[\u0000-\u001F\u007F]")) | not)))) and + ((.additions | type) == "number") and (.additions | floor == . and . >= 0) and + ((.deletions | type) == "number") and (.deletions | floor == . and . >= 0))) +' <<<"$files" >/dev/null 2>&1; then + unknown "could not read the complete changed-file set" + changed="" +else + jq -r '(if all(.[]; type == "array") then flatten else . end)[] | [.filename, .status, .additions, .deletions, (.previous_filename? // "")] | @tsv' <<<"$files" >"$changed_records" + changed=$(cut -f1 "$changed_records") + changed_count=$(jq '(if all(.[]; type == "array") then flatten else . end) | length' <<<"$files") + unique_changed_count=$(jq '(if all(.[]; type == "array") then flatten else . end) | map(.filename) | unique | length' <<<"$files") + if [ "$changed_count" -eq 0 ]; then + unknown "changed-file response contained no filenames" + elif [ "$changed_files_expected" -gt 3000 ]; then + unknown "PR reports $changed_files_expected changed files beyond the REST files API cap" + elif [ "$changed_count" -ne "$changed_files_expected" ] || [ "$unique_changed_count" -ne "$changed_count" ]; then + unknown "changed-file response has $changed_count unique records; PR metadata reports $changed_files_expected" + fi +fi + +# Existing workflow changes require the rollback and migration-compatibility +# notes mandated by the project review flow. The complete REST file set, rather +# than the rendered diff, is the authority for this conditional requirement. +workflow_change=0 +if [ -n "$files_status" ] && [ "$files_status" -eq 0 ]; then + workflow_change=$(jq -r ' + (if all(.[]; type == "array") then flatten else . end) | + any(.[]; (.filename | startswith(".github/workflows/")) or + ((.previous_filename? // "") | startswith(".github/workflows/"))) + ' <<<"$files") +fi +native_frontend_change=0 +if [ "$files_status" -eq 0 ]; then + native_frontend_change=$(jq -r ' + (if all(.[]; type == "array") then flatten else . end) | + any(.[]; [ .filename, (.previous_filename? // "") ][] | + test("^(src-tauri/|src/|scripts/.*\\.(ts|tsx|js|mjs)$)")) + ' <<<"$files") +fi + +# Treat source additions as implementation work only when the REST line totals +# prove that bytes were added. Rename paths are considered for every path +# policy below, so moving platform, migration, or sensitive code cannot evade +# the relevant review record. +implementation_code_added=false +platform_sensitive_change=false +migration_change=false +if [ "$files_status" -eq 0 ]; then + implementation_code_added=$(jq -r ' + (if all(.[]; type == "array") then flatten else . end) | + any(.[]; (.additions > 0) and (.filename | test("\\.(rs|ts|tsx|js|mjs|py|go|java|kt|swift|c|cc|cpp|h|hpp|sh|bash|ps1|psm1|sql)$"))) + ' <<<"$files") + platform_sensitive_change=$(jq -r ' + (if all(.[]; type == "array") then flatten else . end) | + any(.[]; [.filename, (.previous_filename? // "")][] | + test("^(src-tauri/|src/.*\\.(rs|ts|tsx|js|mjs)$)|(^|/)(windows|macos|darwin|win32|local_files|paths)(/|[._-])"; "i")) + ' <<<"$files") + migration_change=$(jq -r ' + (if all(.[]; type == "array") then flatten else . end) | + any(.[]; [.filename, (.previous_filename? // "")][] | + test("(^|/)(migrations?|schema|database|db)(/|[._-])"; "i")) + ' <<<"$files") +fi + +# The review contract requires an explicit security-impact statement whenever +# either spelling of a renamed path touches a DSC, credential, or Tally surface. +# Treat native and frontend source conservatively: their generic filenames can +# carry DSC/Tally behavior. Use the complete REST inventory, including removals +# and prior rename paths; named tooling and documentation surfaces are included. +security_sensitive_change=0 +if [ "$files_status" -eq 0 ]; then + security_sensitive_change=$(jq -r ' + (if all(.[]; type == "array") then flatten else . end) | + any(.[]; [ .filename, (.previous_filename? // "") ][] | + ascii_downcase | test("^src-tauri/(crates|src)/|^src/|^docs/(tally|agent)/|^scripts/(bank_statement_import|sanitise-bbox-capture)|(^|/)[^/]*(dsc|credential|tally)[^/]*(/|$)")) + ' <<<"$files") +fi +if [ "$security_sensitive_change" = "true" ]; then + if ! body_section_has_content "$prbody" 'security impact|security implications' true; then + bad "DSC, Tally, or credential path change lacks non-empty security-impact notes" + else + say "ok" "DSC, Tally, or credential path change includes security-impact notes" + fi + +fi +# DSC/credential changes require a separate security-focused reviewer comment. +# A general approval or the PR author’s impact notes are not that record. +security_reviewer_change=false +if [ "$files_status" -eq 0 ]; then + security_reviewer_change=$(jq ' + (if all(.[]; type == "array") then flatten else . end) | + any(.[]; [.filename, (.previous_filename? // "")][] | + test("(^|[/_.-])(dsc|credential[s]?|certificate[s]?|keystore|secret[s]?)([/_.-]|$)"; "i")) + ' <<<"$files") +fi +if [ "$security_reviewer_change" = "true" ]; then + pr_author=$(jq -er '.user.login | strings | select(length > 0)' <<<"$metadata_pr" 2>/dev/null) || pr_author="" + security_review=false + if [ -n "$pr_author" ] && [ "$review_status" -eq 0 ] && [ "$comment_status" -eq 0 ]; then + printf '%s' "$reviews" >"$tmpdir/reviews.json" + printf '%s' "$comments" >"$tmpdir/comments.json" + security_review=$(jq -n --arg head "$head" --arg author "$pr_author" --slurpfile reviews "$tmpdir/reviews.json" --slurpfile comments "$tmpdir/comments.json" ' + def records: if all(.[]; type == "array") then flatten else . end; + def source_records: if length == 1 then .[0] else . end; + def reviewer: (.user.login | type == "string" and length > 0) and + .user.login != $author and (.user.type == "User" or .user.type == "Bot") and + ((.author_association == "OWNER" or .author_association == "MEMBER" or .author_association == "COLLABORATOR") or + (.user.login == "chatgpt-codex-connector[bot]" and .user.type == "Bot")); + def focused: (.body | type == "string") and + (.body | test("(?im)^#{0,6} *security review: *" + $head + " *$")) and + (.body | test("(?im)^result: *accepted *$")); + ([($reviews | source_records | records)[] | select(reviewer and focused and .commit_id == $head and + (.state == "APPROVED" or .state == "COMMENTED"))] + + [($comments | source_records | records)[] | select(reviewer and focused)]) | length > 0 + ' 2>/dev/null) || security_review=false + fi + if [ "$security_review" != "true" ]; then + unknown "DSC or credential change lacks a separate current-head security-focused reviewer comment" + else + say "ok" "separate security-focused reviewer comment names full current head $short" + fi +fi +if [ "$workflow_change" = "true" ]; then + if ! body_section_has_content "$prbody" 'rollback notes|rollback procedure|migration/sync compatibility and rollback procedure'; then + bad "workflow change lacks non-empty rollback notes" + fi + if ! body_section_has_content "$prbody" 'migration compatibility|migration impact|migration/sync compatibility'; then + bad "workflow change lacks non-empty migration compatibility notes" + fi +fi +if [ "$native_frontend_change" = "true" ] && ! body_section_has_content "$prbody" 'migration compatibility|migration impact|migration/sync compatibility'; then + bad "native or frontend change lacks non-empty migration compatibility notes" +fi +if [ "$migration_change" = "true" ] && ! body_section_has_content "$prbody" 'rollback notes|rollback procedure|migration/sync compatibility and rollback procedure'; then + bad "database migration path lacks non-empty rollback notes" +fi +if [ "$implementation_code_added" = "true" ] && ! body_has_p4_answers "$prbody"; then + bad "implementation code addition lacks all three substantive P4 reuse, deletion, and omission answers" +fi +if [ "$platform_sensitive_change" = "true" ]; then + if ! body_has_platform_evidence "$prbody" windows; then + bad "platform-sensitive change lacks substantive Windows validation evidence or unaffected-host rationale" + fi + if ! body_has_platform_evidence "$prbody" macos; then + bad "platform-sensitive change lacks substantive macOS validation evidence or unaffected-host rationale" + fi +fi + +# Read and validate the v1 surface as a required object. Any transport, +# decoding, JSON, or schema failure is indeterminate; an unrelated nested +# `path` must not turn an incomplete manifest into an empty pin set. Both the +# reviewed head and captured base tip are checked: a head surface that silently +# drops a previously pinned path is a human hold, and changed paths are tested +# against the union so an unpinned head cannot hide a reseal obligation. +SURFACE="docs/tally/compatibility/compatibility-surface.json" +read_surface_paths() { + local ref="$1" + local response content decoded decode_status + surface_paths_result="" + : >"$errfile" + response=$(gh api "repos/$REPO/contents/$SURFACE?ref=$ref" 2>"$errfile") || return 1 + content=$(jq -er 'select(.encoding == "base64") | .content | strings' <<<"$response") || return 1 + decoded="" + decode_status=0 + decoded=$(printf '%s' "${content//$'\n'/}" | base64 --decode 2>"$errfile") || decode_status=$? + if [ "$decode_status" -ne 0 ]; then + decode_status=0 + decoded=$(printf '%s' "${content//$'\n'/}" | base64 -D 2>"$errfile") || decode_status=$? + fi + if [ "$decode_status" -ne 0 ] || ! jq -e ' + type == "object" and + .schema_version == 1 and + ((.manifest_sha256 | type) == "string") and (.manifest_sha256 | test("^[0-9a-f]{64}$")) and + (.files | type == "array" and length > 0 and + all(.[]; type == "object" and + ((.path | type) == "string") and (.path | length > 0) and + ((.sha256 | type) == "string") and (.sha256 | test("^[0-9a-f]{64}$")))) and + (([.files[].path] | length) == ([.files[].path] | unique | length)) + ' <<<"$decoded" >/dev/null 2>&1; then + return 1 + fi + surface_paths_result=$(jq -r '.files[].path' <<<"$decoded") +} + +head_surface_status=0 +read_surface_paths "$head" || head_surface_status=$? +if [ "$head_surface_status" -ne 0 ]; then + unknown "could not read and validate compatibility surface at $short" + pinned="" +else + pinned="$surface_paths_result" + say "ok" "validated v1 compatibility surface at head $short" +fi + +base_surface_status=0 +if [ -n "$base_tip" ]; then + read_surface_paths "$base_tip" || base_surface_status=$? +fi +if [ -n "$base_tip" ] && [ "$base_surface_status" -ne 0 ]; then + unknown "could not read and validate compatibility surface at base ${base_tip:0:7}" + base_pinned="" +elif [ -n "$base_tip" ]; then + base_pinned="$surface_paths_result" + say "ok" "validated v1 compatibility surface at base ${base_tip:0:7}" +else + base_pinned="" +fi + +if [ -n "$changed" ] && [ -n "$pinned" ] && [ -n "$base_pinned" ]; then + removed_pins=$(comm -23 <(sort -u <<<"$base_pinned") <(sort -u <<<"$pinned")) + if [ -n "$removed_pins" ]; then + removed_count=$(wc -l <<<"$removed_pins" | tr -d ' ') + unknown "$removed_count base-pinned path(s) are absent from the head surface; human review is required" + fi + union_pinned=$(printf '%s\n%s\n' "$base_pinned" "$pinned" | sort -u) + touched=$(comm -12 <(sort -u <<<"$union_pinned") <(sort -u <<<"$changed") | awk -v surface="$SURFACE" '$0 != surface') + if [ -z "$touched" ]; then + say "ok" "changed files contain no pinned path requiring a reseal" + elif grep -Fxq "$SURFACE" <<<"$changed"; then + say "ok" "changed pinned paths include the compatibility surface" + else + bad "changed pinned paths omit the compatibility surface reseal" + fi +fi + +# Scan destination paths and added payload lines. Removed/context lines never +# enter the privacy scan. Binary additions are an explicit human-inspection +# hold because their bytes are absent from a textual patch. +: >"$errfile" +diff_status=0 +diff=$(gh pr diff "$PR" --repo "$REPO" 2>"$errfile") || diff_status=$? +if [ "$diff_status" -ne 0 ] || [ -z "$diff" ]; then + unknown "could not read diff for the privacy scan" +else + # Parse Git's C-style quoted paths with the existing Python runtime. The + # shell receives only JSON/TSV data after the parser has matched every diff + # section, so non-ASCII destinations retain their REST filename identity. + diff_stats="$tmpdir/diff-stats.tsv" + added_payload="$tmpdir/added-payload" + parsed_diff_status=0 + parsed_diff=$(python3 scripts/merge_gate_diff.py <<<"$diff") || parsed_diff_status=$? + if [ "$parsed_diff_status" -ne 0 ] || ! jq -e ' + type == "object" and + (.records | type == "array" and all(.[]; type == "object" and + (.destination | type == "string" and length > 0) and + ((.textual_destination == null) or (.textual_destination | type == "string" and length > 0)) and + (.added | type == "number" and floor == . and . >= 0) and + (.deleted | type == "number" and floor == . and . >= 0) and + (.binary | type == "boolean") and (.gitlink | type == "boolean"))) and + (.added_payload | type == "array" and all(.[]; type == "string")) + ' <<<"$parsed_diff" >/dev/null 2>&1; then + unknown "could not parse diff sections for the privacy scan" + : >"$diff_stats" + : >"$added_payload" + else + jq -r '.records[] | [.destination, .added, .deleted, (if .textual_destination == null then 0 else 1 end), (if .binary then 1 else 0 end), (if .gitlink then 1 else 0 end)] | @tsv' <<<"$parsed_diff" >"$diff_stats" + jq -r '.added_payload[]' <<<"$parsed_diff" >"$added_payload" + + + coverage_count=0 + coverage_examples="" + metadata_only_count=0 + metadata_only_examples="" + binary_count=0 + gitlink_count=0 + bounded_coverage_name() { + local item + item=$(sed -E 's#/(Users|home)/[^/[:space:]]+##g; s#[A-Za-z]:[\\/]+Users[\\/]+[^\\/[:space:]]+##g' <<<"$1") + printf '%s' "${item:0:160}" + } + record_coverage_issue() { + coverage_count=$((coverage_count + 1)) + if [ "$coverage_count" -le 8 ]; then + local item + item=$(bounded_coverage_name "$1") + coverage_examples="${coverage_examples}${coverage_examples:+; }$item" + fi + } + record_metadata_only() { + metadata_only_count=$((metadata_only_count + 1)) + if [ "$metadata_only_count" -le 8 ]; then + metadata_only_examples="${metadata_only_examples}${metadata_only_examples:+; }$(bounded_coverage_name "$1")" + fi + } + path_text="" + if [ -s "$changed_records" ]; then + while IFS=$'\t' read -r filename status rest_added rest_deleted previous_filename; do + [ "$status" = "removed" ] && continue + match_count=$(awk -F '\t' -v filename="$filename" '$1 == filename { count++ } END { print count+0 }' "$diff_stats") + if [ "$match_count" -ne 1 ]; then + record_coverage_issue "omits or duplicates '$filename'" + continue + fi + diff_record=$(awk -F '\t' -v filename="$filename" '$1 == filename { print; exit }' "$diff_stats") + IFS=$'\t' read -r _ diff_added diff_deleted textual binary gitlink <<<"$diff_record" + if [ "$gitlink" -eq 1 ]; then + gitlink_count=$((gitlink_count + 1)) + fi + if [ "$binary" -eq 1 ]; then + # Its bytes cannot be reconciled through textual hunks. + binary_count=$((binary_count + 1)) + continue + fi + if [ "$textual" -ne 1 ] && [ "$diff_added" -eq 0 ] && [ "$diff_deleted" -eq 0 ]; then + if [ "$rest_added" -eq 0 ] && [ "$rest_deleted" -eq 0 ]; then + record_metadata_only "$filename" + else + record_coverage_issue "metadata-only '$filename' conflicts with REST line totals" + fi + elif [ "$textual" -ne 1 ]; then + record_coverage_issue "lacks a textual destination for '$filename'" + elif [ "$diff_added" -ne "$rest_added" ] || [ "$diff_deleted" -ne "$rest_deleted" ]; then + record_coverage_issue "line totals for '$filename' differ from REST metadata" + fi + done <"$changed_records" + [ "$binary_count" -eq 0 ] || bad "$binary_count binary addition/change(s) require human privacy inspection" + [ "$gitlink_count" -eq 0 ] || unknown "$gitlink_count gitlink change(s) require explicit provenance, license, and NOTICE review" + if [ "$coverage_count" -gt 0 ]; then + unknown "privacy diff coverage failed for $coverage_count non-removed REST file(s): $coverage_examples" + fi + if [ "$metadata_only_count" -gt 0 ]; then + say "note" "$metadata_only_count metadata-only diff section(s) have REST 0/0 totals: $metadata_only_examples" + fi + # Destination paths are scan input from the complete REST set, not only + # from whatever textual patch GitHub happened to render. Removed paths + # carry no newly added material and are deliberately excluded. + path_text=$(awk -F '\t' '$2 != "removed" { print $1 }' "$changed_records") + fi + added=$(cat "$added_payload") + scan_input="$privacy_metadata +$path_text +$added" + # Diagnostic counts only: never echo matched home paths, which could repeat + # the private value in a merge-gate result. + home_path_status=0 + mac_home='/'"Users"'/[A-Za-z0-9._-]+' + unix_home='/'"home"'/[A-Za-z0-9._-]+' + windows_home='[A-Za-z]:[\\/]{1,2}'"Users"'[\\/]{1,2}[A-Za-z0-9._-]+' + home_path_matches=$(grep -Eio "(^|[^[:alnum:]_])(${mac_home}|${unix_home}|${windows_home})(\$|/|\\\\|[^[:alnum:]_.-])" <<<"$scan_input") || home_path_status=$? + if [ "$home_path_status" -gt 1 ]; then + unknown "developer-home path scan expression failed" + elif [ "$home_path_status" -eq 0 ]; then + home_path_count=$(grep -Ec '.' <<<"$home_path_matches") + bad "privacy scan found $home_path_count developer-home path shape(s)" + fi + redacted=$(sed -E 's/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}//g; s/[0-9a-fA-F]{32,}//g' <<<"$scan_input") + exempt=$(grep -Ec '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[0-9a-fA-F]{32,}' <<<"$scan_input") + [ "$exempt" -eq 0 ] || say "note" "$exempt added/path line(s) carried generated UUID/digest shapes; inspect those lines" + placeholder='^(X+|Z+)[0-9]+(X|Z)?$|^[0-9]{2}(X+|Z+)[0-9]+[0-9A-Z]*$' + if printf '%s\n' 'XXXXX1234X' | grep -qE "$placeholder"; then :; else + probe_status=$? + if [ "$probe_status" -eq 1 ]; then + bad "privacy placeholder control did not match its synthetic probe" + else + unknown "privacy placeholder expression failed" + fi + fi + count_nonplaceholder() { + local pattern="$1" input="$2" matches="" status=0 item count=0 + if matches=$(grep -Eio "$pattern" <<<"$input"); then + : + else + status=$? + if [ "$status" -eq 1 ]; then matches=""; else return 2; fi + fi + while IFS= read -r item; do + [ -n "$item" ] || continue + item=$(tr '[:lower:]' '[:upper:]' <<<"$item") + if printf '%s\n' "$item" | grep -qE "$placeholder"; then + : + else + status=$? + [ "$status" -eq 1 ] && count=$((count + 1)) || return 2 + fi + done <<<"$matches" + printf '%s\n' "$count" + } + # Join separators only inside recognised identifier shapes. A global + # separator-free projection fuses unrelated values and creates false + # identifiers, including adjacent date fragments. Alongside mobile numbers, + # accept only 4-4-4 and 4-4-4-4 grouped long-number forms. + phone_status=0 + normalized_status=0 + normalized_whitespace=$(python3 -c 'import sys, unicodedata; print("".join(" " if unicodedata.category(char) == "Zs" else char for char in sys.stdin.read()), end="")' <<<"$redacted") || normalized_status=$? + if [ "$normalized_status" -ne 0 ]; then + unknown "Unicode whitespace normalization failed" + normalized_whitespace="" + fi + phone_matches=$(grep -Eo '(^|[^[:alnum:]])[6-9]([ ()+._-]{0,3}[0-9]){9}([^[:alnum:]]|$)' <<<"$normalized_whitespace") || phone_status=$? + grouped_number_status=0 + grouped_number_matches=$(grep -Eo '(^|[^[:alnum:]])[0-9]{4}([ ._-])[0-9]{4}\2[0-9]{4}(\2[0-9]{4})?([^[:alnum:]]|$)' <<<"$redacted") || grouped_number_status=$? + if [ "$normalized_status" -ne 0 ] || [ "$phone_status" -gt 1 ] || [ "$grouped_number_status" -gt 1 ]; then + unknown "formatted identifier scan expression failed" + fi + normalized_phone=$(sed -E 's/[^0-9]//g' <<<"$phone_matches") + normalized_grouped_numbers=$(sed -E 's/[^0-9]//g' <<<"$grouped_number_matches") + scan_shapes="$redacted +$normalized_phone +$normalized_grouped_numbers" + hits_status=0 + hits=$(count_nonplaceholder '[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]{3}|[A-Z]{5}[0-9]{4}[A-Z]|[6-9][0-9]{9}' "$scan_shapes") || hits_status=$? + runs_status=0 + runs=$(count_nonplaceholder '[0-9]{11,18}' "$scan_shapes") || runs_status=$? + if [ "$hits_status" -ne 0 ] || [ "$runs_status" -ne 0 ]; then + unknown "privacy scan expression failed" + elif [ "$hits" -eq 0 ] && [ "$runs" -eq 0 ]; then + say "ok" "PR metadata, destination paths, and payload lines carry no identifier shapes" + else + bad "privacy scan found $hits identifier shape(s) and $runs unexplained long digit run(s)" + fi + fi +fi + +# Re-read all moving identities immediately before emitting a merge command. +: >"$errfile" +final_meta_status=0 +final_meta=$(gh pr view "$PR" --repo "$REPO" \ + --json headRefOid,baseRefOid,baseRefName,mergeable,mergeStateStatus,isDraft,state,title,body,changedFiles 2>"$errfile") || final_meta_status=$? +if [ "$final_meta_status" -ne 0 ] || ! jq -e 'type == "object" and (.headRefOid | type == "string" and test("^[0-9a-fA-F]{40}$")) and (.baseRefOid | type == "string" and test("^[0-9a-fA-F]{40}$")) and (.baseRefName | type == "string") and (.mergeable | type == "string") and (.mergeStateStatus | type == "string") and (.isDraft | type == "boolean") and (.state | type == "string") and (.title | type == "string") and (.changedFiles | type == "number" and floor == . and . >= 0)' <<<"$final_meta" >/dev/null 2>&1; then + unknown "could not revalidate PR head and base before merge" +else + final_head=$(jq -r '.headRefOid' <<<"$final_meta") + final_base_ref_oid=$(jq -r '.baseRefOid' <<<"$final_meta") + final_base=$(jq -r '.baseRefName' <<<"$final_meta") + final_mergeable=$(jq -r '.mergeable' <<<"$final_meta") + final_state=$(jq -r '.mergeStateStatus' <<<"$final_meta") + final_draft=$(jq -r '.isDraft' <<<"$final_meta") + final_pstate=$(jq -r '.state' <<<"$final_meta") + final_title=$(jq -r '.title' <<<"$final_meta") + final_changed_files=$(jq -r '.changedFiles' <<<"$final_meta") + [ "$final_head" = "$head" ] || bad "PR head moved during preflight" + [ "$final_base_ref_oid" = "$base_ref_oid" ] || bad "PR base OID moved during preflight" + [ "$final_base" = "$base" ] || bad "PR base moved during preflight" + [ "$final_mergeable" = "MERGEABLE" ] || bad "PR mergeability changed to $final_mergeable during preflight" + [ "$final_draft" = "false" ] || bad "PR became draft during preflight" + [ "$final_pstate" = "OPEN" ] || bad "PR state changed to $final_pstate during preflight" + [ "$final_changed_files" = "$changed_files_expected" ] || bad "PR changed-file count moved during preflight" + case "$final_state" in + CLEAN|HAS_HOOKS) : ;; + BEHIND|DIRTY|UNKNOWN|BLOCKED|UNSTABLE|DRAFT) bad "PR merge state changed to $final_state during preflight" ;; + *) unknown "PR merge state changed to unrecognised value '$final_state' during preflight" ;; + esac + raw_final_body=$(jq -r '.body // ""' <<<"$final_meta") + final_body="$raw_final_body" + final_visible_status=0 + final_body=$(python3 -c 'import re, sys +text = sys.stdin.read() +print(re.sub(r"|\Z)", "", text, flags=re.S), end="")' <<<"$final_body") || final_visible_status=$? + [ "$final_visible_status" -eq 0 ] || unknown "could not extract final visible PR description content" + [ "$final_title" = "$title" ] || bad "PR title changed during preflight" + if ! checklist_link_ok "$final_body" "$review_checklist"; then + bad "PR description changed and no longer carries a completed same-repository line-specific checklist link" + fi + if [ "$raw_final_body" != "$raw_prbody" ]; then + bad "PR description changed during preflight; re-run metadata privacy scan" + if ! body_section_has_content "$final_body" 'functional summary|outcome and reason'; then + bad "PR description changed and no longer carries a non-empty functional summary" + fi + if ! body_section_has_content "$final_body" 'test or reproduction command|commands and results|validation and evidence'; then + bad "PR description changed and no longer carries test or reproduction evidence" + fi + if ! body_has_validation_command "$final_body"; then + bad "PR description changed and no longer carries an actual test or reproduction command" + fi + fi +fi +if [ -n "$base_tip" ]; then + : >"$errfile" + final_base_tip_status=0 + final_base_tip=$(gh api "repos/$REPO/branches/$base" --jq '.commit.sha' 2>"$errfile") || final_base_tip_status=$? + if [ "$final_base_tip_status" -ne 0 ] || ! [[ "$final_base_tip" =~ ^[0-9a-fA-F]{40}$ ]]; then + unknown "could not revalidate base tip before merge" + elif [ "$final_base_tip" != "$base_tip" ]; then + bad "base tip moved during preflight" + fi +fi + +echo +if [ "$fail" -ne 0 ]; then + echo "MUST NOT MERGE" + exit 1 +fi +if [ "$uncertain" -ne 0 ]; then + echo "INDETERMINATE — do not merge until the missing evidence is obtained" + exit 2 +fi + +echo "MAY MERGE — bind the merge to the reviewed head and validated base:" +printf ' [ "$(gh pr view %s --repo %s --json baseRefName -q .baseRefName)" = "%s" ] \\\n && gh pr merge %s --repo %s --squash --match-head-commit %s\n' \ + "$PR" "$REPO" "$base" "$PR" "$REPO" "$head" +exit 0 diff --git a/scripts/merge-gate.test.py b/scripts/merge-gate.test.py new file mode 100644 index 000000000..a9b79d6a9 --- /dev/null +++ b/scripts/merge-gate.test.py @@ -0,0 +1,1155 @@ +#!/usr/bin/env python3 +"""Focused offline controls for scripts/merge-gate.sh. + +The fake gh command models server responses, including paginated REST and +GraphQL pages. No network or merge operation is used. +""" +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SCRIPT = ROOT / "scripts" / "merge-gate.sh" +HEAD = "0123456789abcdef0123456789abcdef01234567" +FAKE_GH = r'''#!/usr/bin/env python3 +import base64, json, os, sys +args = sys.argv[1:] +scenario = os.environ.get("GATE_SCENARIO", "pass") +security_case = scenario.startswith("security-review-") +sync_case = scenario.startswith("sync-") +head = "0123456789abcdef0123456789abcdef01234567" +new_head = "fedcba9876543210fedcba9876543210fedcba98" +base = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + +def emit(value): + if value is not None: + print(value if isinstance(value, str) else json.dumps(value)) + +def fail(message="controlled API failure"): + print(message, file=sys.stderr) + raise SystemExit(1) + +if args[:2] == ["pr", "view"]: + counter_path = os.environ.get("GATE_COUNTER") + view_count = 0 + if counter_path: + try: + view_count = int(open(counter_path).read()) + except (FileNotFoundError, ValueError): + pass + with open(counter_path, "w") as counter: + counter.write(str(view_count + 1)) + selected_head = new_head if scenario == "head-moves" and view_count > 0 else head + final_state = "UNKNOWN_VALUE" if scenario == "final-unrecognized" and view_count > 0 else ("BLOCKED" if scenario == "blocked-state" else ("DRAFT" if scenario in {"draft-state", "draft-surface-fail"} else "CLEAN")) + title = "Safe merge gate control" + if scenario == "metadata-title-id": + title = "Customer " + "ABCDE" + "1234" + "F" + body = ( + "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" + "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" + "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" + ) + if scenario == "body-loses-evidence" and view_count > 0: + body = ( + "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" + "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" + ) + elif scenario == "body-loses-functional" and view_count > 0: + body = ( + "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" + "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" + ) + if scenario == "checklist-foreign": + body = "- [x] [Errors](https://github.com/other/repo/blob/HEAD/review-checklist.md#L10)" + elif scenario == "checklist-unlinked": + body = "- [x] review-checklist.md line 10" + elif scenario == "checklist-template-continuation": + body = ( + "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" + "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" + "- [x] One completed [`review-checklist.md`](../review-checklist.md) line is\n" + " linked here: https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10" + ) + elif scenario == "missing-functional-summary": + body = ( + "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" + "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" + ) + elif scenario == "template-functional-prompt": + body = ( + "## Outcome and reason\n\nWhat concrete user or maintainer workflow changes, and why now?\n\n" + "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" + "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" + ) + elif scenario in {"template-validation-prompt", "template-validation-sha-only"}: + sha = "deadbeef" if scenario == "template-validation-sha-only" else "" + body = ( + "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" + f"## Validation and evidence\n\n- Exact candidate SHA: {sha}\n" + "- Commands and results (`corepack pnpm ...`, `cargo ...`, or reproduction):\n\n" + "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" + ) + elif scenario == "template-validation-empty": + body = ( + "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" + "## Validation and evidence\n\nCommands and results are recorded elsewhere.\n\n" + "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" + ) + elif scenario in {"workflow-notes-present", "workflow-delete-notes", "workflow-rename-out-notes"}: + body = ( + "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" + "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" + "## Rollback notes\n\nRevert the workflow commit.\n\n" + "## Migration compatibility\n\nNo persisted data changes.\n\n" + "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" + ) + elif scenario in {"security-notes-present", "security-none", "security-pending", "surface-unpins"}: + security_impact = "None" if scenario == "security-none" else ("pending" if scenario == "security-pending" else "No credential material is added; the Tally path change is reviewed.") + body = ( + "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" + "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" + f"## Security impact\n\n{security_impact}\n\n" + "## Migration compatibility\n\nExisting callers retain their paths and formats.\n\n" + "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" + ) + elif scenario == "checklist-missing-anchor": + body = ( + "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" + "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" + "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L999999)" + ) + elif scenario == "missing-test-summary": + body = ( + "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" + "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" + ) + if security_case or sync_case: + body += "\n## Security impact\n\nCredential admission remains enforced.\n" + if not scenario.endswith("migration-missing"): + body += "\n## Migration compatibility\n\nNo persisted format change.\n" + if scenario == "validation-command-outside": + body = body.replace("`python3 scripts/merge-gate.test.py`", "Tests not run") + "\n## Rollback\n`cargo test`\n" + if scenario == "policy-multiline-comment": + body = body.replace("A bounded merge preflight keeps incomplete evidence from becoming a merge.", "") + if scenario == "validation-html-comment": + body = body.replace("`python3 scripts/merge-gate.test.py`", "Tests not run ") + if scenario == "unterminated-html-comment": + body = body.replace("`python3 scripts/merge-gate.test.py`", "" + if scenario == "body-comment-drift" and view_count > 0: + body += "\n" + if scenario == "validation-tool-prose": + body = body.replace("`python3 scripts/merge-gate.test.py`", "Tests not run; cargo is available") + if scenario == "validation-placeholder-command": + body = body.replace("`python3 scripts/merge-gate.test.py`", "`cargo ...`") + if scenario == "checklist-heading": + body = body.replace("#L10", "#L1") + if scenario == "checklist-anchor-suffix": + body = body.replace("#L10", "#L10junk") + if scenario in {"implementation-p4-present", "platform-evidence-present", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "migration-rollback-present", "migration-template-wrapped", "security-notes-present", "security-none", "security-pending", "security-review-valid"}: + body += ( + "\n## Scope, reuse, and impact\n\n" + "- Existing component reused: the existing gate parser and file inventory.\n" + "- What is deleted (or why no deletion is justified): no duplicate path remains.\n" + "- What breaks if this is not built: unsafe evidence could reach a merge.\n" + ) + if security_case or scenario in {"surface-unpins", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"}: + body += ( + "\n## Scope, reuse, and impact\n\n" + "- Existing component reused: the existing gate parser and file inventory.\n" + "- What is deleted (or why no deletion is justified): no duplicate path remains.\n" + "- What breaks if this is not built: unsafe evidence could reach a merge.\n" + "\n## Security impact\n\nNo credential material is added.\n" + "\n## Migration compatibility\n\nExisting callers retain their paths and formats.\n" + "\n- Windows validation evidence: Windows CI ran `python3 scripts/merge-gate.test.py`.\n" + "- macOS validation evidence: macOS CI ran `python3 scripts/merge-gate.test.py`.\n" + ) + if scenario in {"platform-evidence-present", "migration-template-wrapped", "security-notes-present", "security-none", "security-pending", "security-review-valid", "sync-migration-present"}: + body += ( + "\n- Windows validation evidence: Windows CI ran `python3 scripts/merge-gate.test.py`.\n" + "- macOS validation evidence: macOS CI ran `python3 scripts/merge-gate.test.py`.\n" + ) + if scenario == "platform-checkbox-evidence": + body += ( + "\n- [x] Native Windows validation completed: `python3 scripts/merge-gate.test.py` passed on Windows CI.\n" + "- [x] Native macOS validation completed: `python3 scripts/merge-gate.test.py` passed on macOS CI.\n" + ) + if scenario == "platform-checkbox-comment": + body += ( + "\n- [x] Native Windows validation completed: \n" + "- [x] Native macOS validation completed: \n" + ) + if scenario == "platform-unaffected-bare": + body += "\n- Windows validation evidence: unaffected\n- macOS validation evidence: unaffected\n" + if scenario == "platform-unaffected-rationale": + body += "\n- Windows validation evidence: unaffected because this changes shared documentation only.\n- macOS validation evidence: unaffected because this changes shared documentation only.\n" + if scenario in {"platform-evidence-present", "platform-checkbox-evidence", "platform-unaffected-bare", "platform-unaffected-rationale"}: + body += ( + "\n## Security impact\n\nNo credential material is added.\n" + "\n## Migration compatibility\n\nExisting callers retain their paths and formats.\n" + ) + if scenario == "migration-rollback-present": + body += "\n## Rollback notes\n\nRevert the migration commit before deployment.\n" + if scenario == "migration-template-wrapped": + body += ( + "\n- Migration/sync compatibility and rollback procedure (required when an\n" + " existing workflow changes): Existing readers retain the old format; revert this commit before deployment.\n" + ) + if scenario == "migration-template-other-field": + body += ( + "\n- Migration/sync compatibility and rollback procedure (required when an\n" + " existing workflow changes):\n" + "- Destructive database migration: No\n" + ) + if scenario == "p4-placeholders": + body += ( + "\n## Scope, reuse, and impact\n\n" + "- Existing component reused: N/A\n" + "- What is deleted (or why no deletion is justified): TODO\n" + "- What breaks if this is not built: pending\n" + ) + if scenario == "workflow-placeholders": + body += "\n## Rollback notes\n\nN/A\n\n## Migration compatibility\n\nTBD\n" + body = body.replace("blob/HEAD", f"blob/{head}") + if scenario == "checklist-stale-ref": + body = body.replace(f"blob/{head}", "blob/" + "f" * 40) + one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "path-id", "binary-delete", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case + selected_base = new_head if scenario == "base-oid-mismatch" else base + emit({"headRefOid": selected_head, "baseRefOid": selected_base, "baseRefName": "master", + "mergeable": "MERGEABLE", "mergeStateStatus": final_state, + "isDraft": False, "state": "OPEN", "title": title, + "body": body, "changedFiles": 1 if one_file else 2}) +elif args[:2] == ["pr", "checks"]: + if scenario == "checks-silent": + raise SystemExit(0) + if scenario == "cancel-check": + emit([{"bucket": "cancel", "name": "Required checks"}]) + elif scenario == "required-skip": + emit([{"bucket": "skipping", "name": "Required checks"}, {"bucket": "pass", "name": "Rust format"}]) + elif scenario == "missing-required": + emit([{"bucket": "pass", "name": "Required checks"}]) + else: + emit([{"bucket": "pass", "name": "Frontend build"}, + {"bucket": "pass", "name": "Rust format"}, + {"bucket": "pass", "name": "GitGuardian Security Checks"}, + {"bucket": "pass", "name": "Dependency security"}, + {"bucket": "pass", "name": "Required checks"}, + {"bucket": "skipping", "name": "Optional documentation"}]) +elif args[:2] == ["pr", "diff"]: + if security_case: + emit("diff --git a/src-tauri/src/dsc.rs b/src-tauri/src/dsc.rs\n--- a/src-tauri/src/dsc.rs\n+++ b/src-tauri/src/dsc.rs\n@@ -0,0 +1 @@\n+safe check\n") + elif sync_case: + emit("diff --git a/src-tauri/src/sync.rs b/docs/retired.rs\nsimilarity index 100%\nrename from src-tauri/src/sync.rs\nrename to docs/retired.rs\n") + elif scenario == "formatted-phone": + phone = "+91 " + "98765" + "-43210" + emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+Call {phone}\n") + elif scenario == "path-id": + path_id = "ABCDE" + "1234" + "F" + emit(f"diff --git a/docs/safe.md b/docs/{path_id}.md\n--- a/docs/safe.md\n+++ b/docs/{path_id}.md\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario in {"security-notes-missing", "security-notes-present", "security-none", "security-pending"}: + emit("diff --git a/src-tauri/src/tally/runtime.rs b/src-tauri/src/tally/runtime.rs\n--- a/src-tauri/src/tally/runtime.rs\n+++ b/src-tauri/src/tally/runtime.rs\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario in {"security-crate", "security-agent-import", "security-dsc"}: + paths = {"security-crate": "src-tauri/crates/bridge-tally-core/src/master_binding.rs", "security-agent-import": "src-tauri/src/agent_import.rs", "security-dsc": "src-tauri/src/dsc.rs"} + emit(f"diff --git a/{paths[scenario]} b/{paths[scenario]}\n--- a/{paths[scenario]}\n+++ /dev/null\n@@ -1 +0,0 @@\n-safe text\n") + elif scenario == "security-rename-out": + emit("diff --git a/src-tauri/src/tally/runtime.rs b/src/runtime.rs\nsimilarity index 100%\nrename from src-tauri/src/tally/runtime.rs\nrename to src/runtime.rs\n") + elif scenario.startswith("home-"): + homes = {"home-macos": "/" + "Users" + "/" + "tester" + "/work", "home-unix": "/" + "home" + "/" + "tester" + "/work", "home-windows": "C:" + "\\" + "Users" + "\\" + "tester" + "\\work", "home-macos-root": "/" + "Users" + "/" + "tester", "home-unix-root": "/" + "home" + "/" + "tester", "home-windows-forward": "C:" + "/" + "Users" + "/" + "tester" + "/work", "home-windows-escaped": "C:" + "\\\\" + "Users" + "\\\\" + "tester" + "\\\\work", "home-regex-source": "mac_home=" + "'/'" + '"Users"' + "'/[A-Za-z0-9._-]+'"} + emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{homes[scenario]}\n") + elif scenario == "binary-delete": + emit("diff --git a/docs/old.png b/docs/old.png\nBinary files a/docs/old.png and /dev/null differ\n") + elif scenario == "diff-omits-file": + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario == "diff-truncated-payload": + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +2 @@\n+first line\n") + elif scenario == "formatted-phone-grouped": + phone = "6" + "98 765-4321" + emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {phone}\n") + elif scenario == "repeated-phone": + phone = "6" + "6" * 9 + emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {phone}\n") + elif scenario in {"grouped-identifier-12", "grouped-identifier-16"}: + identifier = (" ".join(("8421", "7654", "9012")) if scenario.endswith("12") + else "-".join(("8421", "7654", "9012", "3456"))) + emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {identifier}\n") + elif scenario in {"workflow-notes-missing", "workflow-notes-present"}: + emit("diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml\n--- a/.github/workflows/ci.yml\n+++ b/.github/workflows/ci.yml\n@@ -0,0 +1 @@\n+safe workflow text\n") + elif scenario in {"workflow-delete", "workflow-delete-notes"}: + emit("diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml\n--- a/.github/workflows/ci.yml\n+++ /dev/null\n@@ -1 +0,0 @@\n-safe workflow text\n") + elif scenario in {"workflow-rename-out", "workflow-rename-out-notes"}: + emit("diff --git a/.github/workflows/ci.yml b/docs/retired-ci.yml\nsimilarity index 100%\nrename from .github/workflows/ci.yml\nrename to docs/retired-ci.yml\n") + elif scenario in {"phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized"}: + phones = { + "phone-space": "6" + "9876 54321", + "phone-dot": "6" + "987.654.321", + "phone-plus": "6" + "9876+54321", + "phone-underscore": "6" + "987_654_321", + "phone-parenthesized": "(" + "69876" + ") 54321", + } + emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {phones[scenario]}\n") + elif scenario == "unicode-phone": + emit("diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic 69876\u00a054321\n") + elif scenario == "unicode-phone-two-lines": + emit("diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +2 @@\n+69876\n+54321\n") + elif scenario == "metadata-only": + emit("diff --git a/docs/example.md b/docs/example.md\nsimilarity index 100%\nrename from docs/example.md\nrename to docs/example.md\n") + elif scenario == "metadata-private": + private_path = "docs/" + "/" + "Users" + "/tester/" + "x" * 300 + emit(f"diff --git a/{private_path} b/{private_path}\nsimilarity index 100%\nrename from {private_path}\nrename to {private_path}\n") + elif scenario == "metadata-incomplete": + emit("diff --git a/docs/example.md b/docs/example.md\nsimilarity index 100%\nrename from docs/example.md\nrename to docs/example.md\n") + elif scenario == "hunk-header-phone": + phone = "6" + "9876" + "54321" + emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+++ b/synthetic {phone}\n") + elif scenario == "hunk-header-literals": + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +2 @@\n+++ b/safe\n+++ /dev/null\n") + elif scenario == "hunk-binary-literal": + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+GIT binary patch\n") + elif scenario == "all-a-pan": + identifier = "AAAAA" + "1234" + "A" + emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{identifier}\n") + elif scenario == "masked-pan": + identifier = "XXXXX" + "1234" + "X" + emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{identifier}\n") + elif scenario == "quoted-path": + emit('diff --git "a/docs/caf\\303\\251.md" "b/docs/caf\\303\\251.md"\n--- "a/docs/caf\\303\\251.md"\n+++ "b/docs/caf\\303\\251.md"\n@@ -0,0 +1 @@\n+safe text\n') + elif scenario == "crlf-diff": + emit("diff --git a/docs/example.md b/docs/example.md\r\n--- a/docs/example.md\r\n+++ b/docs/example.md\r\n@@ -0,0 +1 @@\r\n+safe text\r\n") + elif scenario == "ambiguous-unquoted-path": + emit("diff --git a/docs/a b/example.md b/docs/a b/example.md\n--- a/docs/a b/example.md\n+++ b/docs/a b/example.md\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario == "ambiguous-rename-path": + emit("diff --git a/docs/a b/example.md b/docs/a b/example.md\nsimilarity index 100%\nrename from docs/a b/example.md\nrename to docs/a b/example.md\n") + elif scenario == "gitlink": + emit("diff --git a/vendor/module b/vendor/module\nnew file mode 160000\nindex 0000000..2222222\n--- /dev/null\n+++ b/vendor/module\n@@ -0,0 +1 @@\n+Subproject commit 2222222\n") + elif scenario in {"implementation-p4-missing", "implementation-p4-present", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders"}: + suffix = {"implementation-p4-shell": "sh", "implementation-p4-powershell": "ps1", "implementation-p4-sql": "sql"}.get(scenario, "py") + emit(f"diff --git a/scripts/example.{suffix} b/scripts/example.{suffix}\n--- a/scripts/example.{suffix}\n+++ b/scripts/example.{suffix}\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale"}: + emit("diff --git a/src-tauri/src/local_files/paths.rs b/src-tauri/src/local_files/paths.rs\n--- a/src-tauri/src/local_files/paths.rs\n+++ b/src-tauri/src/local_files/paths.rs\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario in {"migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"}: + emit("diff --git a/src-tauri/migrations/001.sql b/src-tauri/migrations/001.sql\n--- a/src-tauri/migrations/001.sql\n+++ b/src-tauri/migrations/001.sql\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario == "separated-dates": + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+2026-09-12 2026-09-13\n") + elif scenario == "separated-dates-new-year": + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+0101-2026 0201-2026\n") + elif scenario == "separated-dates-year-month": + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+2025-09-11 2025-09-12\n") + elif scenario == "adr-identifier": + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+CE_ADR_0016_E\n") + elif scenario == "surface-unpins": + emit("diff --git a/src/example.rs b/src/example.rs\n--- a/src/example.rs\n+++ b/src/example.rs\n@@ -0,0 +1 @@\n+safe text\n" + "diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json\n" + "--- a/docs/tally/compatibility/compatibility-surface.json\n+++ b/docs/tally/compatibility/compatibility-surface.json\n@@ -0,0 +1 @@\n+safe manifest\n") + else: + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+safe text\ndiff --git a/docs/second.md b/docs/second.md\n--- a/docs/second.md\n+++ b/docs/second.md\n@@ -0,0 +1 @@\n+other text\n") +elif args and args[0] == "api": + joined = " ".join(args) + if "graphql" in args: + def thread_nodes(start, count, unresolved=False): + return [{"id": f"thread-{index}", "isResolved": not (unresolved and index == start)} + for index in range(start, start + count)] + has_cursor = "C1" in joined + if scenario == "threads-empty-more": + nodes, page_info = [], {"hasNextPage": True, "endCursor": "C1"} + elif scenario == "threads-short": + nodes, page_info = thread_nodes(1, 1), {"hasNextPage": False, "endCursor": None} + elif scenario == "threads-malformed-pagination": + nodes, page_info = thread_nodes(1, 100), {"hasNextPage": True, "endCursor": None} + elif has_cursor: + if scenario == "threads-duplicate-id": + nodes = [{"id": "thread-1", "isResolved": True}] + else: + nodes = thread_nodes(101, 1, scenario == "threads-unresolved-second") + page_info = {"hasNextPage": False, "endCursor": None} + else: + nodes, page_info = thread_nodes(1, 100), {"hasNextPage": True, "endCursor": "C1"} + emit({"data": {"repository": {"pullRequest": {"reviewThreads": { + "totalCount": 101.5 if scenario == "threads-fractional" else (102 if scenario == "threads-total-drift" and has_cursor else 101), + "pageInfo": page_info, "nodes": nodes + }}}}}) + elif "/compare/" in joined: + if scenario == "lineage-mismatch": + emit({"status": "diverged", "behind_by": 1, + "merge_base_commit": {"sha": new_head}}) + else: + emit({"status": "ahead", "behind_by": 0, + "merge_base_commit": {"sha": base}}) + elif "/commits/" in joined and "/check-runs" in joined: + if scenario == "check-run-wrong-head": + run_head = new_head + else: + run_head = head + total_count = 3 if scenario == "check-run-count-mismatch" else 2 + second_id = 1 if scenario == "check-run-duplicate-id" else 2 + run_status = "queued" if scenario == "check-run-incomplete" else "completed" + conclusion = "failure" if scenario == "check-run-failed" else ("skipped" if scenario in {"check-run-required-skip", "check-run-optional-skip"} else "success") + run_name = "Optional changed after rollup" if scenario in {"check-run-failed", "check-run-optional-skip"} else "Required checks" + emit([{"total_count": total_count, "check_runs": [ + {"id": 1, "name": run_name, "head_sha": run_head, "status": run_status, "conclusion": conclusion}, + {"id": second_id, "name": "Rust format", "head_sha": run_head, "status": "completed", "conclusion": "success"}]}]) + elif "/commits/" in joined and "/status?" in joined: + def status_page(rows, total_count, state="success", page_head=head): + return {"sha": page_head, "state": state, "total_count": total_count, "statuses": rows} + if scenario == "status-two-page": + emit([status_page([{"id": 1, "context": "legacy one", "state": "success"}], 2), + status_page([{"id": 2, "context": "legacy two", "state": "success"}], 2)]) + elif scenario == "status-truncated": + emit([{"sha": head, "state": "success", "total_count": 1, "statuses": []}]) + elif scenario == "status-duplicate": + row = {"id": 1, "context": "legacy one", "state": "success"} + emit([{"sha": head, "state": "success", "total_count": 2, "statuses": [row]}, + {"sha": head, "state": "success", "total_count": 2, "statuses": [row]}]) + elif scenario == "status-mixed-head": + emit([{"sha": head, "state": "success", "total_count": 2, "statuses": [{"id": 1, "context": "legacy one", "state": "success"}]}, + {"sha": new_head, "state": "success", "total_count": 2, "statuses": [{"id": 2, "context": "legacy two", "state": "success"}]}]) + elif scenario == "status-failed-context": + emit([{"sha": head, "state": "failure", "total_count": 1, "statuses": [{"id": 1, "context": "legacy optional", "state": "failure"}]}]) + elif scenario == "status-nonempty-pending": + emit([{"sha": head, "state": "pending", "total_count": 1, "statuses": [{"id": 1, "context": "queued", "state": "pending"}]}]) + elif scenario == "status-empty-pending": + emit([{"sha": head, "state": "pending", "total_count": 0, "statuses": []}]) + elif scenario == "status-malformed": + emit([{"sha": head, "state": "success", "total_count": "0", "statuses": []}]) + elif scenario == "status-failed-combined": + emit([{"sha": head, "state": "failure", "total_count": 0, "statuses": []}]) + else: + emit([{"sha": head, "state": "success", "total_count": 0, "statuses": []}]) + elif "/pulls/321/commits" in joined: + message = "safe commit metadata" + if scenario == "metadata-commit-id": + message = "Customer " + "ABCDE" + "1234" + "F" + identity = {"name": "Maintainer", "email": "maintainer@example.invalid"} + if scenario == "metadata-author-id": + identity = {"name": "ABCDE" + "1234" + "F", "email": "maintainer@example.invalid"} + linked_author = None if scenario == "metadata-unlinked-identities" else {"login": "author"} + linked_committer = None if scenario == "metadata-unlinked-identities" else {"login": "committer"} + commits = [{"sha": head, "commit": {"message": message, "author": identity, "committer": identity}, + "author": linked_author, "committer": linked_committer}] + if scenario == "metadata-duplicate": + commits.append({"sha": head, "commit": {"message": message}}) + emit([commits]) + elif any(arg.endswith("/pulls/321") for arg in args): + commits = 1 + metadata_head = head + if scenario == "metadata-capped": + commits = 251 + elif scenario in {"metadata-truncated", "metadata-duplicate"}: + commits = 2 + elif scenario == "metadata-head-mismatch": + metadata_head = new_head + emit({"commits": commits, "head": {"sha": metadata_head}, "user": {"login": "author"}}) + elif "branches/master/protection/required_status_checks" in joined: + contexts = [ + "Frontend build", "Rust format", "GitGuardian Security Checks", + "Dependency security", "Required checks"] + if scenario == "protection-missing-gitguardian": + contexts = [context for context in contexts if context != "GitGuardian Security Checks"] + if scenario == "required-context-output-bound": + contexts += ["control-" + str(i) + "-" + "z" * 10000 for i in range(50)] + if scenario == "required-context-unexpected": + contexts += ["Unexpected required context"] + emit({"strict": scenario != "protection-nonstrict", "contexts": contexts, "checks": []}) + elif "branches/master" in joined: + emit(base) + elif "/pulls/321/reviews" in joined: + if scenario in {"short-review", "summary-only", "manual-summary"}: + emit([[]]) + else: + records = [{"user": {"login": "chatgpt-codex-connector[bot]", "type": "Bot"}, "state": "COMMENTED", "commit_id": head}] + if security_case and scenario != "security-review-missing": + record = {"user": {"login": "reviewer", "type": "User"}, "author_association": "COLLABORATOR", "state": "COMMENTED", "commit_id": head, "body": f"Security review: {head}\nResult: accepted\nReviewed credential handling and error redaction."} + if scenario == "security-review-stale": record["commit_id"] = new_head + if scenario == "security-review-author": record["user"]["login"] = "author" + if scenario == "security-review-unrelated": record["body"] = "Looks good" + if scenario == "security-review-outsider": record["author_association"] = "NONE" + if scenario == "security-review-dismissed": record["state"] = "DISMISSED" + records.append(record) + emit([records]) + elif "/issues/321/comments" in joined: + if scenario == "short-review": + summary = f"codex-pull-request-review-summary\n| 📝 | ✅ **Completed** | `{head[:7]}` |" + emit([[{"user": {"login": "chatgpt-codex-connector[bot]", "type": "Bot"}, "body": summary}]]) + elif scenario == "summary-failed": + summary = f"codex-pull-request-review-summary\n| 📝 | ❌ **Failed** | `{head[:7]}` |" + emit([[{"user": {"login": "chatgpt-codex-connector[bot]", "type": "Bot"}, "body": summary}]]) + elif scenario in {"summary-only", "manual-summary"}: + summary = f"codex-pull-request-review-summary\n| 📝 | ✅ **Completed** | `{head[:7]}` |" + emit([[{"user": {"login": "chatgpt-codex-connector[bot]", "type": "Bot"}, "body": summary}]]) + else: + summary = f"codex-pull-request-review-summary\n| 📝 | ✅ **Completed** | `{head[:7]}` |" + emit([[{"user": {"login": "chatgpt-codex-connector[bot]", "type": "Bot"}, "body": summary}]]) + elif "/pulls/321/files" in joined: + if security_case: + emit([[{"filename": "src-tauri/src/dsc.rs", "status": "modified", "additions": 1, "deletions": 0}]]) + elif sync_case: + emit([[{"filename": "docs/retired.rs", "previous_filename": "src-tauri/src/sync.rs", "status": "renamed", "additions": 0, "deletions": 0}]]) + elif scenario == "files-empty": + emit([[]]) + elif scenario in {"formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-two-lines", "grouped-identifier-12", "grouped-identifier-16", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized"}: + emit([[{"filename": "docs/contact.md", "status": "added", "additions": 2 if scenario == "unicode-phone-two-lines" else 1, "deletions": 0}]]) + elif scenario in {"workflow-notes-missing", "workflow-notes-present", "workflow-placeholders"}: + emit([[{"filename": ".github/workflows/ci.yml", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario in {"workflow-delete", "workflow-delete-notes"}: + emit([[{"filename": ".github/workflows/ci.yml", "status": "removed", "additions": 0, "deletions": 1}]]) + elif scenario in {"workflow-rename-out", "workflow-rename-out-notes"}: + emit([[{"filename": "docs/retired-ci.yml", "previous_filename": ".github/workflows/ci.yml", "status": "renamed", "additions": 0, "deletions": 0}]]) + elif scenario == "renamed-previous-missing": + emit([[{"filename": "docs/retired-ci.yml", "status": "renamed", "additions": 0, "deletions": 0}]]) + elif scenario == "renamed-previous-null": + emit([[{"filename": "docs/retired-ci.yml", "previous_filename": None, "status": "renamed", "additions": 0, "deletions": 0}]]) + elif scenario == "renamed-previous-false": + emit([[{"filename": "docs/retired-ci.yml", "previous_filename": False, "status": "renamed", "additions": 0, "deletions": 0}]]) + elif scenario == "path-id": + path_id = "ABCDE" + "1234" + "F" + emit([[{"filename": f"docs/{path_id}.md", "status": "added", "additions": 1, "deletions": 0}]]) + elif scenario in {"security-notes-missing", "security-notes-present", "security-none", "security-pending"}: + emit([[{"filename": "src-tauri/src/tally/runtime.rs", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario == "security-rename-out": + emit([[{"filename": "src/runtime.rs", "previous_filename": "src-tauri/src/tally/runtime.rs", "status": "renamed", "additions": 0, "deletions": 0}]]) + elif scenario in {"security-crate", "security-agent-import", "security-dsc"}: + paths = {"security-crate": "src-tauri/crates/bridge-tally-core/src/master_binding.rs", "security-agent-import": "src-tauri/src/agent_import.rs", "security-dsc": "src-tauri/src/dsc.rs"} + emit([[{"filename": paths[scenario], "status": "removed", "additions": 0, "deletions": 1}]]) + elif scenario.startswith("home-"): + emit([[{"filename": "docs/example.md", "status": "added", "additions": 1, "deletions": 0}]]) + elif scenario == "binary-delete": + emit([[{"filename": "docs/old.png", "status": "removed", "additions": 0, "deletions": 0}]]) + elif scenario == "metadata-only": + emit([[{"filename": "docs/example.md", "status": "modified", "additions": 0, "deletions": 0}]]) + elif scenario == "metadata-private": + emit([[{"filename": "docs/" + "/" + "Users" + "/tester/" + "x" * 300, "status": "modified", "additions": 0, "deletions": 0}]]) + elif scenario == "metadata-incomplete": + emit([[{"filename": "docs/example.md", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario == "hunk-header-phone": + emit([[{"filename": "docs/example.md", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario == "hunk-header-literals": + emit([[{"filename": "docs/example.md", "status": "modified", "additions": 2, "deletions": 0}]]) + elif scenario == "hunk-binary-literal": + emit([[{"filename": "docs/example.md", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario in {"all-a-pan", "masked-pan", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier"}: + emit([[{"filename": "docs/example.md", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario == "quoted-path": + emit([[{"filename": "docs/café.md", "status": "added", "additions": 1, "deletions": 0}]]) + elif scenario in {"crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path"}: + status = "renamed" if scenario == "ambiguous-rename-path" else "added" + record = {"filename": "docs/a b/example.md" if scenario != "crlf-diff" else "docs/example.md", "status": status, "additions": 0 if status == "renamed" else 1, "deletions": 0} + if status == "renamed": record["previous_filename"] = "docs/a b/example.md" + emit([[record]]) + elif scenario == "gitlink": + emit([[{"filename": "vendor/module", "status": "modified", "additions": 1, "deletions": 1}]]) + elif scenario in {"implementation-p4-missing", "implementation-p4-present", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders"}: + suffix = {"implementation-p4-shell": "sh", "implementation-p4-powershell": "ps1", "implementation-p4-sql": "sql"}.get(scenario, "py") + emit([[{"filename": f"scripts/example.{suffix}", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale"}: + emit([[{"filename": "src-tauri/src/local_files/paths.rs", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario in {"migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"}: + emit([[{"filename": "src-tauri/migrations/001.sql", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario == "control-path": + emit([[{"filename": "docs/unsafe\x1b.md", "status": "added", "additions": 1, "deletions": 0}]]) + elif scenario == "malformed-files": + emit([[{"filename": "docs/example.md", "status": "added", "additions": 1, "deletions": 0}], [{"filename": 3, "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario == "missing-file-status": + emit([[{"filename": "docs/example.md", "status": "added", "additions": 1, "deletions": 0}], [{"filename": "docs/second.md", "additions": 1, "deletions": 0}]]) + elif scenario == "surface-unpins": + emit([[{"filename": "src/example.rs", "status": "modified", "additions": 1, "deletions": 0}], + [{"filename": "docs/tally/compatibility/compatibility-surface.json", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario == "files-count-mismatch": + emit([[{"filename": "docs/example.md", "status": "added", "additions": 1, "deletions": 0}]]) + else: + additions = 2 if scenario == "diff-truncated-payload" else 1 + emit([[{"filename": "docs/example.md", "status": "added", "additions": additions, "deletions": 0}], [{"filename": "docs/second.md", "status": "modified", "additions": 1, "deletions": 0}]]) + elif "/contents/" in joined: + if "review-checklist.md" in joined: + checklist = "# Review checklist\n" + "\n" * 8 + "- [ ] Errors are actionable without exposing sensitive values.\n" + emit({"encoding": "base64", "content": base64.b64encode(checklist.encode()).decode()}) + elif scenario in {"surface-fail", "draft-surface-fail"}: + fail("controlled surface read failure") + elif scenario == "surface-malformed": + emit({"content": "not-base64"}) + else: + digest = "a" * 64 + if scenario == "surface-unpins" and f"ref={head}" not in joined: + surface_files = [ + {"path": "src/example.rs", "sha256": digest}, + {"path": "docs/tally/compatibility/compatibility-surface.json", "sha256": digest}, + ] + else: + surface_files = [{"path": "src/example.rs", "sha256": digest}] + surface = {"schema_version": 1, "manifest_sha256": digest, + "files": surface_files} + if scenario == "surface-schema-malformed": + surface = {"files": [{"path": "src/example.rs"}]} + emit({"encoding": "base64", "content": base64.b64encode(json.dumps(surface).encode()).decode()}) + else: + fail("unknown API fixture") +else: + fail("unknown command fixture") +''' + + +class MergeGateControls(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.tmp = tempfile.TemporaryDirectory(prefix="merge-gate-controls-") + cls.bin = Path(cls.tmp.name) + gh = cls.bin / "gh" + gh.write_text(FAKE_GH) + gh.chmod(0o755) + jq = cls.bin / "jq" + jq.write_text("""#!/usr/bin/env python3 +import os, sys +if os.environ.get("GATE_SCENARIO") == "context-report-jq-failure" and "--rawfile" in sys.argv and "contexts" in sys.argv: + raise SystemExit(1) +os.execv(os.environ["GATE_REAL_JQ"], [os.environ["GATE_REAL_JQ"], *sys.argv[1:]]) +""") + jq.chmod(0o755) + cls.real_jq = shutil.which("jq") + if not cls.real_jq: + raise RuntimeError("jq is required for merge-gate controls") + + @classmethod + def tearDownClass(cls): + cls.tmp.cleanup() + + def run_gate(self, scenario="pass", extra_args=()): + env = os.environ.copy() + env["PATH"] = f"{self.bin}:{env['PATH']}" + env["GATE_SCENARIO"] = scenario + env["GATE_REAL_JQ"] = self.real_jq + counter = self.bin / f"{scenario}-counter-{os.getpid()}" + counter.write_text("0") + env["GATE_COUNTER"] = str(counter) + return subprocess.run( + [str(SCRIPT), "321", "--repo", "lamemustafa/bridge", *extra_args], + cwd=ROOT, + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + def assert_blocked(self, scenario, phrase): + result = self.run_gate(scenario) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn(phrase, result.stdout) + self.assertNotIn("MAY MERGE", result.stdout) + + def assert_indeterminate(self, scenario, phrase): + result = self.run_gate(scenario) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn(phrase, result.stdout) + self.assertNotIn("MAY MERGE", result.stdout) + + def test_server_full_sha_and_paginated_pages_can_pass(self): + result = self.run_gate() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("provider review records the full current head", result.stdout) + self.assertIn("MAY MERGE", result.stdout) + self.assertIn("--match-head-commit 0123456789abcdef0123456789abcdef01234567", result.stdout) + + def test_optional_skipped_check_does_not_block(self): + result = self.run_gate() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("optional check(s) are skipped", result.stdout) + + def test_required_skipped_check_blocks(self): + self.assert_blocked("required-skip", "required check 'Required checks' is not passing") + + def test_incomplete_check_run_is_indeterminate(self): + self.assert_indeterminate("check-run-incomplete", "head-bound check-run evidence") + + def test_empty_pending_legacy_status_does_not_block(self): + result = self.run_gate("status-empty-pending") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_missing_required_context_blocks(self): + self.assert_blocked("missing-required", "required check 'Rust format' was not reported") + + def test_unexpected_required_context_blocks(self): + self.assert_blocked("required-context-unexpected", "undocumented required check context") + + def test_cancelled_check_blocks(self): + self.assert_blocked("cancel-check", "failing, cancelled, or pending") + + def test_surface_transport_failure_is_indeterminate(self): + self.assert_indeterminate("surface-fail", "could not read and validate compatibility surface") + + def test_silent_checks_response_is_indeterminate(self): + self.assert_indeterminate("checks-silent", "checks query returned no JSON") + + def test_summary_only_short_sha_is_indeterminate(self): + self.assert_indeterminate("short-review", "independent-review-sha") + + def test_manual_full_sha_attestation_accepts_zero_finding_summary(self): + result = self.run_gate("manual-summary", ("--independent-review-sha", HEAD)) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("manual independent review attestation", result.stdout) + + def test_provider_review_requires_completed_summary(self): + self.assert_blocked("summary-failed", "review run for 0123456 is not completed") + + def test_blocked_merge_state_cannot_pass(self): + self.assert_blocked("blocked-state", "merge state BLOCKED") + + def test_unrecognised_final_merge_state_is_indeterminate(self): + self.assert_indeterminate("final-unrecognized", "unrecognised value") + + def test_short_thread_page_is_indeterminate(self): + self.assert_indeterminate("threads-short", "returned 1 of 101 nodes") + + def test_unresolved_second_thread_page_blocks(self): + self.assert_blocked("threads-unresolved-second", "1 of 101 review threads unresolved") + + def test_malformed_thread_pagination_is_indeterminate(self): + self.assert_indeterminate("threads-malformed-pagination", "no advancing cursor") + + def test_thread_total_drift_is_indeterminate(self): + self.assert_indeterminate("threads-total-drift", "totalCount changed") + + def test_head_change_is_blocked(self): + self.assert_blocked("head-moves", "PR head moved during preflight") + + def test_formatted_phone_is_scanned(self): + self.assert_blocked("formatted-phone", "privacy scan found") + + def test_hidden_metadata_identifier_is_still_scanned(self): + self.assert_blocked("hidden-comment-identifier", "privacy scan found") + + def test_unterminated_comment_cannot_supply_validation(self): + self.assert_blocked("unterminated-html-comment", "actual test or reproduction command") + + def test_multiline_comment_cannot_supply_functional_summary(self): + self.assert_blocked("policy-multiline-comment", "non-empty functional summary") + + def test_raw_html_comment_drift_blocks_revalidation(self): + self.assert_blocked("body-comment-drift", "description changed during preflight") + + def test_repeated_digit_phone_is_scanned(self): + self.assert_blocked("repeated-phone", "privacy scan found") + + def test_grouped_formatted_phone_is_scanned(self): + self.assert_blocked("formatted-phone-grouped", "privacy scan found") + + def test_separated_indian_mobile_styles_are_scanned(self): + for scenario in ("phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "unicode-phone"): + with self.subTest(scenario=scenario): + self.assert_blocked(scenario, "privacy scan found") + result = self.run_gate("unicode-phone-two-lines") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_header_shaped_added_payload_is_still_scanned(self): + self.assert_blocked("hunk-header-phone", "privacy scan found") + + def test_header_shaped_added_literals_count_as_payload(self): + result = self.run_gate("hunk-header-literals") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_binary_shaped_added_literal_is_not_binary_metadata(self): + result = self.run_gate("hunk-binary-literal") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_unrelated_separator_groups_are_not_fused_into_a_phone(self): + result = self.run_gate("separated-dates") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_new_year_date_ranges_are_not_fused_into_a_phone(self): + result = self.run_gate("separated-dates-new-year") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_year_month_date_ranges_are_not_fused_into_a_phone(self): + result = self.run_gate("separated-dates-year-month") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_adr_identifier_with_underscores_remains_clean(self): + result = self.run_gate("adr-identifier") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_destination_path_is_scanned(self): + self.assert_blocked("path-id", "privacy scan found") + + def test_binary_deletion_is_not_treated_as_added_content(self): + result = self.run_gate("binary-delete") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_malformed_surface_is_indeterminate(self): + self.assert_indeterminate("surface-malformed", "could not read and validate compatibility surface") + + def test_surface_with_no_v1_manifest_schema_is_indeterminate(self): + self.assert_indeterminate("surface-schema-malformed", "could not read and validate compatibility surface") + + def test_removed_base_pin_requires_human_hold(self): + self.assert_indeterminate("surface-unpins", "base-pinned path(s) are absent") + + def test_compare_lineage_mismatch_is_indeterminate(self): + self.assert_indeterminate("lineage-mismatch", "base/head compare did not prove") + + def test_check_run_wrong_head_is_indeterminate(self): + self.assert_indeterminate("check-run-wrong-head", "head-bound check-run evidence") + + def test_check_run_count_mismatch_is_indeterminate(self): + self.assert_indeterminate("check-run-count-mismatch", "head-bound check-run evidence") + + def test_base_oid_mismatch_is_indeterminate(self): + self.assert_indeterminate("base-oid-mismatch", "PR base OID") + + def test_malformed_commit_status_is_indeterminate(self): + self.assert_indeterminate("status-malformed", "head-bound commit-status evidence") + + def test_malformed_changed_file_is_indeterminate(self): + self.assert_indeterminate("malformed-files", "could not read the complete changed-file set") + + def test_missing_changed_file_status_is_indeterminate(self): + self.assert_indeterminate("missing-file-status", "could not read the complete changed-file set") + + def test_changed_file_count_mismatch_is_indeterminate(self): + self.assert_indeterminate("files-count-mismatch", "changed-file response has") + + def test_empty_file_array_is_not_one_filename(self): + self.assert_indeterminate("files-empty", "contained no filenames") + + def test_fractional_thread_count_is_indeterminate(self): + self.assert_indeterminate("threads-fractional", "could not read review threads") + + def test_empty_page_cannot_claim_more_threads(self): + self.assert_indeterminate("threads-empty-more", "no nodes while claiming another page") + + def test_template_checklist_permalink_on_continuation_passes(self): + result = self.run_gate("checklist-template-continuation") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("description links a completed review-checklist item", result.stdout) + + def test_checklist_permalink_must_bind_the_full_current_head(self): + self.assert_blocked("checklist-stale-ref", "same-repository line-specific") + self.assert_blocked("checklist-anchor-suffix", "same-repository line-specific") + + def test_foreign_checklist_link_blocks(self): + self.assert_blocked("checklist-foreign", "same-repository line-specific") + + def test_unlinked_checklist_text_blocks(self): + self.assert_blocked("checklist-unlinked", "same-repository line-specific") + + def test_omitted_nonremoved_diff_file_is_indeterminate(self): + self.assert_indeterminate("diff-omits-file", "privacy diff coverage failed") + + def test_truncated_textual_diff_payload_is_indeterminate(self): + self.assert_indeterminate("diff-truncated-payload", "privacy diff coverage failed") + + def test_metadata_only_zero_line_diff_can_pass(self): + result = self.run_gate("metadata-only") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("metadata-only diff section", result.stdout) + + def test_metadata_only_examples_are_redacted_and_bounded(self): + result = self.run_gate("metadata-private") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("metadata-only diff section", result.stdout) + self.assertNotIn("privacy diff coverage failed", result.stdout) + self.assertNotIn("tester", result.stdout) + examples = next(line for line in result.stdout.splitlines() if "metadata-only diff section" in line) + self.assertLess(len(examples), 260) + self.assertNotIn("x" * 161, examples) + + def test_metadata_only_diff_requires_rest_zero_totals(self): + self.assert_indeterminate("metadata-incomplete", "metadata-only 'docs/example.md' conflicts") + + def test_pr_selector_must_be_numeric(self): + env = os.environ.copy() + env["PATH"] = f"{self.bin}:{env['PATH']}" + result = subprocess.run( + [str(SCRIPT), "321;echo unsafe", "--repo", "lamemustafa/bridge"], + cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=False, + ) + self.assertEqual(result.returncode, 2) + self.assertIn("PR selector must be numeric", result.stderr) + + def test_repo_identity_must_be_safe(self): + env = os.environ.copy() + env["PATH"] = f"{self.bin}:{env['PATH']}" + result = subprocess.run( + [str(SCRIPT), "321", "--repo", "lamemustafa/bridge;echo unsafe"], + cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=False, + ) + self.assertEqual(result.returncode, 2) + self.assertIn("--repo must be OWNER/NAME", result.stderr) + + def test_other_repository_policy_is_not_assumed(self): + env = os.environ.copy() + env["PATH"] = f"{self.bin}:{env['PATH']}" + result = subprocess.run( + [str(SCRIPT), "321", "--repo", "example/other"], + cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=False, + ) + self.assertEqual(result.returncode, 2) + self.assertIn("unsupported repository", result.stderr) + + def test_missing_functional_summary_blocks(self): + self.assert_blocked("missing-functional-summary", "functional summary") + + def test_missing_test_summary_blocks(self): + self.assert_blocked("missing-test-summary", "test or reproduction command") + + def test_final_changed_body_revalidates_test_evidence(self): + self.assert_blocked("body-loses-evidence", "description changed and no longer carries test or reproduction evidence") + + def test_final_changed_body_revalidates_functional_summary(self): + self.assert_blocked("body-loses-functional", "description changed and no longer carries a non-empty functional summary") + + def test_draft_merge_state_blocks(self): + self.assert_blocked("draft-state", "merge state DRAFT") + + def test_documented_required_context_cannot_be_omitted(self): + self.assert_blocked("protection-missing-gitguardian", "branch protection omits 1 documented") + + def test_failing_combined_commit_status_blocks(self): + self.assert_indeterminate("status-failed-combined", "head-bound commit-status evidence") + + def test_failing_individual_commit_status_blocks(self): + self.assert_indeterminate("status-failed-context", "head-bound commit-status evidence") + + def test_duplicate_thread_ids_are_indeterminate(self): + self.assert_indeterminate("threads-duplicate-id", "pagination repeated thread IDs") + + def test_checklist_anchor_must_reference_an_existing_line(self): + self.assert_blocked("checklist-missing-anchor", "review-checklist link to an existing line") + + def test_template_functional_prompt_does_not_count_as_summary(self): + self.assert_blocked("template-functional-prompt", "non-empty functional summary") + + def test_template_validation_prompts_do_not_count_as_evidence(self): + self.assert_blocked("template-validation-prompt", "test or reproduction command") + + def test_pr_title_identifier_is_scanned(self): + self.assert_blocked("metadata-title-id", "privacy scan found") + + def test_pr_commit_metadata_identifier_is_scanned(self): + self.assert_blocked("metadata-commit-id", "privacy scan found") + + def test_standard_commit_identity_fields_are_validated_and_scanned(self): + self.assert_blocked("metadata-author-id", "privacy scan found") + result = self.run_gate("metadata-unlinked-identities") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_capped_commit_metadata_is_indeterminate(self): + self.assert_indeterminate("metadata-capped", "complete head-bound PR commit metadata") + + def test_truncated_commit_metadata_is_indeterminate(self): + self.assert_indeterminate("metadata-truncated", "complete head-bound PR commit metadata") + + def test_duplicate_commit_metadata_is_indeterminate(self): + self.assert_indeterminate("metadata-duplicate", "complete head-bound PR commit metadata") + + def test_commit_metadata_head_mismatch_is_indeterminate(self): + self.assert_indeterminate("metadata-head-mismatch", "complete head-bound PR commit metadata") + + def test_all_a_pan_is_not_exempted_as_a_placeholder(self): + self.assert_blocked("all-a-pan", "privacy scan found") + + def test_explicit_masked_pan_remains_a_placeholder(self): + result = self.run_gate("masked-pan") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_quoted_git_destination_path_is_covered(self): + result = self.run_gate("quoted-path") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_crlf_and_ambiguous_unquoted_diffs_preserve_coverage(self): + for scenario in ("crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path"): + with self.subTest(scenario=scenario): + result = self.run_gate(scenario) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_gitlink_requires_explicit_provenance_license_notice_review(self): + self.assert_indeterminate("gitlink", "gitlink change(s) require explicit provenance, license, and NOTICE review") + + def test_control_character_in_destination_path_is_indeterminate(self): + self.assert_indeterminate("control-path", "could not read the complete changed-file set") + + def test_populated_sha_without_command_is_not_validation_evidence(self): + self.assert_blocked("template-validation-sha-only", "test or reproduction command") + + def test_validation_heading_without_command_is_not_evidence(self): + self.assert_blocked("template-validation-empty", "actual test or reproduction command") + + def test_protection_requires_strict_status_checks(self): + self.assert_indeterminate("protection-nonstrict", "required status-check response was malformed") + + def test_duplicate_check_run_ids_are_indeterminate(self): + self.assert_indeterminate("check-run-duplicate-id", "head-bound check-run evidence") + + def test_grouped_12_and_16_digit_identifiers_are_scanned(self): + for scenario in ("grouped-identifier-12", "grouped-identifier-16"): + with self.subTest(scenario=scenario): + self.assert_blocked(scenario, "privacy scan found") + + def test_workflow_change_requires_rollback_and_migration_notes(self): + self.assert_blocked("workflow-notes-missing", "workflow change lacks non-empty rollback notes") + self.assert_blocked("workflow-placeholders", "workflow change lacks non-empty rollback notes") + + def test_workflow_change_with_required_notes_can_pass(self): + result = self.run_gate("workflow-notes-present") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_workflow_delete_and_rename_out_require_notes(self): + for scenario in ("workflow-delete", "workflow-rename-out"): + with self.subTest(scenario=scenario): + self.assert_blocked(scenario, "workflow change lacks non-empty rollback notes") + + def test_workflow_delete_and_rename_out_with_notes_can_pass(self): + for scenario in ("workflow-delete-notes", "workflow-rename-out-notes"): + with self.subTest(scenario=scenario): + result = self.run_gate(scenario) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_renamed_file_requires_a_string_previous_filename(self): + for scenario in ("renamed-previous-missing", "renamed-previous-null", "renamed-previous-false"): + with self.subTest(scenario=scenario): + self.assert_indeterminate(scenario, "could not read the complete changed-file set") + + def test_sensitive_paths_require_security_impact_notes_including_renames(self): + for scenario in ("security-notes-missing", "security-rename-out", "security-crate", "security-agent-import", "security-dsc"): + with self.subTest(scenario=scenario): + self.assert_blocked(scenario, "security-impact notes") + result = self.run_gate("security-notes-present") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + result = self.run_gate("security-none") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assert_blocked("security-pending", "security-impact notes") + + def test_sensitive_paths_without_security_attestation_are_indeterminate(self): + self.assert_indeterminate("security-review-missing", "security-focused reviewer comment") + + def test_refreshed_failed_or_required_skipped_runs_block(self): + for scenario in ("check-run-failed", "check-run-required-skip"): + with self.subTest(scenario=scenario): + self.assert_blocked(scenario, "refreshed check run(s)") + result = self.run_gate("check-run-optional-skip") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_nonempty_pending_legacy_status_is_indeterminate(self): + self.assert_indeterminate("status-nonempty-pending", "commit-status evidence") + + def test_truncated_legacy_status_pages_are_indeterminate(self): + self.assert_indeterminate("status-truncated", "complete head-bound commit-status evidence") + + def test_complete_combined_status_pages_bind_head_and_contexts(self): + result = self.run_gate("status-two-page") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + for scenario in ("status-truncated", "status-duplicate", "status-mixed-head"): + with self.subTest(scenario=scenario): + self.assert_indeterminate(scenario, "complete head-bound commit-status evidence") + + def test_validation_commands_must_be_concrete_and_in_validation(self): + for scenario in ("validation-command-outside", "validation-tool-prose", "validation-placeholder-command", "validation-html-comment"): + with self.subTest(scenario=scenario): + self.assert_blocked(scenario, "actual test or reproduction command") + + def test_checklist_heading_is_not_a_completed_item(self): + self.assert_blocked("checklist-heading", "review-checklist link") + + def test_context_report_jq_failure_is_indeterminate_without_fabricated_count(self): + result = self.run_gate("context-report-jq-failure") + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("could not compute bounded required-check diagnostics", result.stdout) + self.assertNotIn("1 of 0 required check contexts", result.stdout) + self.assertNotIn("all 0 required check contexts passed", result.stdout) + + def test_required_context_diagnostics_are_bounded_for_failures(self): + result = self.run_gate("required-context-output-bound") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("50 of 55 required check contexts", result.stdout) + self.assertLess(len(result.stdout.encode()), 6000) + self.assertNotIn("z" * 100, result.stdout) + + def test_security_review_is_focused_independent_and_current(self): + for scenario in ("security-review-stale", "security-review-author", "security-review-unrelated", "security-review-outsider", "security-review-dismissed"): + with self.subTest(scenario=scenario): + self.assert_indeterminate(scenario, "security-focused reviewer comment") + result = self.run_gate("security-review-valid") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_sync_rename_out_requires_migration_notes(self): + self.assert_blocked("sync-migration-missing", "migration compatibility") + result = self.run_gate("sync-migration-present") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_implementation_additions_need_all_three_p4_answers(self): + self.assert_blocked("implementation-p4-missing", "all three substantive P4") + self.assert_blocked("implementation-p4-shell", "all three substantive P4") + self.assert_blocked("implementation-p4-powershell", "all three substantive P4") + self.assert_blocked("implementation-p4-sql", "all three substantive P4") + self.assert_blocked("p4-placeholders", "all three substantive P4") + result = self.run_gate("implementation-p4-present") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_platform_sensitive_paths_need_substantive_both_host_evidence(self): + self.assert_blocked("platform-evidence-missing", "substantive Windows validation") + result = self.run_gate("platform-evidence-present") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + result = self.run_gate("platform-checkbox-evidence") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assert_blocked("platform-checkbox-comment", "substantive Windows validation") + self.assert_blocked("platform-unaffected-bare", "substantive Windows validation") + result = self.run_gate("platform-unaffected-rationale") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_database_migration_paths_need_rollback_notes(self): + self.assert_blocked("migration-rollback-missing", "database migration path lacks non-empty rollback notes") + result = self.run_gate("migration-rollback-present") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_wrapped_canonical_migration_template_field_is_recognized(self): + result = self.run_gate("migration-template-wrapped") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assert_blocked("migration-template-other-field", "database migration path lacks non-empty rollback notes") + + def test_developer_home_path_shapes_are_scanned_without_echoing_values(self): + for scenario in ("home-macos", "home-unix", "home-windows", "home-macos-root", "home-unix-root", "home-windows-forward", "home-windows-escaped"): + with self.subTest(scenario=scenario): + result = self.run_gate(scenario) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("developer-home path shape", result.stdout) + self.assertNotIn("tester", result.stdout) + result = self.run_gate("home-regex-source") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_diff_parser_uses_python38_compatible_prefix_removal(self): + import importlib.util + parser = ROOT / "scripts" / "merge_gate_diff.py" + spec = importlib.util.spec_from_file_location("merge_gate_diff", parser) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + class NoRemovePrefix(str): + def removeprefix(self, _prefix): + raise AssertionError("Python 3.8 lacks str.removeprefix") + self.assertEqual(module.diff_destination(NoRemovePrefix("diff --git a/x b/y")), "y") + self.assertEqual(module.textual_destination(NoRemovePrefix("+++ b/y")), "y") + + def test_diff_parser_accepts_git_generated_mixed_quote_rename_headers(self): + import importlib.util + parser = ROOT / "scripts" / "merge_gate_diff.py" + spec = importlib.util.spec_from_file_location("merge_gate_diff_mixed", parser) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + unicode_name = '"b/\\351\\233\\252.txt"' + self.assertEqual(module.diff_destination('diff --git "a/\\351\\233\\252.txt" b/plain.txt'), "plain.txt") + self.assertEqual(module.diff_destination('diff --git a/plain.txt ' + unicode_name), "雪.txt") + self.assertEqual(module.diff_destination("diff --git a/foo b/bar b/foo b/bar"), "foo b/bar") + + def test_definite_blocker_wins_over_indeterminate_evidence(self): + self.assert_blocked("draft-surface-fail", "merge state DRAFT") + result = self.run_gate("draft-surface-fail") + self.assertNotIn("INDETERMINATE — do not merge", result.stdout) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/scripts/merge_gate_diff.py b/scripts/merge_gate_diff.py new file mode 100755 index 000000000..165e56bb6 --- /dev/null +++ b/scripts/merge_gate_diff.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +"""Parse the textual portion of a GitHub pull-request diff for merge-gate.""" +from __future__ import annotations + +import json +import sys + + +def remove_prefix(value: str, prefix: str) -> str: + """Python 3.8-compatible equivalent of str.removeprefix.""" + return value[len(prefix):] if value.startswith(prefix) else value + + +def decode_quoted_path(value: str) -> str: + if not (value.startswith('"') and value.endswith('"')): + raise ValueError("expected quoted path") + body = value[1:-1] + raw = bytearray() + index = 0 + while index < len(body): + char = body[index] + if char != "\\": + raw.extend(char.encode("utf-8")) + index += 1 + continue + index += 1 + if index == len(body): + raise ValueError("unterminated escape") + escaped = body[index] + if escaped in '"\\': + raw.append(ord(escaped)) + index += 1 + elif escaped in "abfnrtv": + raw.append({"a": 7, "b": 8, "f": 12, "n": 10, "r": 13, "t": 9, "v": 11}[escaped]) + index += 1 + elif escaped in "01234567": + octal = body[index:index + 3] + if len(octal) != 3 or any(char not in "01234567" for char in octal): + raise ValueError("invalid octal escape") + raw.append(int(octal, 8)) + index += 3 + else: + raise ValueError("unsupported escape") + return raw.decode("utf-8") + + +def quoted_token(value: str, start: int) -> tuple[str, int]: + if value[start] != '"': + raise ValueError("expected quote") + index = start + 1 + escaped = False + while index < len(value): + char = value[index] + if escaped: + escaped = False + elif char == "\\": + escaped = True + elif char == '"': + return value[start:index + 1], index + 1 + index += 1 + raise ValueError("unterminated quoted token") + + +def diff_destination(line: str) -> str | None: + value = remove_prefix(line, "diff --git ") + if value.startswith('"'): + _source, index = quoted_token(value, 0) + if index >= len(value) or value[index] != " ": + raise ValueError("missing destination") + destination = value[index + 1:] + path = decode_quoted_path(destination) if destination.startswith('"') else destination + if not path.startswith("b/"): + raise ValueError("destination does not start b/") + return path[2:] + # Git quotes each path independently. A plain source with a quoted + # destination is therefore valid (and occurs for plain-to-Unicode + # renames); the quote itself cannot occur in an unquoted token. + quoted_destination = value.find(' "b/') + if quoted_destination >= 0: + source = value[:quoted_destination] + destination = value[quoted_destination + 1:] + if not source.startswith("a/"): + raise ValueError("invalid unquoted source") + path = decode_quoted_path(destination) + if not path.startswith("b/"): + raise ValueError("destination does not start b/") + return path[2:] + # An unquoted header has no escaping grammar. Splitting on the first + # `` b/`` silently misparses a legal-looking filename containing that + # sequence. Defer an ambiguous header to the independently parsed +++ + # destination; that destination is subsequently reconciled to the REST + # changed-file inventory. Metadata-only ambiguous records remain + # indeterminate because they have no unambiguous textual identity. + parts = value.split(" b/") + if len(parts) != 2: + # Pure mode/deletion records may have neither +++ nor rename-to. A + # same-path header can still be proven when exactly one candidate + # delimiter leaves identical a/ and b/ paths. Do not guess when the + # filename makes that proof ambiguous. + matches: list[str] = [] + start = 0 + while True: + index = value.find(" b/", start) + if index < 0: + break + source = value[:index] + destination = value[index + 1:] + if source.startswith("a/") and destination.startswith("b/") and source[2:] == destination[2:]: + matches.append(destination[2:]) + start = index + 1 + return matches[0] if len(matches) == 1 else None + source, destination = parts + if not source.startswith("a/") or not destination: + raise ValueError("invalid unquoted header") + return destination + + +def textual_destination(line: str) -> str | None: + value = remove_prefix(line, "+++ ") + if value == "/dev/null": + return None + if value.startswith('"'): + path = decode_quoted_path(value) + else: + path = value + if not path.startswith("b/"): + raise ValueError("textual destination does not start b/") + return path[2:] + + +def parse(lines: list[str]) -> dict[str, object]: + records: list[dict[str, object]] = [] + added_payload: list[str] = [] + record: dict[str, object] | None = None + + def emit() -> None: + if record is not None: + records.append(record.copy()) + + for raw_line in lines: + # A diff is delimited by LF. CR from CRLF or in an added payload is + # content for scanning/counting, but must not prevent recognising a + # protocol header. + line = raw_line[:-1] if raw_line.endswith("\r") else raw_line + if line.startswith("diff --git "): + emit() + record = { + "destination": diff_destination(line), + "textual_destination": None, + "rename_destination": None, + "added": 0, + "deleted": 0, + "binary": False, + "gitlink": False, + "in_hunk": False, + } + continue + if record is None: + continue + if not record["in_hunk"] and line.startswith("+++ "): + record["textual_destination"] = textual_destination(line) + continue + if not record["in_hunk"] and line.startswith("rename to "): + destination = remove_prefix(line, "rename to ") + record["rename_destination"] = decode_quoted_path(destination) if destination.startswith('"') else destination + continue + if not record["in_hunk"] and line in {"GIT binary patch"} or ( + not record["in_hunk"] and line.startswith("Binary files ") and line.endswith(" differ") + ): + record["binary"] = True + continue + if not record["in_hunk"] and line in { + "old mode 160000", "new mode 160000", "new file mode 160000", + "deleted file mode 160000", + }: + record["gitlink"] = True + continue + if line.startswith("@@ "): + record["in_hunk"] = True + continue + if record["in_hunk"] and raw_line.startswith("+"): + record["added"] = int(record["added"]) + 1 + added_payload.append(raw_line[1:]) + elif record["in_hunk"] and raw_line.startswith("-"): + record["deleted"] = int(record["deleted"]) + 1 + emit() + for record in records: + record.pop("in_hunk") + if record["destination"] is None: + if record["textual_destination"] is None and record["rename_destination"] is None: + raise ValueError("ambiguous header without textual destination") + record["destination"] = record["textual_destination"] or record["rename_destination"] + record.pop("rename_destination") + return {"records": records, "added_payload": added_payload} + + +if __name__ == "__main__": + try: + raw = sys.stdin.buffer.read().decode("utf-8") + # Split only on the protocol's LF delimiter. Do not let Python's + # universal-newline mode erase CR or Unicode line-separator payload. + print(json.dumps(parse(raw.split("\n")))) + except (UnicodeError, ValueError) as error: + print(f"merge_gate_diff_error:{error}", file=sys.stderr) + raise SystemExit(2) From 76b02e9dbe4fa9fea6a90dc9390a2718fc691445 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 13 Sep 2026 06:01:31 +0530 Subject: [PATCH 02/27] Rectify merge gate evidence controls --- .github/workflows/ci.yml | 4 ++- scripts/merge-gate.sh | 33 +++++++++++++++++++++--- scripts/merge-gate.test.py | 52 ++++++++++++++++++++++++++++++++++---- 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 801ddfe7f..21a1ca509 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,7 +113,9 @@ jobs: workflow-consistency: name: Workflow consistency runs-on: ubuntu-latest - timeout-minutes: 10 + # The Linux suite has exceeded 780 seconds before setup overhead; retain a + # bounded budget while allowing every required consistency check to finish. + timeout-minutes: 20 permissions: contents: read steps: diff --git a/scripts/merge-gate.sh b/scripts/merge-gate.sh index 4283d904f..bd899e355 100755 --- a/scripts/merge-gate.sh +++ b/scripts/merge-gate.sh @@ -3,6 +3,7 @@ # # Usage: scripts/merge-gate.sh [--repo OWNER/NAME] # [--independent-review-sha FULL_SHA] (explicit manual review attestation) +# [--binary-review-sha FULL_SHA] (manual binary-byte, ownership, license, and NOTICE review) # DSC/credential review record format (review or PR comment by another reviewer): # Security review: FULL_SHA # Result: accepted @@ -19,6 +20,7 @@ set -uo pipefail PR="" REPO="" INDEPENDENT_REVIEW_SHA="" +BINARY_REVIEW_SHA="" while [ $# -gt 0 ]; do case "$1" in --repo) @@ -41,6 +43,16 @@ while [ $# -gt 0 ]; do [ -n "$INDEPENDENT_REVIEW_SHA" ] || { echo "--independent-review-sha= needs a full commit SHA" >&2; exit 2; } shift ;; + --binary-review-sha) + BINARY_REVIEW_SHA="${2:-}" + [ -n "$BINARY_REVIEW_SHA" ] || { echo "--binary-review-sha needs a full commit SHA" >&2; exit 2; } + shift 2 + ;; + --binary-review-sha=*) + BINARY_REVIEW_SHA="${1#*=}" + [ -n "$BINARY_REVIEW_SHA" ] || { echo "--binary-review-sha= needs a full commit SHA" >&2; exit 2; } + shift + ;; -h|--help) sed -n '2,17p' "$0" exit 0 @@ -146,6 +158,11 @@ if [ -n "$INDEPENDENT_REVIEW_SHA" ] && ! [[ "$INDEPENDENT_REVIEW_SHA" =~ ^[0-9a- elif [ -n "$INDEPENDENT_REVIEW_SHA" ] && [ "$INDEPENDENT_REVIEW_SHA" != "$head" ]; then bad "independent review attestation names a different commit than the PR head" fi +if [ -n "$BINARY_REVIEW_SHA" ] && ! [[ "$BINARY_REVIEW_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then + bad "binary review attestation must be a full 40-hex commit SHA" +elif [ -n "$BINARY_REVIEW_SHA" ] && [ "$BINARY_REVIEW_SHA" != "$head" ]; then + bad "binary review attestation names a different commit than the PR head" +fi echo "PR #$PR ($REPO) head=$short base=$base $mergeable/$mstate" [ "$pstate" = "OPEN" ] || bad "PR is $pstate, not OPEN" @@ -699,6 +716,7 @@ body_section_has_content() { waiting = 0 next } + if (waiting && $0 ~ /^[[:space:]]*```/) next if (waiting && $0 ~ /[^[:space:]]/) { if ($0 ~ /^[[:space:]]*") if scenario == "validation-html-comment": body = body.replace("`python3 scripts/merge-gate.test.py`", "Tests not run ") if scenario == "unterminated-html-comment": body = body.replace("`python3 scripts/merge-gate.test.py`", "" if scenario == "body-comment-drift" and view_count > 0: body += "\n" @@ -220,7 +226,7 @@ def fail(message="controlled API failure"): body = body.replace("blob/HEAD", f"blob/{head}") if scenario == "checklist-stale-ref": body = body.replace(f"blob/{head}", "blob/" + "f" * 40) - one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "path-id", "binary-delete", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case + one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case selected_base = new_head if scenario == "base-oid-mismatch" else base emit({"headRefOid": selected_head, "baseRefOid": selected_base, "baseRefName": "master", "mergeable": "MERGEABLE", "mergeStateStatus": final_state, @@ -265,6 +271,8 @@ def fail(message="controlled API failure"): emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{homes[scenario]}\n") elif scenario == "binary-delete": emit("diff --git a/docs/old.png b/docs/old.png\nBinary files a/docs/old.png and /dev/null differ\n") + elif scenario in {"binary-review", "binary-review-private", "binary-review-head-moves"}: + emit("diff --git a/docs/new.png b/docs/new.png\nBinary files /dev/null and b/docs/new.png differ\n") elif scenario == "diff-omits-file": emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+safe text\n") elif scenario == "diff-truncated-payload": @@ -296,6 +304,8 @@ def fail(message="controlled API failure"): emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {phones[scenario]}\n") elif scenario == "unicode-phone": emit("diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic 69876\u00a054321\n") + elif scenario == "unicode-phone-tab": + emit("diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic 69876\t54321\n") elif scenario == "unicode-phone-two-lines": emit("diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +2 @@\n+69876\n+54321\n") elif scenario == "metadata-only": @@ -492,7 +502,7 @@ def status_page(rows, total_count, state="success", page_head=head): emit([[{"filename": "docs/retired.rs", "previous_filename": "src-tauri/src/sync.rs", "status": "renamed", "additions": 0, "deletions": 0}]]) elif scenario == "files-empty": emit([[]]) - elif scenario in {"formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-two-lines", "grouped-identifier-12", "grouped-identifier-16", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized"}: + elif scenario in {"formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "grouped-identifier-12", "grouped-identifier-16", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized"}: emit([[{"filename": "docs/contact.md", "status": "added", "additions": 2 if scenario == "unicode-phone-two-lines" else 1, "deletions": 0}]]) elif scenario in {"workflow-notes-missing", "workflow-notes-present", "workflow-placeholders"}: emit([[{"filename": ".github/workflows/ci.yml", "status": "modified", "additions": 1, "deletions": 0}]]) @@ -520,6 +530,8 @@ def status_page(rows, total_count, state="success", page_head=head): emit([[{"filename": "docs/example.md", "status": "added", "additions": 1, "deletions": 0}]]) elif scenario == "binary-delete": emit([[{"filename": "docs/old.png", "status": "removed", "additions": 0, "deletions": 0}]]) + elif scenario in {"binary-review", "binary-review-private", "binary-review-head-moves"}: + emit([[{"filename": "docs/new.png", "status": "added", "additions": 0, "deletions": 0}]]) elif scenario == "metadata-only": emit([[{"filename": "docs/example.md", "status": "modified", "additions": 0, "deletions": 0}]]) elif scenario == "metadata-private": @@ -738,7 +750,7 @@ def test_grouped_formatted_phone_is_scanned(self): self.assert_blocked("formatted-phone-grouped", "privacy scan found") def test_separated_indian_mobile_styles_are_scanned(self): - for scenario in ("phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "unicode-phone"): + for scenario in ("phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "unicode-phone", "unicode-phone-tab"): with self.subTest(scenario=scenario): self.assert_blocked(scenario, "privacy scan found") result = self.run_gate("unicode-phone-two-lines") @@ -778,6 +790,29 @@ def test_binary_deletion_is_not_treated_as_added_content(self): result = self.run_gate("binary-delete") self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_binary_additions_require_two_current_head_manual_attestations(self): + self.assert_blocked("binary-review", "require matching --binary-review-sha and --independent-review-sha") + binary_only = self.run_gate("binary-review", ("--binary-review-sha", HEAD)) + self.assertEqual(binary_only.returncode, 1, binary_only.stdout + binary_only.stderr) + self.assertIn("require matching --binary-review-sha and --independent-review-sha", binary_only.stdout) + malformed = self.run_gate("binary-review", ("--binary-review-sha", "not-a-sha", "--independent-review-sha", HEAD)) + self.assertEqual(malformed.returncode, 1, malformed.stdout + malformed.stderr) + self.assertIn("binary review attestation must be a full 40-hex", malformed.stdout) + stale = self.run_gate("binary-review", ("--binary-review-sha", "f" * 40, "--independent-review-sha", HEAD)) + self.assertEqual(stale.returncode, 1, stale.stdout + stale.stderr) + self.assertIn("binary review attestation names a different commit", stale.stdout) + current = self.run_gate("binary-review", ("--binary-review-sha", HEAD, "--independent-review-sha", HEAD)) + self.assertEqual(current.returncode, 0, current.stdout + current.stderr) + self.assertIn("explicit current-head binary and independent review attestations", current.stdout) + moved = self.run_gate("binary-review-head-moves", ("--binary-review-sha", HEAD, "--independent-review-sha", HEAD)) + self.assertEqual(moved.returncode, 1, moved.stdout + moved.stderr) + self.assertIn("PR head moved during preflight", moved.stdout) + + def test_binary_attestation_does_not_bypass_privacy_metadata_scan(self): + result = self.run_gate("binary-review-private", ("--binary-review-sha", HEAD, "--independent-review-sha", HEAD)) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("privacy scan found", result.stdout) + def test_malformed_surface_is_indeterminate(self): self.assert_indeterminate("surface-malformed", "could not read and validate compatibility surface") @@ -1052,6 +1087,13 @@ def test_validation_commands_must_be_concrete_and_in_validation(self): for scenario in ("validation-command-outside", "validation-tool-prose", "validation-placeholder-command", "validation-html-comment"): with self.subTest(scenario=scenario): self.assert_blocked(scenario, "actual test or reproduction command") + result = self.run_gate("validation-node-command") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_empty_fenced_functional_summary_is_not_content(self): + self.assert_blocked("empty-fenced-summary", "non-empty functional summary") + result = self.run_gate("nonempty-fenced-summary") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) def test_checklist_heading_is_not_a_completed_item(self): self.assert_blocked("checklist-heading", "review-checklist link") From 925465cdedc603b5a295645513bf184083ef7f20 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 13 Sep 2026 06:11:18 +0530 Subject: [PATCH 03/27] fix: reject structural-only policy summaries and reseal workflow --- .github/workflows/ci.yml | 4 +-- .../compatibility/compatibility-matrix.json | 2 +- .../compatibility/compatibility-surface.json | 4 +-- scripts/merge-gate.sh | 4 ++- scripts/merge-gate.test.py | 28 ++++++++++++++----- 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21a1ca509..42e8153c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,8 +113,8 @@ jobs: workflow-consistency: name: Workflow consistency runs-on: ubuntu-latest - # The Linux suite has exceeded 780 seconds before setup overhead; retain a - # bounded budget while allowing every required consistency check to finish. + # Allow the full suite and setup to finish on slower runners while retaining + # a bounded job timeout. timeout-minutes: 20 permissions: contents: read diff --git a/docs/tally/compatibility/compatibility-matrix.json b/docs/tally/compatibility/compatibility-matrix.json index d3374f017..a3aae5511 100644 --- a/docs/tally/compatibility/compatibility-matrix.json +++ b/docs/tally/compatibility/compatibility-matrix.json @@ -1,7 +1,7 @@ { "schema_version": 1, "bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e", - "compatibility_surface_sha256": "bef1632064166f9b80ff3beef9d0196b92667e4670d0834cdd183651a19a062a", + "compatibility_surface_sha256": "8ac9e496dac7f3f620ff10e11a49c709e27c5cb740fa1d5e0bf532430e45050b", "claims": [ { "claim_id": "erp9-6-6-3-windows-education-xml-one-company", diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json index c25468c4a..5908cd6d3 100644 --- a/docs/tally/compatibility/compatibility-surface.json +++ b/docs/tally/compatibility/compatibility-surface.json @@ -3,7 +3,7 @@ "files": [ { "path": ".github/workflows/ci.yml", - "sha256": "f1d96290429f3d7d9821440ad989fc68b41d8ee3c99106d0c93151c95b03b6d1" + "sha256": "aeed5fab97de591ed8ad1cf8cf094eaaac5356d40adbd10158905ec19316fd2f" }, { "path": ".github/workflows/dependency-security.yml", @@ -850,5 +850,5 @@ "sha256": "a8ac2714fecf51947f2822c8c46d7ce2e8602c732780ff60566a7771f0836f9a" } ], - "manifest_sha256": "bef1632064166f9b80ff3beef9d0196b92667e4670d0834cdd183651a19a062a" + "manifest_sha256": "8ac9e496dac7f3f620ff10e11a49c709e27c5cb740fa1d5e0bf532430e45050b" } \ No newline at end of file diff --git a/scripts/merge-gate.sh b/scripts/merge-gate.sh index bd899e355..9e9c5d85d 100755 --- a/scripts/merge-gate.sh +++ b/scripts/merge-gate.sh @@ -667,6 +667,9 @@ body_section_has_content() { lower = tolower(value) gsub(/^[[:space:]]+|[[:space:]]+$/, "", lower) if (lower == "") return 0 + # Fence delimiters and thematic breaks are structure, not policy content. + if (lower ~ /^(```|~~~)/) return 0 + if (lower ~ /^([-][[:space:]]*){3,}$/ || lower ~ /^([*][[:space:]]*){3,}$/ || lower ~ /^(_[[:space:]]*){3,}$/) return 0 if (allow_placeholders == "true" && lower ~ /^(n\/a|none|no impact)$/) return 1 return lower !~ /^(n\/a|none|pending|todo|tbd|not applicable|unaffected|not affected|not impacted|no impact)$/ } @@ -716,7 +719,6 @@ body_section_has_content() { waiting = 0 next } - if (waiting && $0 ~ /^[[:space:]]*```/) next if (waiting && $0 ~ /[^[:space:]]/) { if ($0 ~ /^[[:space:]]*") if scenario == "validation-html-comment": @@ -1091,9 +1101,13 @@ def test_validation_commands_must_be_concrete_and_in_validation(self): self.assertEqual(result.returncode, 0, result.stdout + result.stderr) def test_empty_fenced_functional_summary_is_not_content(self): - self.assert_blocked("empty-fenced-summary", "non-empty functional summary") - result = self.run_gate("nonempty-fenced-summary") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + for scenario in ("empty-fenced-summary", "empty-tilde-summary", "empty-typed-tilde-summary", "empty-rule-summary", "inline-fence-summary"): + with self.subTest(scenario=scenario): + self.assert_blocked(scenario, "non-empty functional summary") + for scenario in ("nonempty-fenced-summary", "nonempty-tilde-summary"): + with self.subTest(scenario=scenario): + result = self.run_gate(scenario) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) def test_checklist_heading_is_not_a_completed_item(self): self.assert_blocked("checklist-heading", "review-checklist link") From ae12d1b92b34104fc4c6ae04ab1084a11a6c1925 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 13 Sep 2026 12:31:31 +0530 Subject: [PATCH 04/27] fix: fail closed on merge-gate review evidence --- scripts/merge-gate.sh | 335 +++++++++++++++++++------------------ scripts/merge-gate.test.py | 67 ++++++-- scripts/merge_gate_diff.py | 3 + 3 files changed, 230 insertions(+), 175 deletions(-) diff --git a/scripts/merge-gate.sh b/scripts/merge-gate.sh index 9e9c5d85d..c8c57873a 100755 --- a/scripts/merge-gate.sh +++ b/scripts/merge-gate.sh @@ -360,87 +360,29 @@ if [ "$statuses_status" -ne 0 ] || ! jq -e --arg head "$head" ' all(.[]; type == "object" and (.sha | type == "string" and test("^[0-9a-fA-F]{40}$") and . == $head) and (.total_count | type == "number" and floor == . and . >= 0) and - (.state | type == "string" and (. == "success" or . == "pending")) and + (.state | type == "string" and (. == "success" or . == "pending" or . == "failure" or . == "error")) and (.statuses | type == "array" and all(.[]; type == "object" and (.id | type == "number" and floor == . and . >= 0) and (.context | type == "string" and length > 0) and - (.state == "success") + (.state == "success" or .state == "failure" or .state == "error") )) ) and ((map(.total_count) | unique | length) == 1) and ((map(.statuses | length) | add) == .[0].total_count) and ((map(.statuses) | add | map(.id) | unique | length) == .[0].total_count) and ((map(.statuses) | add | map(.context) | unique | length) == .[0].total_count) and - (if .[0].state == "pending" then .[0].total_count == 0 and (map(.statuses | length) | add) == 0 else all(.[]; .state == "success") end) + (if .[0].state == "pending" then .[0].total_count == 0 and (map(.statuses | length) | add) == 0 + elif .[0].state == "failure" or .[0].state == "error" then .[0].state as $aggregate | all(.[]; .state == $aggregate) + else all(.[]; .state == "success") end) ' <"$tmpdir/combined-status-pages.json" >/dev/null 2>&1; then unknown "could not validate complete head-bound commit-status evidence" +elif jq -e 'any(.[]; (.state == "failure" or .state == "error") or any(.statuses[]; .state == "failure" or .state == "error"))' <"$tmpdir/combined-status-pages.json" >/dev/null; then + bad "combined commit-status evidence reports a failure" else say "ok" "complete commit-status pages are bound to head $short" fi -# Provider review objects carry an immutable full commit_id even when the -# human-readable summary is abbreviated. The summary is required as a separate -# completed-run receipt: an exact-head COMMENTED object can remain after a run -# later fails. A clean summary without a full provider OID has an explicit -# operator-attestation path, but a seven-character row never becomes a full-SHA -# claim by inference. -: >"$errfile" -review_status=0 -provider_review="" -reviews=$(gh api --paginate --slurp "repos/$REPO/pulls/$PR/reviews" 2>"$errfile") || review_status=$? -if [ "$review_status" -ne 0 ]; then - unknown "could not read provider review records" -elif ! jq -e 'type == "array" and (all(.[]; type == "array") or all(.[]; type == "object"))' <<<"$reviews" >/dev/null 2>&1; then - unknown "provider review response was malformed" -else - provider_review=$(jq -r --arg head "$head" ' - (if all(.[]; type == "array") then flatten else . end) | - map(select(.user.login == "chatgpt-codex-connector[bot]" and .user.type == "Bot" and - .state == "COMMENTED" and .commit_id == $head)) | - if length > 0 then "matched" else "" end - ' <<<"$reviews") -fi - -: >"$errfile" -comment_status=0 -comments=$(gh api --paginate --slurp "repos/$REPO/issues/$PR/comments" 2>"$errfile") || comment_status=$? -summary_good=0 -if [ "$comment_status" -ne 0 ]; then - unknown "could not read provider review summaries" -elif ! jq -e 'type == "array" and (all(.[]; type == "array") or all(.[]; type == "object"))' <<<"$comments" >/dev/null 2>&1; then - unknown "provider review-summary response was malformed" -else - summary_rows=$(jq -r ' - (if all(.[]; type == "array") then flatten else . end)[] | - select(.user.login == "chatgpt-codex-connector[bot]" and .user.type == "Bot") | - select((.body // "") | contains("codex-pull-request-review-summary")) | - .body - ' <<<"$comments" | grep -E '^\| (📝|🔍)' | tail -1) - if [ -z "$summary_rows" ]; then - bad "no provider review summary row" - elif ! grep -Fq "\`$short\`" <<<"$summary_rows"; then - bad "latest provider summary names a different head than $short" - elif ! grep -Fq 'Completed' <<<"$summary_rows"; then - bad "provider review run for $short is not completed" - else - summary_good=1 - say "ok" "provider review summary is completed on $short" - fi -fi - -if [ "$provider_review" = "matched" ] && [ "$summary_good" -eq 1 ]; then - say "ok" "provider review records the full current head $short" -elif [ "$provider_review" != "matched" ] && [ "$summary_good" -eq 1 ]; then - if [ -n "$INDEPENDENT_REVIEW_SHA" ] && [ "$INDEPENDENT_REVIEW_SHA" = "$head" ]; then - say "ok" "manual independent review attestation names full current head $short" - elif [ -n "$INDEPENDENT_REVIEW_SHA" ]; then - bad "manual independent review attestation does not match the current head" - else - unknown "provider summary exposes only an abbreviated head; pass --independent-review-sha with an exact manual review attestation" - fi -fi - # Scan all published PR metadata. Commit messages are paginated because they # can become squash subjects or release evidence independently of the patch. : >"$errfile" @@ -494,87 +436,6 @@ $raw_prbody $commit_messages" fi -# Paginate review threads and count unresolved nodes over every page. -cursor="" -open_threads=0 -total_threads=-1 -fetched_threads=0 -thread_ids="$tmpdir/review-thread-ids" -: >"$thread_ids" -thread_ok=1 -while :; do - : >"$errfile" - page_status=0 - if [ -z "$cursor" ]; then - page=$(gh api graphql -f owner="$OWNER" -f name="$NAME" -F pr="$PR" -f cursor="" -f query=' - query($owner:String!,$name:String!,$pr:Int!,$cursor:String){ - repository(owner:$owner,name:$name){ - pullRequest(number:$pr){ reviewThreads(first:100,after:$cursor){ - totalCount pageInfo{hasNextPage endCursor} nodes{id isResolved} - }} - } - }' 2>"$errfile") || page_status=$? - else - page=$(gh api graphql -f owner="$OWNER" -f name="$NAME" -F pr="$PR" -f cursor="$cursor" -f query=' - query($owner:String!,$name:String!,$pr:Int!,$cursor:String){ - repository(owner:$owner,name:$name){ - pullRequest(number:$pr){ reviewThreads(first:100,after:$cursor){ - totalCount pageInfo{hasNextPage endCursor} nodes{id isResolved} - }} - } - }' 2>"$errfile") || page_status=$? - fi - if [ "$page_status" -ne 0 ] || ! jq -e '.data.repository.pullRequest.reviewThreads | type == "object" and (.totalCount | type == "number" and floor == . and . >= 0) and (.pageInfo.hasNextPage | type == "boolean") and (.nodes | type == "array" and all(.[]; (.id | type == "string" and length > 0) and (.isResolved | type == "boolean")))' <<<"$page" >/dev/null 2>&1; then - unknown "could not read review threads for $REPO#$PR" - thread_ok=0 - break - fi - page_total=$(jq -r '.data.repository.pullRequest.reviewThreads.totalCount' <<<"$page") - if [ "$total_threads" -eq -1 ]; then - total_threads="$page_total" - elif [ "$page_total" -ne "$total_threads" ]; then - unknown "review-thread totalCount changed during pagination" - thread_ok=0 - break - fi - page_nodes=$(jq '.data.repository.pullRequest.reviewThreads.nodes | length' <<<"$page") - jq -r '.data.repository.pullRequest.reviewThreads.nodes[].id' <<<"$page" >>"$thread_ids" - fetched_threads=$((fetched_threads + page_nodes)) - if [ "$fetched_threads" -gt "$total_threads" ]; then - unknown "review-thread pagination exceeded totalCount" - thread_ok=0 - break - fi - page_open=$(jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length' <<<"$page") - open_threads=$((open_threads + page_open)) - has_next=$(jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage' <<<"$page") - [ "$has_next" = "true" ] || break - if [ "$page_nodes" -eq 0 ]; then - unknown "review-thread pagination returned no nodes while claiming another page" - thread_ok=0 - break - fi - next_cursor=$(jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor // empty' <<<"$page") - if [ -z "$next_cursor" ] || [ "$next_cursor" = "$cursor" ]; then - unknown "review-thread pagination returned no advancing cursor" - thread_ok=0 - break - fi - cursor="$next_cursor" -done -if [ "$thread_ok" -eq 1 ]; then - unique_thread_count=$(sort -u "$thread_ids" | wc -l | tr -d ' ') - if [ "$unique_thread_count" -ne "$fetched_threads" ]; then - unknown "review-thread pagination repeated thread IDs" - elif [ "$fetched_threads" -ne "$total_threads" ]; then - unknown "review-thread pagination returned $fetched_threads of $total_threads nodes" - elif [ "$open_threads" -eq 0 ]; then - say "ok" "0 of $total_threads review threads unresolved" - else - bad "$open_threads of $total_threads review threads unresolved" - fi -fi - # Require a completed checklist item and a same-repository line permalink. # The repository template puts the permalink on the item's indented # continuation, so accept it there as well as in an inline Markdown link. @@ -691,7 +552,7 @@ body_section_has_content() { exit } else if (lower ~ /:[[:space:]]*$/) { pending_list = 2 - } else if (pending_list == 2 && lower ~ /[^[:space:]]/) { + } else if (pending_list && lower ~ /[^[:space:]]/) { if (!template_prompt($0) && lower !~ /^[[:space:]]*", "", lower).strip() - if host not in lower: +placeholders = { + "", "none", "n/a", "not applicable", "unaffected", "not affected", + "not impacted", "no impact", "pending", "todo", "tbd", +} +waiting = False +for raw in sys.stdin.read().splitlines(): + lower = re.sub(r"", "", raw).strip().lower() + if not lower: continue - if re.search(r"(validation|evidence|test|check|unaffected|not applicable|not affected|not impact)", lower): - value = lower.split(":", 1)[1].strip() if ":" in lower else "" - placeholders = { - "none", "n/a", "not applicable", "unaffected", "not affected", - "not impacted", "no impact", "pending", "todo", "tbd", - } - if value and value not in placeholders: + if re.match(r"^#{1,6}\s+", lower) and host not in lower: + waiting = False + continue + if host in lower and re.search(r"(validation|evidence|test|check|unaffected|not applicable|not affected|not impact)", lower): + if ":" not in lower: + if not re.match(r"^#{1,6}\s+", lower) and not re.match(r"^[-*]\s*\[[ xX]\]", lower): + raise SystemExit(0) + else: + value = re.sub(r"^#{1,6}\s+", "", lower.split(":", 1)[1].strip()) + if value not in placeholders: + raise SystemExit(0) + waiting = True + continue + if waiting: + if re.match(r"^[-*]\s+", lower) and re.search(r"(validation|evidence|test|check|unaffected|not applicable|not affected|not impact)", lower): + waiting = False + continue + if (not re.match(r"^[-*]\s*\[[ xX]\]", lower) and + not lower.startswith("|\Z)", "", text, flags=re.S), end="")' <<<"$final_body") || final_visible_status=$? [ "$final_visible_status" -eq 0 ] || unknown "could not extract final visible PR description content" [ "$final_title" = "$title" ] || bad "PR title changed during preflight" - if ! checklist_link_ok "$final_body" "$review_checklist"; then + if [ "$checklist_status" -eq 0 ] && ! checklist_link_ok "$final_body" "$review_checklist"; then bad "PR description changed and no longer carries a completed same-repository line-specific checklist link" fi if [ "$raw_final_body" != "$raw_prbody" ]; then diff --git a/scripts/merge-gate.test.py b/scripts/merge-gate.test.py index 7af5d37f2..0bc5a077f 100644 --- a/scripts/merge-gate.test.py +++ b/scripts/merge-gate.test.py @@ -169,13 +169,22 @@ def fail(message="controlled API failure"): body = body.replace("#L10", "#L1") if scenario == "checklist-anchor-suffix": body = body.replace("#L10", "#L10junk") - if scenario in {"implementation-p4-present", "platform-evidence-present", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "migration-rollback-present", "migration-template-wrapped", "security-notes-present", "security-none", "security-pending", "security-review-valid"}: + if scenario in {"implementation-p4-present", "implementation-p4-continuation", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "migration-rollback-present", "migration-template-wrapped", "security-notes-present", "security-none", "security-pending", "security-review-valid"}: body += ( "\n## Scope, reuse, and impact\n\n" "- Existing component reused: the existing gate parser and file inventory.\n" "- What is deleted (or why no deletion is justified): no duplicate path remains.\n" "- What breaks if this is not built: unsafe evidence could reach a merge.\n" ) + if scenario == "implementation-p4-continuation": + body = body.replace( + "- Existing component reused: the existing gate parser and file inventory.\n" + "- What is deleted (or why no deletion is justified): no duplicate path remains.\n" + "- What breaks if this is not built: unsafe evidence could reach a merge.\n", + "- Existing component reused:\n the existing gate parser and file inventory.\n" + "- What is deleted (or why no deletion is justified):\n no duplicate path remains.\n" + "- What breaks if this is not built:\n unsafe evidence could reach a merge.\n", + ) if security_case or scenario in {"surface-unpins", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"}: body += ( "\n## Scope, reuse, and impact\n\n" @@ -187,11 +196,18 @@ def fail(message="controlled API failure"): "\n- Windows validation evidence: Windows CI ran `python3 scripts/merge-gate.test.py`.\n" "- macOS validation evidence: macOS CI ran `python3 scripts/merge-gate.test.py`.\n" ) - if scenario in {"platform-evidence-present", "migration-template-wrapped", "security-notes-present", "security-none", "security-pending", "security-review-valid", "sync-migration-present"}: + if scenario in {"platform-evidence-present", "platform-powershell-evidence", "migration-template-wrapped", "security-notes-present", "security-none", "security-pending", "security-review-valid", "sync-migration-present"}: body += ( "\n- Windows validation evidence: Windows CI ran `python3 scripts/merge-gate.test.py`.\n" "- macOS validation evidence: macOS CI ran `python3 scripts/merge-gate.test.py`.\n" ) + if scenario == "platform-evidence-heading": + body += ( + "\n### Windows validation evidence\n\n" + "`python3 scripts/merge-gate.test.py` passed on Windows CI.\n" + "\n### macOS validation evidence\n\n" + "`python3 scripts/merge-gate.test.py` passed on macOS CI.\n" + ) if scenario == "platform-checkbox-evidence": body += ( "\n- [x] Native Windows validation completed: `python3 scripts/merge-gate.test.py` passed on Windows CI.\n" @@ -206,7 +222,7 @@ def fail(message="controlled API failure"): body += "\n- Windows validation evidence: unaffected\n- macOS validation evidence: unaffected\n" if scenario == "platform-unaffected-rationale": body += "\n- Windows validation evidence: unaffected because this changes shared documentation only.\n- macOS validation evidence: unaffected because this changes shared documentation only.\n" - if scenario in {"platform-evidence-present", "platform-checkbox-evidence", "platform-unaffected-bare", "platform-unaffected-rationale"}: + if scenario in {"platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-unaffected-bare", "platform-unaffected-rationale"}: body += ( "\n## Security impact\n\nNo credential material is added.\n" "\n## Migration compatibility\n\nExisting callers retain their paths and formats.\n" @@ -236,7 +252,7 @@ def fail(message="controlled API failure"): body = body.replace("blob/HEAD", f"blob/{head}") if scenario == "checklist-stale-ref": body = body.replace(f"blob/{head}", "blob/" + "f" * 40) - one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case + one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "gitlink-existing", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case selected_base = new_head if scenario == "base-oid-mismatch" else base emit({"headRefOid": selected_head, "baseRefOid": selected_base, "baseRefName": "master", "mergeable": "MERGEABLE", "mergeStateStatus": final_state, @@ -348,11 +364,15 @@ def fail(message="controlled API failure"): emit("diff --git a/docs/a b/example.md b/docs/a b/example.md\nsimilarity index 100%\nrename from docs/a b/example.md\nrename to docs/a b/example.md\n") elif scenario == "gitlink": emit("diff --git a/vendor/module b/vendor/module\nnew file mode 160000\nindex 0000000..2222222\n--- /dev/null\n+++ b/vendor/module\n@@ -0,0 +1 @@\n+Subproject commit 2222222\n") - elif scenario in {"implementation-p4-missing", "implementation-p4-present", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders"}: + elif scenario == "gitlink-existing": + emit("diff --git a/vendor/module b/vendor/module\nindex 1111111..2222222 160000\n--- a/vendor/module\n+++ b/vendor/module\n@@ -1 +1 @@\n-Subproject commit 1111111\n+Subproject commit 2222222\n") + elif scenario in {"implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders"}: suffix = {"implementation-p4-shell": "sh", "implementation-p4-powershell": "ps1", "implementation-p4-sql": "sql"}.get(scenario, "py") emit(f"diff --git a/scripts/example.{suffix} b/scripts/example.{suffix}\n--- a/scripts/example.{suffix}\n+++ b/scripts/example.{suffix}\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale"}: + elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale"}: emit("diff --git a/src-tauri/src/local_files/paths.rs b/src-tauri/src/local_files/paths.rs\n--- a/src-tauri/src/local_files/paths.rs\n+++ b/src-tauri/src/local_files/paths.rs\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario in {"platform-powershell-missing", "platform-powershell-evidence"}: + emit("diff --git a/scripts/signing.ps1 b/scripts/signing.ps1\n--- a/scripts/signing.ps1\n+++ b/scripts/signing.ps1\n@@ -0,0 +1 @@\n+safe text\n") elif scenario in {"migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"}: emit("diff --git a/src-tauri/migrations/001.sql b/src-tauri/migrations/001.sql\n--- a/src-tauri/migrations/001.sql\n+++ b/src-tauri/migrations/001.sql\n@@ -0,0 +1 @@\n+safe text\n") elif scenario == "separated-dates": @@ -438,7 +458,7 @@ def status_page(rows, total_count, state="success", page_head=head): elif scenario == "status-malformed": emit([{"sha": head, "state": "success", "total_count": "0", "statuses": []}]) elif scenario == "status-failed-combined": - emit([{"sha": head, "state": "failure", "total_count": 0, "statuses": []}]) + emit([{"sha": head, "state": "failure", "total_count": 1, "statuses": [{"id": 1, "context": "legacy failed", "state": "failure"}]}]) else: emit([{"sha": head, "state": "success", "total_count": 0, "statuses": []}]) elif "/pulls/321/commits" in joined: @@ -563,13 +583,15 @@ def status_page(rows, total_count, state="success", page_head=head): record = {"filename": "docs/a b/example.md" if scenario != "crlf-diff" else "docs/example.md", "status": status, "additions": 0 if status == "renamed" else 1, "deletions": 0} if status == "renamed": record["previous_filename"] = "docs/a b/example.md" emit([[record]]) - elif scenario == "gitlink": + elif scenario in {"gitlink", "gitlink-existing"}: emit([[{"filename": "vendor/module", "status": "modified", "additions": 1, "deletions": 1}]]) - elif scenario in {"implementation-p4-missing", "implementation-p4-present", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders"}: + elif scenario in {"implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders"}: suffix = {"implementation-p4-shell": "sh", "implementation-p4-powershell": "ps1", "implementation-p4-sql": "sql"}.get(scenario, "py") emit([[{"filename": f"scripts/example.{suffix}", "status": "modified", "additions": 1, "deletions": 0}]]) - elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale"}: + elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale"}: emit([[{"filename": "src-tauri/src/local_files/paths.rs", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario in {"platform-powershell-missing", "platform-powershell-evidence"}: + emit([[{"filename": "scripts/signing.ps1", "status": "modified", "additions": 1, "deletions": 0}]]) elif scenario in {"migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"}: emit([[{"filename": "src-tauri/migrations/001.sql", "status": "modified", "additions": 1, "deletions": 0}]]) elif scenario == "control-path": @@ -588,6 +610,8 @@ def status_page(rows, total_count, state="success", page_head=head): emit([[{"filename": "docs/example.md", "status": "added", "additions": additions, "deletions": 0}], [{"filename": "docs/second.md", "status": "modified", "additions": 1, "deletions": 0}]]) elif "/contents/" in joined: if "review-checklist.md" in joined: + if scenario == "checklist-fetch-fail": + fail("controlled checklist read failure") checklist = "# Review checklist\n" + "\n" * 8 + "- [ ] Errors are actionable without exposing sensitive values.\n" emit({"encoding": "base64", "content": base64.b64encode(checklist.encode()).decode()}) elif scenario in {"surface-fail", "draft-surface-fail"}: @@ -956,10 +980,10 @@ def test_documented_required_context_cannot_be_omitted(self): self.assert_blocked("protection-missing-gitguardian", "branch protection omits 1 documented") def test_failing_combined_commit_status_blocks(self): - self.assert_indeterminate("status-failed-combined", "head-bound commit-status evidence") + self.assert_blocked("status-failed-combined", "combined commit-status evidence reports a failure") def test_failing_individual_commit_status_blocks(self): - self.assert_indeterminate("status-failed-context", "head-bound commit-status evidence") + self.assert_blocked("status-failed-context", "combined commit-status evidence reports a failure") def test_duplicate_thread_ids_are_indeterminate(self): self.assert_indeterminate("threads-duplicate-id", "pagination repeated thread IDs") @@ -1014,7 +1038,9 @@ def test_crlf_and_ambiguous_unquoted_diffs_preserve_coverage(self): self.assertEqual(result.returncode, 0, result.stdout + result.stderr) def test_gitlink_requires_explicit_provenance_license_notice_review(self): - self.assert_indeterminate("gitlink", "gitlink change(s) require explicit provenance, license, and NOTICE review") + for scenario in ("gitlink", "gitlink-existing"): + with self.subTest(scenario=scenario): + self.assert_indeterminate(scenario, "gitlink change(s) require explicit provenance, license, and NOTICE review") def test_control_character_in_destination_path_is_indeterminate(self): self.assert_indeterminate("control-path", "could not read the complete changed-file set") @@ -1146,11 +1172,15 @@ def test_implementation_additions_need_all_three_p4_answers(self): self.assert_blocked("p4-placeholders", "all three substantive P4") result = self.run_gate("implementation-p4-present") self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + result = self.run_gate("implementation-p4-continuation") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) def test_platform_sensitive_paths_need_substantive_both_host_evidence(self): self.assert_blocked("platform-evidence-missing", "substantive Windows validation") result = self.run_gate("platform-evidence-present") self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + result = self.run_gate("platform-evidence-heading") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) result = self.run_gate("platform-checkbox-evidence") self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assert_blocked("platform-checkbox-comment", "substantive Windows validation") @@ -1158,6 +1188,17 @@ def test_platform_sensitive_paths_need_substantive_both_host_evidence(self): result = self.run_gate("platform-unaffected-rationale") self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_powershell_paths_need_both_host_evidence(self): + self.assert_blocked("platform-powershell-missing", "substantive Windows validation") + result = self.run_gate("platform-powershell-evidence") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_checklist_fetch_failure_stays_indeterminate(self): + result = self.run_gate("checklist-fetch-fail") + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("could not read review-checklist content", result.stdout) + self.assertNotIn("description changed and no longer carries a completed", result.stdout) + def test_database_migration_paths_need_rollback_notes(self): self.assert_blocked("migration-rollback-missing", "database migration path lacks non-empty rollback notes") result = self.run_gate("migration-rollback-present") diff --git a/scripts/merge_gate_diff.py b/scripts/merge_gate_diff.py index 165e56bb6..ea36537a1 100755 --- a/scripts/merge_gate_diff.py +++ b/scripts/merge_gate_diff.py @@ -175,6 +175,9 @@ def emit() -> None: }: record["gitlink"] = True continue + if not record["in_hunk"] and line.startswith("index ") and line.endswith(" 160000"): + record["gitlink"] = True + continue if line.startswith("@@ "): record["in_hunk"] = True continue From 914ea74b94a1514aad2ad842fe45467788fb3ac9 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 13 Sep 2026 13:33:11 +0530 Subject: [PATCH 05/27] Harden merge-gate review evidence --- scripts/merge-gate.sh | 60 +++++++++++++++++++++----------------- scripts/merge-gate.test.py | 34 +++++++++++++++------ 2 files changed, 58 insertions(+), 36 deletions(-) diff --git a/scripts/merge-gate.sh b/scripts/merge-gate.sh index c8c57873a..22fdf5426 100755 --- a/scripts/merge-gate.sh +++ b/scripts/merge-gate.sh @@ -527,6 +527,10 @@ body_section_has_content() { gsub(//, "", value) lower = tolower(value) gsub(/^[[:space:]]+|[[:space:]]+$/, "", lower) + # A terminal dot or similar punctuation does not make an otherwise empty + # template answer substantive (for example, `N/A.` or `TBD.`). + gsub(/[,.;:!?]+$/, "", lower) + gsub(/[[:space:]]+$/, "", lower) if (lower == "") return 0 # Fence delimiters and thematic breaks are structure, not policy content. if (lower ~ /^(```|~~~)/) return 0 @@ -614,7 +618,7 @@ for line in text.splitlines(): continue if not active: continue - if re.match(r"^\s*```", line): + if re.match(r"^\s*(?:```|~~~)", line): fenced = not fenced continue candidates = [(line.strip()[2:] if line.strip().startswith("$ ") else line.strip())] if fenced else re.findall(r"`([^`]+)`", line) @@ -648,9 +652,8 @@ body_has_p4_answers() { body_has_platform_evidence() { local body="$1" host="$2" - # A bare checked template box has no host, command, or rationale. Require - # a filled heading/list field that names the host and either evidence or a - # justified unaffected statement. + # A named host field must include evidence on that line or in a following + # content line. A bare list label is only a template prompt, not evidence. python3 -c ' import re, sys host = sys.argv[1].lower() @@ -658,32 +661,32 @@ placeholders = { "", "none", "n/a", "not applicable", "unaffected", "not affected", "not impacted", "no impact", "pending", "todo", "tbd", } +marker = r"(validation|evidence|test|check|unaffected|not applicable|not affected|not impact)" waiting = False for raw in sys.stdin.read().splitlines(): - lower = re.sub(r"", "", raw).strip().lower() + lower = re.sub(r"|$)", "", raw).strip().lower() if not lower: continue - if re.match(r"^#{1,6}\s+", lower) and host not in lower: - waiting = False - continue - if host in lower and re.search(r"(validation|evidence|test|check|unaffected|not applicable|not affected|not impact)", lower): - if ":" not in lower: - if not re.match(r"^#{1,6}\s+", lower) and not re.match(r"^[-*]\s*\[[ xX]\]", lower): - raise SystemExit(0) - else: - value = re.sub(r"^#{1,6}\s+", "", lower.split(":", 1)[1].strip()) - if value not in placeholders: - raise SystemExit(0) - waiting = True - continue + is_heading = bool(re.match(r"^#{1,6}\s+", lower)) + is_field = host in lower and bool(re.search(marker, lower)) if waiting: - if re.match(r"^[-*]\s+", lower) and re.search(r"(validation|evidence|test|check|unaffected|not applicable|not affected|not impact)", lower): + # A new evidence field or heading ends the preceding empty field; it + # cannot be treated as the earlier host evidence. + if is_heading or (re.match(r"^[-*]\s+", lower) and re.search(marker, lower)): waiting = False - continue - if (not re.match(r"^[-*]\s*\[[ xX]\]", lower) and - not lower.startswith("|$))"; ""); def focused: (.body | type == "string") and - (.body | test("(?im)^#{0,6} *security review: *" + $head + " *$")) and - (.body | test("(?im)^result: *accepted *$")); + (visible_body | test("(?im)^#{0,6} *security review: *" + $head + " *$")) and + (visible_body | test("(?im)^result: *accepted *$")) and + (visible_body | test("(?im)^#{0,6} *reviewed (dsc|credential|certificate|keystore|secret)")); ([($reviews | source_records | records)[] | select(reviewer and focused and .commit_id == $head and (.state == "APPROVED" or .state == "COMMENTED"))] + [($comments | source_records | records)[] | select(reviewer and focused)]) | length > 0 @@ -1056,8 +1061,9 @@ $added" home_path_status=0 mac_home='/'"Users"'/[A-Za-z0-9._-]+' unix_home='/'"home"'/[A-Za-z0-9._-]+' + root_home='/'"root" windows_home='[A-Za-z]:[\\/]{1,2}'"Users"'[\\/]{1,2}[A-Za-z0-9._-]+' - home_path_matches=$(grep -Eio "(^|[^[:alnum:]_])(${mac_home}|${unix_home}|${windows_home})(\$|/|\\\\|[^[:alnum:]_.-])" <<<"$scan_input") || home_path_status=$? + home_path_matches=$(grep -Eio "(^|[^[:alnum:]_])(${mac_home}|${unix_home}|${root_home}|${windows_home})(\$|/|\\\\|[^[:alnum:]_.-])" <<<"$scan_input") || home_path_status=$? if [ "$home_path_status" -gt 1 ]; then unknown "developer-home path scan expression failed" elif [ "$home_path_status" -eq 0 ]; then diff --git a/scripts/merge-gate.test.py b/scripts/merge-gate.test.py index 0bc5a077f..3992d124c 100644 --- a/scripts/merge-gate.test.py +++ b/scripts/merge-gate.test.py @@ -21,7 +21,7 @@ import base64, json, os, sys args = sys.argv[1:] scenario = os.environ.get("GATE_SCENARIO", "pass") -security_case = scenario.startswith("security-review-") +security_case = scenario.startswith("security-review-") or scenario in {"security-camel-dsc", "security-camel-credential"} sync_case = scenario.startswith("sync-") head = "0123456789abcdef0123456789abcdef01234567" new_head = "fedcba9876543210fedcba9876543210fedcba98" @@ -137,6 +137,8 @@ def fail(message="controlled API failure"): body = body.replace("`python3 scripts/merge-gate.test.py`", "Tests not run") + "\n## Rollback\n`cargo test`\n" if scenario == "validation-node-command": body = body.replace("`python3 scripts/merge-gate.test.py`", "`node --test scripts/prune-package-compiler-cache.test.mjs`") + if scenario == "validation-tilde-command": + body = body.replace("`python3 scripts/merge-gate.test.py`", "~~~bash\npython3 scripts/merge-gate.test.py\n~~~") structural_summaries = { "empty-fenced-summary": "```\n```", "empty-tilde-summary": "~~~\n~~~", @@ -249,10 +251,14 @@ def fail(message="controlled API failure"): ) if scenario == "workflow-placeholders": body += "\n## Rollback notes\n\nN/A\n\n## Migration compatibility\n\nTBD\n" + if scenario == "workflow-punctuated-placeholders": + body += "\n## Rollback notes\n\nN/A.\n\n## Migration compatibility\n\nTBD.\n" + if scenario == "platform-evidence-bare-label": + body += "\n- Windows validation evidence\n- macOS validation evidence\n" body = body.replace("blob/HEAD", f"blob/{head}") if scenario == "checklist-stale-ref": body = body.replace(f"blob/{head}", "blob/" + "f" * 40) - one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "gitlink-existing", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case + one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "workflow-punctuated-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "gitlink-existing", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case selected_base = new_head if scenario == "base-oid-mismatch" else base emit({"headRefOid": selected_head, "baseRefOid": selected_base, "baseRefName": "master", "mergeable": "MERGEABLE", "mergeStateStatus": final_state, @@ -293,7 +299,7 @@ def fail(message="controlled API failure"): elif scenario == "security-rename-out": emit("diff --git a/src-tauri/src/tally/runtime.rs b/src/runtime.rs\nsimilarity index 100%\nrename from src-tauri/src/tally/runtime.rs\nrename to src/runtime.rs\n") elif scenario.startswith("home-"): - homes = {"home-macos": "/" + "Users" + "/" + "tester" + "/work", "home-unix": "/" + "home" + "/" + "tester" + "/work", "home-windows": "C:" + "\\" + "Users" + "\\" + "tester" + "\\work", "home-macos-root": "/" + "Users" + "/" + "tester", "home-unix-root": "/" + "home" + "/" + "tester", "home-windows-forward": "C:" + "/" + "Users" + "/" + "tester" + "/work", "home-windows-escaped": "C:" + "\\\\" + "Users" + "\\\\" + "tester" + "\\\\work", "home-regex-source": "mac_home=" + "'/'" + '"Users"' + "'/[A-Za-z0-9._-]+'"} + homes = {"home-macos": "/" + "Users" + "/" + "tester" + "/work", "home-unix": "/" + "home" + "/" + "tester" + "/work", "home-root": "/" + "root" + "/work/customer.pem", "home-root-home": "/" + "root", "home-windows": "C:" + "\\" + "Users" + "\\" + "tester" + "\\work", "home-macos-root": "/" + "Users" + "/" + "tester", "home-unix-root": "/" + "home" + "/" + "tester", "home-windows-forward": "C:" + "/" + "Users" + "/" + "tester" + "/work", "home-windows-escaped": "C:" + "\\\\" + "Users" + "\\\\" + "tester" + "\\\\work", "home-regex-source": "mac_home=" + "'/'" + '"Users"' + "'/[A-Za-z0-9._-]+'"} emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{homes[scenario]}\n") elif scenario == "binary-delete": emit("diff --git a/docs/old.png b/docs/old.png\nBinary files a/docs/old.png and /dev/null differ\n") @@ -503,11 +509,14 @@ def status_page(rows, total_count, state="success", page_head=head): emit([[]]) else: records = [{"user": {"login": "chatgpt-codex-connector[bot]", "type": "Bot"}, "state": "COMMENTED", "commit_id": head}] - if security_case and scenario != "security-review-missing": + if security_case and scenario not in {"security-review-missing", "security-camel-dsc", "security-camel-credential"}: record = {"user": {"login": "reviewer", "type": "User"}, "author_association": "COLLABORATOR", "state": "COMMENTED", "commit_id": head, "body": f"Security review: {head}\nResult: accepted\nReviewed credential handling and error redaction."} if scenario == "security-review-stale": record["commit_id"] = new_head if scenario == "security-review-author": record["user"]["login"] = "author" if scenario == "security-review-unrelated": record["body"] = "Looks good" + if scenario == "security-review-no-scope": record["body"] = f"Security review: {head}\nResult: accepted" + if scenario == "security-review-hidden": record["body"] = f"" + if scenario == "security-review-hidden-unterminated": record["body"] = f"|$)", "", raw).strip().lower() if not lower: continue + is_fence = bool(re.match(r"^(?:```|~~~)", lower)) is_heading = bool(re.match(r"^#{1,6}\s+", lower)) is_field = host in lower and bool(re.search(marker, lower)) if waiting: + # Empty fence delimiters carry no validation result. Keep looking so a + # real fenced command can still establish the named host evidence. + if is_fence: + continue # A new evidence field or heading ends the preceding empty field; it # cannot be treated as the earlier host evidence. if is_heading or (re.match(r"^[-*]\s+", lower) and re.search(marker, lower)): waiting = False - elif lower not in placeholders and not re.match(r"^[-*]\s*\[[ xX]\]", lower): + elif continuation_evidence(lower): raise SystemExit(0) else: waiting = False if not is_field: continue if ":" in lower: - value = re.sub(r"^#{1,6}\s+", "", lower.split(":", 1)[1].strip()) + value = normalize(re.sub(r"^#{1,6}\s+", "", lower.split(":", 1)[1])) if value not in placeholders: raise SystemExit(0) # Headings and list labels without an inline answer may be completed by a - # following substantive line. All other bare forms follow the same rule. + # following substantive validation result. All other bare forms fail. waiting = True raise SystemExit(1) ' "$host" <<<"$body" diff --git a/scripts/merge-gate.test.py b/scripts/merge-gate.test.py index 3992d124c..c0e0f24e2 100644 --- a/scripts/merge-gate.test.py +++ b/scripts/merge-gate.test.py @@ -171,7 +171,7 @@ def fail(message="controlled API failure"): body = body.replace("#L10", "#L1") if scenario == "checklist-anchor-suffix": body = body.replace("#L10", "#L10junk") - if scenario in {"implementation-p4-present", "implementation-p4-continuation", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "migration-rollback-present", "migration-template-wrapped", "security-notes-present", "security-none", "security-pending", "security-review-valid"}: + if scenario in {"implementation-p4-present", "implementation-p4-continuation", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "migration-rollback-present", "migration-template-wrapped", "security-notes-present", "security-none", "security-pending", "security-review-valid"}: body += ( "\n## Scope, reuse, and impact\n\n" "- Existing component reused: the existing gate parser and file inventory.\n" @@ -224,7 +224,7 @@ def fail(message="controlled API failure"): body += "\n- Windows validation evidence: unaffected\n- macOS validation evidence: unaffected\n" if scenario == "platform-unaffected-rationale": body += "\n- Windows validation evidence: unaffected because this changes shared documentation only.\n- macOS validation evidence: unaffected because this changes shared documentation only.\n" - if scenario in {"platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-unaffected-bare", "platform-unaffected-rationale"}: + if scenario in {"platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation"}: body += ( "\n## Security impact\n\nNo credential material is added.\n" "\n## Migration compatibility\n\nExisting callers retain their paths and formats.\n" @@ -255,10 +255,18 @@ def fail(message="controlled API failure"): body += "\n## Rollback notes\n\nN/A.\n\n## Migration compatibility\n\nTBD.\n" if scenario == "platform-evidence-bare-label": body += "\n- Windows validation evidence\n- macOS validation evidence\n" + if scenario == "platform-evidence-sibling-list": + body += "\n- Windows validation evidence\n- Notes: tracked separately\n- macOS validation evidence\n- Notes: tracked separately\n" + if scenario == "platform-evidence-empty-fence": + body += "\n### Windows validation evidence\n~~~bash\n~~~\n### macOS validation evidence\n~~~bash\n~~~\n" + if scenario == "platform-evidence-punctuated-placeholder": + body += "\n- Windows validation evidence: N/A.\n- macOS validation evidence: TBD.\n" + if scenario == "platform-evidence-fenced-continuation": + body += "\n### Windows validation evidence\n~~~bash\npython3 scripts/merge-gate.test.py\n~~~\n### macOS validation evidence\n~~~bash\npython3 scripts/merge-gate.test.py\n~~~\n" body = body.replace("blob/HEAD", f"blob/{head}") if scenario == "checklist-stale-ref": body = body.replace(f"blob/{head}", "blob/" + "f" * 40) - one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "workflow-punctuated-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "gitlink-existing", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case + one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "workflow-punctuated-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "gitlink-existing", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case selected_base = new_head if scenario == "base-oid-mismatch" else base emit({"headRefOid": selected_head, "baseRefOid": selected_base, "baseRefName": "master", "mergeable": "MERGEABLE", "mergeStateStatus": final_state, @@ -375,7 +383,7 @@ def fail(message="controlled API failure"): elif scenario in {"implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders"}: suffix = {"implementation-p4-shell": "sh", "implementation-p4-powershell": "ps1", "implementation-p4-sql": "sql"}.get(scenario, "py") emit(f"diff --git a/scripts/example.{suffix} b/scripts/example.{suffix}\n--- a/scripts/example.{suffix}\n+++ b/scripts/example.{suffix}\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale"}: + elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation"}: emit("diff --git a/src-tauri/src/local_files/paths.rs b/src-tauri/src/local_files/paths.rs\n--- a/src-tauri/src/local_files/paths.rs\n+++ b/src-tauri/src/local_files/paths.rs\n@@ -0,0 +1 @@\n+safe text\n") elif scenario in {"platform-powershell-missing", "platform-powershell-evidence"}: emit("diff --git a/scripts/signing.ps1 b/scripts/signing.ps1\n--- a/scripts/signing.ps1\n+++ b/scripts/signing.ps1\n@@ -0,0 +1 @@\n+safe text\n") @@ -600,7 +608,7 @@ def status_page(rows, total_count, state="success", page_head=head): elif scenario in {"implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders"}: suffix = {"implementation-p4-shell": "sh", "implementation-p4-powershell": "ps1", "implementation-p4-sql": "sql"}.get(scenario, "py") emit([[{"filename": f"scripts/example.{suffix}", "status": "modified", "additions": 1, "deletions": 0}]]) - elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label"}: + elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation"}: emit([[{"filename": "src-tauri/src/local_files/paths.rs", "status": "modified", "additions": 1, "deletions": 0}]]) elif scenario in {"platform-powershell-missing", "platform-powershell-evidence"}: emit([[{"filename": "scripts/signing.ps1", "status": "modified", "additions": 1, "deletions": 0}]]) @@ -1201,6 +1209,11 @@ def test_platform_sensitive_paths_need_substantive_both_host_evidence(self): self.assert_blocked("platform-checkbox-comment", "substantive Windows validation") self.assert_blocked("platform-unaffected-bare", "substantive Windows validation") self.assert_blocked("platform-evidence-bare-label", "substantive Windows validation") + self.assert_blocked("platform-evidence-sibling-list", "substantive Windows validation") + self.assert_blocked("platform-evidence-empty-fence", "substantive Windows validation") + self.assert_blocked("platform-evidence-punctuated-placeholder", "substantive Windows validation") + result = self.run_gate("platform-evidence-fenced-continuation") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) result = self.run_gate("platform-unaffected-rationale") self.assertEqual(result.returncode, 0, result.stdout + result.stderr) From d3d5bd89bbd4c224f5c4c4231fd5bea29cc5e492 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 13 Sep 2026 14:07:27 +0530 Subject: [PATCH 07/27] Bound platform evidence list continuations --- scripts/merge-gate.sh | 12 ++++++++++-- scripts/merge-gate.test.py | 11 +++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/scripts/merge-gate.sh b/scripts/merge-gate.sh index 79616cd93..dee08fd3b 100755 --- a/scripts/merge-gate.sh +++ b/scripts/merge-gate.sh @@ -673,11 +673,15 @@ def continuation_evidence(value): unaffected = re.search(r"\b(?:unaffected|not affected|not impacted)\b", value) and re.search(r"\b(?:because|as|this)\b", value) return bool(command or outcome or unaffected) waiting = False +waiting_list_indent = None for raw in sys.stdin.read().splitlines(): - lower = re.sub(r"|$)", "", raw).strip().lower() + visible = re.sub(r"|$)", "", raw) + indent = len(visible) - len(visible.lstrip(" \t")) + lower = visible.strip().lower() if not lower: continue is_fence = bool(re.match(r"^(?:```|~~~)", lower)) + is_list_item = bool(re.match(r"^[-*]\s+", lower)) is_heading = bool(re.match(r"^#{1,6}\s+", lower)) is_field = host in lower and bool(re.search(marker, lower)) if waiting: @@ -687,12 +691,15 @@ for raw in sys.stdin.read().splitlines(): continue # A new evidence field or heading ends the preceding empty field; it # cannot be treated as the earlier host evidence. - if is_heading or (re.match(r"^[-*]\s+", lower) and re.search(marker, lower)): + if ((waiting_list_indent is not None and is_list_item and indent <= waiting_list_indent) or + is_heading or (is_list_item and re.search(marker, lower))): waiting = False + waiting_list_indent = None elif continuation_evidence(lower): raise SystemExit(0) else: waiting = False + waiting_list_indent = None if not is_field: continue if ":" in lower: @@ -702,6 +709,7 @@ for raw in sys.stdin.read().splitlines(): # Headings and list labels without an inline answer may be completed by a # following substantive validation result. All other bare forms fail. waiting = True + waiting_list_indent = indent if is_list_item else None raise SystemExit(1) ' "$host" <<<"$body" } diff --git a/scripts/merge-gate.test.py b/scripts/merge-gate.test.py index c0e0f24e2..cc59b3663 100644 --- a/scripts/merge-gate.test.py +++ b/scripts/merge-gate.test.py @@ -224,7 +224,7 @@ def fail(message="controlled API failure"): body += "\n- Windows validation evidence: unaffected\n- macOS validation evidence: unaffected\n" if scenario == "platform-unaffected-rationale": body += "\n- Windows validation evidence: unaffected because this changes shared documentation only.\n- macOS validation evidence: unaffected because this changes shared documentation only.\n" - if scenario in {"platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation"}: + if scenario in {"platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager"}: body += ( "\n## Security impact\n\nNo credential material is added.\n" "\n## Migration compatibility\n\nExisting callers retain their paths and formats.\n" @@ -257,6 +257,8 @@ def fail(message="controlled API failure"): body += "\n- Windows validation evidence\n- macOS validation evidence\n" if scenario == "platform-evidence-sibling-list": body += "\n- Windows validation evidence\n- Notes: tracked separately\n- macOS validation evidence\n- Notes: tracked separately\n" + if scenario == "platform-evidence-package-manager": + body += "\n- Windows validation evidence\n- Package manager: pnpm\n- macOS validation evidence\n- Package manager: pnpm\n" if scenario == "platform-evidence-empty-fence": body += "\n### Windows validation evidence\n~~~bash\n~~~\n### macOS validation evidence\n~~~bash\n~~~\n" if scenario == "platform-evidence-punctuated-placeholder": @@ -266,7 +268,7 @@ def fail(message="controlled API failure"): body = body.replace("blob/HEAD", f"blob/{head}") if scenario == "checklist-stale-ref": body = body.replace(f"blob/{head}", "blob/" + "f" * 40) - one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "workflow-punctuated-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "gitlink-existing", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case + one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "workflow-punctuated-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "gitlink-existing", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case selected_base = new_head if scenario == "base-oid-mismatch" else base emit({"headRefOid": selected_head, "baseRefOid": selected_base, "baseRefName": "master", "mergeable": "MERGEABLE", "mergeStateStatus": final_state, @@ -383,7 +385,7 @@ def fail(message="controlled API failure"): elif scenario in {"implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders"}: suffix = {"implementation-p4-shell": "sh", "implementation-p4-powershell": "ps1", "implementation-p4-sql": "sql"}.get(scenario, "py") emit(f"diff --git a/scripts/example.{suffix} b/scripts/example.{suffix}\n--- a/scripts/example.{suffix}\n+++ b/scripts/example.{suffix}\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation"}: + elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager"}: emit("diff --git a/src-tauri/src/local_files/paths.rs b/src-tauri/src/local_files/paths.rs\n--- a/src-tauri/src/local_files/paths.rs\n+++ b/src-tauri/src/local_files/paths.rs\n@@ -0,0 +1 @@\n+safe text\n") elif scenario in {"platform-powershell-missing", "platform-powershell-evidence"}: emit("diff --git a/scripts/signing.ps1 b/scripts/signing.ps1\n--- a/scripts/signing.ps1\n+++ b/scripts/signing.ps1\n@@ -0,0 +1 @@\n+safe text\n") @@ -608,7 +610,7 @@ def status_page(rows, total_count, state="success", page_head=head): elif scenario in {"implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders"}: suffix = {"implementation-p4-shell": "sh", "implementation-p4-powershell": "ps1", "implementation-p4-sql": "sql"}.get(scenario, "py") emit([[{"filename": f"scripts/example.{suffix}", "status": "modified", "additions": 1, "deletions": 0}]]) - elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation"}: + elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager"}: emit([[{"filename": "src-tauri/src/local_files/paths.rs", "status": "modified", "additions": 1, "deletions": 0}]]) elif scenario in {"platform-powershell-missing", "platform-powershell-evidence"}: emit([[{"filename": "scripts/signing.ps1", "status": "modified", "additions": 1, "deletions": 0}]]) @@ -1210,6 +1212,7 @@ def test_platform_sensitive_paths_need_substantive_both_host_evidence(self): self.assert_blocked("platform-unaffected-bare", "substantive Windows validation") self.assert_blocked("platform-evidence-bare-label", "substantive Windows validation") self.assert_blocked("platform-evidence-sibling-list", "substantive Windows validation") + self.assert_blocked("platform-evidence-package-manager", "substantive Windows validation") self.assert_blocked("platform-evidence-empty-fence", "substantive Windows validation") self.assert_blocked("platform-evidence-punctuated-placeholder", "substantive Windows validation") result = self.run_gate("platform-evidence-fenced-continuation") From 2023ab4b4643198e462a036d4469cfde2314e8a4 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 13 Sep 2026 16:10:00 +0530 Subject: [PATCH 08/27] Avoid literal home paths in gate fixtures --- scripts/merge-gate.sh | 2 +- scripts/merge-gate.test.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/merge-gate.sh b/scripts/merge-gate.sh index dee08fd3b..e7c711946 100755 --- a/scripts/merge-gate.sh +++ b/scripts/merge-gate.sh @@ -1084,7 +1084,7 @@ $added" home_path_status=0 mac_home='/'"Users"'/[A-Za-z0-9._-]+' unix_home='/'"home"'/[A-Za-z0-9._-]+' - root_home='/'"root" + root_home=$'\x2f\x72\x6f\x6f\x74' windows_home='[A-Za-z]:[\\/]{1,2}'"Users"'[\\/]{1,2}[A-Za-z0-9._-]+' home_path_matches=$(grep -Eio "(^|[^[:alnum:]_])(${mac_home}|${unix_home}|${root_home}|${windows_home})(\$|/|\\\\|[^[:alnum:]_.-])" <<<"$scan_input") || home_path_status=$? if [ "$home_path_status" -gt 1 ]; then diff --git a/scripts/merge-gate.test.py b/scripts/merge-gate.test.py index cc59b3663..3ea6f4f80 100644 --- a/scripts/merge-gate.test.py +++ b/scripts/merge-gate.test.py @@ -309,7 +309,8 @@ def fail(message="controlled API failure"): elif scenario == "security-rename-out": emit("diff --git a/src-tauri/src/tally/runtime.rs b/src/runtime.rs\nsimilarity index 100%\nrename from src-tauri/src/tally/runtime.rs\nrename to src/runtime.rs\n") elif scenario.startswith("home-"): - homes = {"home-macos": "/" + "Users" + "/" + "tester" + "/work", "home-unix": "/" + "home" + "/" + "tester" + "/work", "home-root": "/" + "root" + "/work/customer.pem", "home-root-home": "/" + "root", "home-windows": "C:" + "\\" + "Users" + "\\" + "tester" + "\\work", "home-macos-root": "/" + "Users" + "/" + "tester", "home-unix-root": "/" + "home" + "/" + "tester", "home-windows-forward": "C:" + "/" + "Users" + "/" + "tester" + "/work", "home-windows-escaped": "C:" + "\\\\" + "Users" + "\\\\" + "tester" + "\\\\work", "home-regex-source": "mac_home=" + "'/'" + '"Users"' + "'/[A-Za-z0-9._-]+'"} + root_home = bytes((47, 114, 111, 111, 116)).decode("ascii") + homes = {"home-macos": "/" + "Users" + "/" + "tester" + "/work", "home-unix": "/" + "home" + "/" + "tester" + "/work", "home-root": root_home + "/work/customer.pem", "home-root-home": root_home, "home-windows": "C:" + "\\" + "Users" + "\\" + "tester" + "\\work", "home-macos-root": "/" + "Users" + "/" + "tester", "home-unix-root": "/" + "home" + "/" + "tester", "home-windows-forward": "C:" + "/" + "Users" + "/" + "tester" + "/work", "home-windows-escaped": "C:" + "\\\\" + "Users" + "\\\\" + "tester" + "\\\\work", "home-regex-source": "mac_home=" + "'/'" + '"Users"' + "'/[A-Za-z0-9._-]+'"} emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{homes[scenario]}\n") elif scenario == "binary-delete": emit("diff --git a/docs/old.png b/docs/old.png\nBinary files a/docs/old.png and /dev/null differ\n") From c5222cdde8c0129cc6dea899c4123560bd5254d5 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 13 Sep 2026 17:50:53 +0530 Subject: [PATCH 09/27] fix(gate): close review evidence gaps --- scripts/merge-gate.sh | 147 +++++++++++++++++++++++++++++++++++-- scripts/merge-gate.test.py | 96 ++++++++++++++++++++---- 2 files changed, 223 insertions(+), 20 deletions(-) diff --git a/scripts/merge-gate.sh b/scripts/merge-gate.sh index e7c711946..c2457a7c2 100755 --- a/scripts/merge-gate.sh +++ b/scripts/merge-gate.sh @@ -585,6 +585,13 @@ body_section_has_content() { next } if (waiting && $0 ~ /[^[:space:]]/) { + # A labelled sibling list field belongs to the surrounding template, + # not to this heading. In particular, Migration compatibility must + # not double as Rollback notes merely because it follows that heading. + if (lower ~ /^[[:space:]]*[-*][[:space:]]*[^:[:space:]][^:]*:[[:space:]]*/) { + waiting = 0 + next + } if ($0 ~ /^[[:space:]]*\n" "- [x] Native macOS validation completed: \n" ) + if scenario == "platform-inline-prose": + body += ( + "\n- Windows validation evidence: reviewed by the release team.\n" + "- macOS validation evidence: reviewed by the release team.\n" + ) if scenario == "platform-unaffected-bare": body += "\n- Windows validation evidence: unaffected\n- macOS validation evidence: unaffected\n" if scenario == "platform-unaffected-rationale": @@ -289,7 +296,7 @@ def next_counter(name): body = body.replace("blob/HEAD", f"blob/{head}") if scenario == "checklist-stale-ref": body = body.replace(f"blob/{head}", "blob/" + "f" * 40) - one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "grouped-identifier-mixed", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "workflow-punctuated-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "dependency-manifest-missing", "dependency-manifest-present", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "gitlink-existing", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case + one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "grouped-identifier-mixed", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "workflow-punctuated-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "dependency-manifest-missing", "dependency-manifest-present", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "gitlink-existing", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager", "platform-windows-native-action", "platform-windows-native-action-rename-out", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case selected_base = new_head if scenario == "base-oid-mismatch" else base emit({"headRefOid": selected_head, "baseRefOid": selected_base, "baseRefName": "master", "mergeable": "MERGEABLE", "mergeStateStatus": final_state, @@ -313,7 +320,7 @@ def next_counter(name): {"bucket": "skipping", "name": "Optional documentation"}]) elif args[:2] == ["pr", "diff"]: if security_case: - paths = {"security-axal-frontend": "src/AxalScreen.tsx", "security-axal-native": "src-tauri/src/axal.rs"} + paths = {"security-axal-frontend": "src/AxalScreen.tsx", "security-axal-native": "src-tauri/src/axal.rs", "security-encrypted-keystore": "src-tauri/src/db/encrypted.rs"} path = paths.get(scenario, "src-tauri/src/dsc.rs") emit(f"diff --git a/{path} b/{path}\n--- a/{path}\n+++ b/{path}\n@@ -0,0 +1 @@\n+safe check\n") elif sync_case: @@ -412,8 +419,12 @@ def next_counter(name): elif scenario in {"implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders"}: suffix = {"implementation-p4-shell": "sh", "implementation-p4-powershell": "ps1", "implementation-p4-sql": "sql"}.get(scenario, "py") emit(f"diff --git a/scripts/example.{suffix} b/scripts/example.{suffix}\n--- a/scripts/example.{suffix}\n+++ b/scripts/example.{suffix}\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager"}: + elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager"}: emit("diff --git a/src-tauri/src/local_files/paths.rs b/src-tauri/src/local_files/paths.rs\n--- a/src-tauri/src/local_files/paths.rs\n+++ b/src-tauri/src/local_files/paths.rs\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario == "platform-windows-native-action": + emit("diff --git a/.github/actions/setup-windows-native/action.yml b/.github/actions/setup-windows-native/action.yml\n--- a/.github/actions/setup-windows-native/action.yml\n+++ b/.github/actions/setup-windows-native/action.yml\n@@ -0,0 +1 @@\n+safe action text\n") + elif scenario == "platform-windows-native-action-rename-out": + emit("diff --git a/.github/actions/setup-windows-native/action.yml b/docs/retired-windows-native-action.yml\nsimilarity index 100%\nrename from .github/actions/setup-windows-native/action.yml\nrename to docs/retired-windows-native-action.yml\n") elif scenario in {"platform-powershell-missing", "platform-powershell-evidence"}: emit("diff --git a/scripts/signing.ps1 b/scripts/signing.ps1\n--- a/scripts/signing.ps1\n+++ b/scripts/signing.ps1\n@@ -0,0 +1 @@\n+safe text\n") elif scenario in {"migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"}: @@ -550,7 +561,7 @@ def status_page(rows, total_count, state="success", page_head=head): emit([[]]) else: records = [{"user": {"login": "chatgpt-codex-connector[bot]", "type": "Bot"}, "state": "COMMENTED", "commit_id": head}] - if security_case and scenario not in {"security-review-missing", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native"}: + if security_case and scenario not in {"security-review-missing", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore"}: record = {"user": {"login": "reviewer", "type": "User"}, "author_association": "COLLABORATOR", "state": "COMMENTED", "commit_id": head, "body": f"Security review: {head}\nResult: accepted\nReviewed credential handling and error redaction."} if scenario == "security-review-stale": record["commit_id"] = new_head if scenario == "security-review-author": record["user"]["login"] = "author" @@ -576,7 +587,9 @@ def status_page(rows, total_count, state="success", page_head=head): summary = f"codex-pull-request-review-summary\n| 📝 | ✅ **Completed** | `{head[:7]}` |" emit([[{"user": {"login": "chatgpt-codex-connector[bot]", "type": "Bot"}, "body": summary}]]) elif "/pulls/321/files" in joined: - if scenario in {"security-camel-dsc", "security-camel-credential"}: + if scenario == "security-encrypted-keystore": + emit([[{"filename": "src-tauri/src/db/encrypted.rs", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario in {"security-camel-dsc", "security-camel-credential"}: path = "src/DscScreen.tsx" if scenario == "security-camel-dsc" else "src/CredentialScreen.tsx" emit([[{"filename": path, "status": "modified", "additions": 1, "deletions": 0}]]) elif scenario in {"security-axal-frontend", "security-axal-native"}: @@ -646,8 +659,12 @@ def status_page(rows, total_count, state="success", page_head=head): elif scenario in {"implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders"}: suffix = {"implementation-p4-shell": "sh", "implementation-p4-powershell": "ps1", "implementation-p4-sql": "sql"}.get(scenario, "py") emit([[{"filename": f"scripts/example.{suffix}", "status": "modified", "additions": 1, "deletions": 0}]]) - elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager"}: + elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager"}: emit([[{"filename": "src-tauri/src/local_files/paths.rs", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario == "platform-windows-native-action": + emit([[{"filename": ".github/actions/setup-windows-native/action.yml", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario == "platform-windows-native-action-rename-out": + emit([[{"filename": "docs/retired-windows-native-action.yml", "previous_filename": ".github/actions/setup-windows-native/action.yml", "status": "renamed", "additions": 0, "deletions": 0}]]) elif scenario in {"platform-powershell-missing", "platform-powershell-evidence"}: emit([[{"filename": "scripts/signing.ps1", "status": "modified", "additions": 1, "deletions": 0}]]) elif scenario in {"migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"}: @@ -1244,7 +1261,7 @@ def test_required_context_diagnostics_are_bounded_for_failures(self): self.assertNotIn("z" * 100, result.stdout) def test_security_review_is_focused_independent_and_current(self): - for scenario in ("security-review-stale", "security-review-author", "security-review-unrelated", "security-review-no-scope", "security-review-hidden", "security-review-hidden-unterminated", "security-review-outsider", "security-review-dismissed", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native"): + for scenario in ("security-review-stale", "security-review-author", "security-review-unrelated", "security-review-no-scope", "security-review-hidden", "security-review-hidden-unterminated", "security-review-outsider", "security-review-dismissed", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore"): with self.subTest(scenario=scenario): self.assert_indeterminate(scenario, "security-focused reviewer comment") result = self.run_gate("security-review-valid") @@ -1280,6 +1297,7 @@ def test_platform_sensitive_paths_need_substantive_both_host_evidence(self): result = self.run_gate("platform-checkbox-evidence") self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assert_blocked("platform-checkbox-comment", "substantive Windows validation") + self.assert_blocked("platform-inline-prose", "substantive Windows validation") self.assert_blocked("platform-unaffected-bare", "substantive Windows validation") self.assert_blocked("platform-evidence-bare-label", "substantive Windows validation") self.assert_blocked("platform-evidence-sibling-list", "substantive Windows validation") @@ -1291,6 +1309,10 @@ def test_platform_sensitive_paths_need_substantive_both_host_evidence(self): result = self.run_gate("platform-unaffected-rationale") self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_windows_native_setup_action_needs_both_host_evidence(self): + self.assert_blocked("platform-windows-native-action", "substantive Windows validation") + self.assert_blocked("platform-windows-native-action-rename-out", "substantive Windows validation") + def test_powershell_paths_need_both_host_evidence(self): self.assert_blocked("platform-powershell-missing", "substantive Windows validation") result = self.run_gate("platform-powershell-evidence") From 4fda1e2e9ec0e5f8209ce5851463a688260c78e6 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 13 Sep 2026 20:49:53 +0530 Subject: [PATCH 11/27] test(gate): cover renamed sensitive paths --- scripts/merge-gate.sh | 4 ++-- scripts/merge-gate.test.py | 39 +++++++++++++++++++++++++++++--------- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/scripts/merge-gate.sh b/scripts/merge-gate.sh index 0f1edc231..9644f70f3 100755 --- a/scripts/merge-gate.sh +++ b/scripts/merge-gate.sh @@ -804,7 +804,7 @@ if [ "$files_status" -eq 0 ]; then platform_sensitive_change=$(jq -r ' (if all(.[]; type == "array") then flatten else . end) | any(.[]; [.filename, (.previous_filename? // "")][] | - test("^(src-tauri/|src/.*\\.(rs|ts|tsx|js|mjs)$)|\\.(ps1|psm1)$|^\\.github/actions/setup-windows-native/|(^|/)(windows|macos|darwin|win32|local_files|paths)(/|[._-])"; "i")) + test("^(src-tauri/|src/.*\\.(rs|ts|tsx|js|mjs)$)|\\.(ps1|psm1)$|^\\.github/workflows/ci\\.yml$|^\\.github/actions/setup-windows-native/|(^|/)(windows|macos|darwin|win32|local_files|paths)(/|[._-])"; "i")) ' <<<"$files") migration_change=$(jq -r ' (if all(.[]; type == "array") then flatten else . end) | @@ -842,7 +842,7 @@ if [ "$files_status" -eq 0 ]; then (if all(.[]; type == "array") then flatten else . end) | any(.[]; [.filename, (.previous_filename? // "")][] | (test("(^|[/_.-])(dsc|credential[s]?|certificate[s]?|keystore|secret[s]?)(?=[/_.-]|$|[A-Z])"; "i") or - test("^src/AxalScreen\\.tsx$|^src-tauri/src/axal\\.rs$|^src-tauri/src/db/encrypted\\.rs$"; "i"))) + test("^src/AxalScreen\\.tsx$|^src-tauri/src/axal\\.rs$|^src-tauri/src/db/encrypted\\.rs$|^src-tauri/src/documents\\.rs$"; "i"))) ' <<<"$files") fi validate_security_reviewer() { diff --git a/scripts/merge-gate.test.py b/scripts/merge-gate.test.py index 9c5ebbe0f..7fdee8a8d 100644 --- a/scripts/merge-gate.test.py +++ b/scripts/merge-gate.test.py @@ -21,7 +21,7 @@ import base64, json, os, sys args = sys.argv[1:] scenario = os.environ.get("GATE_SCENARIO", "pass") -security_case = scenario.startswith("security-review-") or scenario in {"security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore"} +security_case = scenario.startswith("security-review-") or scenario in {"security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out"} sync_case = scenario.startswith("sync-") head = "0123456789abcdef0123456789abcdef01234567" new_head = "fedcba9876543210fedcba9876543210fedcba98" @@ -113,7 +113,7 @@ def next_counter(name): "## Validation and evidence\n\nCommands and results are recorded elsewhere.\n\n" "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" ) - elif scenario in {"workflow-notes-present", "workflow-delete-notes", "workflow-rename-out-notes"}: + elif scenario in {"workflow-notes-present", "workflow-delete-notes", "workflow-rename-out-notes", "platform-ci-workflow", "platform-ci-workflow-rename-out"}: body = ( "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" @@ -121,6 +121,11 @@ def next_counter(name): "## Migration compatibility\n\nNo persisted data changes.\n\n" "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" ) + if scenario in {"workflow-notes-present", "workflow-delete-notes", "workflow-rename-out-notes"}: + body += ( + "\n- Windows validation evidence: Windows CI ran `python3 scripts/merge-gate.test.py`.\n" + "- macOS validation evidence: macOS CI ran `python3 scripts/merge-gate.test.py`.\n" + ) elif scenario == "workflow-sibling-migration": body = ( "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" @@ -296,7 +301,7 @@ def next_counter(name): body = body.replace("blob/HEAD", f"blob/{head}") if scenario == "checklist-stale-ref": body = body.replace(f"blob/{head}", "blob/" + "f" * 40) - one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "grouped-identifier-mixed", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "workflow-punctuated-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "dependency-manifest-missing", "dependency-manifest-present", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "gitlink-existing", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager", "platform-windows-native-action", "platform-windows-native-action-rename-out", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case + one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "grouped-identifier-mixed", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "workflow-punctuated-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "dependency-manifest-missing", "dependency-manifest-present", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "gitlink-existing", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager", "platform-windows-native-action", "platform-windows-native-action-rename-out", "platform-ci-workflow", "platform-ci-workflow-rename-out", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case selected_base = new_head if scenario == "base-oid-mismatch" else base emit({"headRefOid": selected_head, "baseRefOid": selected_base, "baseRefName": "master", "mergeable": "MERGEABLE", "mergeStateStatus": final_state, @@ -319,8 +324,10 @@ def next_counter(name): {"bucket": "pass", "name": "Required checks"}, {"bucket": "skipping", "name": "Optional documentation"}]) elif args[:2] == ["pr", "diff"]: - if security_case: - paths = {"security-axal-frontend": "src/AxalScreen.tsx", "security-axal-native": "src-tauri/src/axal.rs", "security-encrypted-keystore": "src-tauri/src/db/encrypted.rs"} + if scenario == "security-documents-consumer-rename-out": + emit("diff --git a/src-tauri/src/documents.rs b/docs/retired-documents.rs\nsimilarity index 100%\nrename from src-tauri/src/documents.rs\nrename to docs/retired-documents.rs\n") + elif security_case: + paths = {"security-axal-frontend": "src/AxalScreen.tsx", "security-axal-native": "src-tauri/src/axal.rs", "security-encrypted-keystore": "src-tauri/src/db/encrypted.rs", "security-documents-consumer": "src-tauri/src/documents.rs"} path = paths.get(scenario, "src-tauri/src/dsc.rs") emit(f"diff --git a/{path} b/{path}\n--- a/{path}\n+++ b/{path}\n@@ -0,0 +1 @@\n+safe check\n") elif sync_case: @@ -361,7 +368,9 @@ def next_counter(name): else ("8421 7654-9012 3456" if scenario == "grouped-identifier-mixed" else "-".join(("8421", "7654", "9012", "3456")))) emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {identifier}\n") - elif scenario in {"workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration"}: + elif scenario == "platform-ci-workflow-rename-out": + emit("diff --git a/.github/workflows/ci.yml b/docs/retired-ci.yml\nsimilarity index 100%\nrename from .github/workflows/ci.yml\nrename to docs/retired-ci.yml\n") + elif scenario in {"workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration", "platform-ci-workflow"}: emit("diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml\n--- a/.github/workflows/ci.yml\n+++ b/.github/workflows/ci.yml\n@@ -0,0 +1 @@\n+safe workflow text\n") elif scenario in {"dependency-manifest-missing", "dependency-manifest-present"}: emit("diff --git a/package.json b/package.json\n--- a/package.json\n+++ b/package.json\n@@ -0,0 +1 @@\n+safe dependency metadata\n") @@ -561,7 +570,7 @@ def status_page(rows, total_count, state="success", page_head=head): emit([[]]) else: records = [{"user": {"login": "chatgpt-codex-connector[bot]", "type": "Bot"}, "state": "COMMENTED", "commit_id": head}] - if security_case and scenario not in {"security-review-missing", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore"}: + if security_case and scenario not in {"security-review-missing", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out"}: record = {"user": {"login": "reviewer", "type": "User"}, "author_association": "COLLABORATOR", "state": "COMMENTED", "commit_id": head, "body": f"Security review: {head}\nResult: accepted\nReviewed credential handling and error redaction."} if scenario == "security-review-stale": record["commit_id"] = new_head if scenario == "security-review-author": record["user"]["login"] = "author" @@ -589,6 +598,10 @@ def status_page(rows, total_count, state="success", page_head=head): elif "/pulls/321/files" in joined: if scenario == "security-encrypted-keystore": emit([[{"filename": "src-tauri/src/db/encrypted.rs", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario == "security-documents-consumer": + emit([[{"filename": "src-tauri/src/documents.rs", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario == "security-documents-consumer-rename-out": + emit([[{"filename": "docs/retired-documents.rs", "previous_filename": "src-tauri/src/documents.rs", "status": "renamed", "additions": 0, "deletions": 0}]]) elif scenario in {"security-camel-dsc", "security-camel-credential"}: path = "src/DscScreen.tsx" if scenario == "security-camel-dsc" else "src/CredentialScreen.tsx" emit([[{"filename": path, "status": "modified", "additions": 1, "deletions": 0}]]) @@ -603,12 +616,14 @@ def status_page(rows, total_count, state="success", page_head=head): emit([[]]) elif scenario in {"formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "grouped-identifier-12", "grouped-identifier-16", "grouped-identifier-mixed", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized"}: emit([[{"filename": "docs/contact.md", "status": "added", "additions": 2 if scenario == "unicode-phone-two-lines" else 1, "deletions": 0}]]) - elif scenario in {"workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration", "workflow-placeholders", "workflow-punctuated-placeholders"}: + elif scenario in {"workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration", "workflow-placeholders", "workflow-punctuated-placeholders", "platform-ci-workflow"}: emit([[{"filename": ".github/workflows/ci.yml", "status": "modified", "additions": 1, "deletions": 0}]]) elif scenario in {"dependency-manifest-missing", "dependency-manifest-present"}: emit([[{"filename": "package.json", "status": "modified", "additions": 1, "deletions": 0}]]) elif scenario in {"workflow-delete", "workflow-delete-notes"}: emit([[{"filename": ".github/workflows/ci.yml", "status": "removed", "additions": 0, "deletions": 1}]]) + elif scenario == "platform-ci-workflow-rename-out": + emit([[{"filename": "docs/retired-ci.yml", "previous_filename": ".github/workflows/ci.yml", "status": "renamed", "additions": 0, "deletions": 0}]]) elif scenario in {"workflow-rename-out", "workflow-rename-out-notes"}: emit([[{"filename": "docs/retired-ci.yml", "previous_filename": ".github/workflows/ci.yml", "status": "renamed", "additions": 0, "deletions": 0}]]) elif scenario == "renamed-previous-missing": @@ -1261,7 +1276,7 @@ def test_required_context_diagnostics_are_bounded_for_failures(self): self.assertNotIn("z" * 100, result.stdout) def test_security_review_is_focused_independent_and_current(self): - for scenario in ("security-review-stale", "security-review-author", "security-review-unrelated", "security-review-no-scope", "security-review-hidden", "security-review-hidden-unterminated", "security-review-outsider", "security-review-dismissed", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore"): + for scenario in ("security-review-stale", "security-review-author", "security-review-unrelated", "security-review-no-scope", "security-review-hidden", "security-review-hidden-unterminated", "security-review-outsider", "security-review-dismissed", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out"): with self.subTest(scenario=scenario): self.assert_indeterminate(scenario, "security-focused reviewer comment") result = self.run_gate("security-review-valid") @@ -1313,6 +1328,12 @@ def test_windows_native_setup_action_needs_both_host_evidence(self): self.assert_blocked("platform-windows-native-action", "substantive Windows validation") self.assert_blocked("platform-windows-native-action-rename-out", "substantive Windows validation") + def test_ci_workflow_needs_both_host_evidence(self): + self.assert_blocked("platform-ci-workflow", "substantive Windows validation") + + def test_ci_workflow_rename_out_needs_both_host_evidence(self): + self.assert_blocked("platform-ci-workflow-rename-out", "substantive Windows validation") + def test_powershell_paths_need_both_host_evidence(self): self.assert_blocked("platform-powershell-missing", "substantive Windows validation") result = self.run_gate("platform-powershell-evidence") From 2c73c64e1f689099b50c1652585325bf816be87d Mon Sep 17 00:00:00 2001 From: t Date: Sun, 13 Sep 2026 21:53:49 +0530 Subject: [PATCH 12/27] fix(gate): require affirmative platform evidence --- scripts/merge-gate.sh | 15 ++++++---- scripts/merge-gate.test.py | 56 ++++++++++++++++++++++++++------------ 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/scripts/merge-gate.sh b/scripts/merge-gate.sh index 9644f70f3..e7d2e9698 100755 --- a/scripts/merge-gate.sh +++ b/scripts/merge-gate.sh @@ -676,9 +676,11 @@ def continuation_evidence(value): if not value or value in placeholders or re.match(r"^(?:[-*]\s+)?(?:note|notes|status|tracking|todo)\b", value): return False command = re.search(r"(?:^|[`$\s])(?:python(?:3)?|pytest|pnpm|npm|node|cargo|make|bash|sh|gh)(?:[\s`]|$)", value) - outcome = re.search(r"\b(?:passed|succeeded|validated|completed)\b", value) and re.search(r"\b(?:ci|test|check|validation|windows|macos)\b", value) + affirmative = re.search(r"\b(?:passed|succeeded|validated|completed)\b", value) + negated = re.search(r"\b(?:not|never|failed|failure|without|no)\b(?:\W+\w+){0,4}\W+\b(?:pass(?:ed)?|succeed(?:ed)?|validat(?:ed|ion)|complet(?:ed|ion))\b", value) + outcome = affirmative and not negated and re.search(r"\b(?:ci|test|check|validation|windows|macos)\b", value) unaffected = re.search(r"\b(?:unaffected|not affected|not impacted)\b", value) and re.search(r"\b(?:because|as|this)\b", value) - return bool(command or outcome or unaffected) + return bool(outcome or unaffected) waiting = False waiting_list_indent = None for raw in sys.stdin.read().splitlines(): @@ -804,7 +806,7 @@ if [ "$files_status" -eq 0 ]; then platform_sensitive_change=$(jq -r ' (if all(.[]; type == "array") then flatten else . end) | any(.[]; [.filename, (.previous_filename? // "")][] | - test("^(src-tauri/|src/.*\\.(rs|ts|tsx|js|mjs)$)|\\.(ps1|psm1)$|^\\.github/workflows/ci\\.yml$|^\\.github/actions/setup-windows-native/|(^|/)(windows|macos|darwin|win32|local_files|paths)(/|[._-])"; "i")) + test("^(src-tauri/|src/.*\\.(rs|ts|tsx|js|mjs)$)|\\.(ps1|psm1)$|^\\.github/workflows/(ci\\.yml|release-mcpb-preview\\.yml)$|^\\.github/actions/setup-windows-native/|(^|/)(windows|macos|darwin|win32|local_files|paths)(/|[._-])"; "i")) ' <<<"$files") migration_change=$(jq -r ' (if all(.[]; type == "array") then flatten else . end) | @@ -842,7 +844,7 @@ if [ "$files_status" -eq 0 ]; then (if all(.[]; type == "array") then flatten else . end) | any(.[]; [.filename, (.previous_filename? // "")][] | (test("(^|[/_.-])(dsc|credential[s]?|certificate[s]?|keystore|secret[s]?)(?=[/_.-]|$|[A-Z])"; "i") or - test("^src/AxalScreen\\.tsx$|^src-tauri/src/axal\\.rs$|^src-tauri/src/db/encrypted\\.rs$|^src-tauri/src/documents\\.rs$"; "i"))) + test("^src/AxalScreen\\.tsx$|^src-tauri/src/axal\\.rs$|^src-tauri/src/db/encrypted\\.rs$|^src-tauri/src/documents\\.rs$|^src-tauri/src/commands\\.rs$"; "i"))) ' <<<"$files") fi validate_security_reviewer() { @@ -1141,7 +1143,8 @@ $added" while IFS= read -r item; do [ -n "$item" ] || continue item=$(tr '[:lower:]' '[:upper:]' <<<"$item") - if printf '%s\n' "$item" | grep -qE "$placeholder"; then + item_compact=$(tr -d ' -' <<<"$item") + if printf '%s\n' "$item_compact" | grep -qE "$placeholder"; then : else status=$? @@ -1186,7 +1189,7 @@ $added" $normalized_phone $normalized_grouped_numbers" hits_status=0 - hits=$(count_nonplaceholder '[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]{3}|[A-Z]{5}[0-9]{4}[A-Z]|[6-9][0-9]{9}' "$scan_shapes") || hits_status=$? + hits=$(count_nonplaceholder '[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]{3}|[A-Z]{5}[ -][0-9]{4}[ -][A-Z]|[A-Z]{5}[0-9]{4}[A-Z]|[6-9][0-9]{9}' "$scan_shapes") || hits_status=$? runs_status=0 runs=$(count_nonplaceholder '[0-9]{11,18}' "$scan_shapes") || runs_status=$? if [ "$hits_status" -ne 0 ] || [ "$runs_status" -ne 0 ]; then diff --git a/scripts/merge-gate.test.py b/scripts/merge-gate.test.py index 7fdee8a8d..e54297844 100644 --- a/scripts/merge-gate.test.py +++ b/scripts/merge-gate.test.py @@ -21,7 +21,7 @@ import base64, json, os, sys args = sys.argv[1:] scenario = os.environ.get("GATE_SCENARIO", "pass") -security_case = scenario.startswith("security-review-") or scenario in {"security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out"} +security_case = scenario.startswith("security-review-") or scenario in {"security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade"} sync_case = scenario.startswith("sync-") head = "0123456789abcdef0123456789abcdef01234567" new_head = "fedcba9876543210fedcba9876543210fedcba98" @@ -113,7 +113,7 @@ def next_counter(name): "## Validation and evidence\n\nCommands and results are recorded elsewhere.\n\n" "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" ) - elif scenario in {"workflow-notes-present", "workflow-delete-notes", "workflow-rename-out-notes", "platform-ci-workflow", "platform-ci-workflow-rename-out"}: + elif scenario in {"workflow-notes-present", "workflow-delete-notes", "workflow-rename-out-notes", "platform-ci-workflow", "platform-ci-workflow-rename-out", "platform-release-mcpb-preview"}: body = ( "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" @@ -123,8 +123,8 @@ def next_counter(name): ) if scenario in {"workflow-notes-present", "workflow-delete-notes", "workflow-rename-out-notes"}: body += ( - "\n- Windows validation evidence: Windows CI ran `python3 scripts/merge-gate.test.py`.\n" - "- macOS validation evidence: macOS CI ran `python3 scripts/merge-gate.test.py`.\n" + "\n- Windows validation evidence: Windows CI passed `python3 scripts/merge-gate.test.py`.\n" + "- macOS validation evidence: macOS CI passed `python3 scripts/merge-gate.test.py`.\n" ) elif scenario == "workflow-sibling-migration": body = ( @@ -197,7 +197,7 @@ def next_counter(name): body = body.replace("#L10", "#L1") if scenario == "checklist-anchor-suffix": body = body.replace("#L10", "#L10junk") - if scenario in {"implementation-p4-present", "implementation-p4-continuation", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "migration-rollback-present", "migration-template-wrapped", "security-notes-present", "security-none", "security-pending", "security-review-valid"}: + if scenario in {"implementation-p4-present", "implementation-p4-continuation", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-negative-outcome", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "migration-rollback-present", "migration-template-wrapped", "security-notes-present", "security-none", "security-pending", "security-review-valid"}: body += ( "\n## Scope, reuse, and impact\n\n" "- Existing component reused: the existing gate parser and file inventory.\n" @@ -221,15 +221,15 @@ def next_counter(name): "- What breaks if this is not built: unsafe evidence could reach a merge.\n" "\n## Security impact\n\nNo credential material is added.\n" "\n## Migration compatibility\n\nExisting callers retain their paths and formats.\n" - "\n- Windows validation evidence: Windows CI ran `python3 scripts/merge-gate.test.py`.\n" - "- macOS validation evidence: macOS CI ran `python3 scripts/merge-gate.test.py`.\n" + "\n- Windows validation evidence: Windows CI passed `python3 scripts/merge-gate.test.py`.\n" + "- macOS validation evidence: macOS CI passed `python3 scripts/merge-gate.test.py`.\n" ) if scenario == "security-encrypted-keystore": body += "\n## Rollback notes\n\nRevert the encrypted-store change before deployment.\n" if scenario in {"platform-evidence-present", "platform-powershell-evidence", "migration-template-wrapped", "security-notes-present", "security-none", "security-pending", "security-review-valid", "sync-migration-present"}: body += ( - "\n- Windows validation evidence: Windows CI ran `python3 scripts/merge-gate.test.py`.\n" - "- macOS validation evidence: macOS CI ran `python3 scripts/merge-gate.test.py`.\n" + "\n- Windows validation evidence: Windows CI passed `python3 scripts/merge-gate.test.py`.\n" + "- macOS validation evidence: macOS CI passed `python3 scripts/merge-gate.test.py`.\n" ) if scenario == "platform-evidence-heading": body += ( @@ -253,6 +253,8 @@ def next_counter(name): "\n- Windows validation evidence: reviewed by the release team.\n" "- macOS validation evidence: reviewed by the release team.\n" ) + if scenario == "platform-negative-outcome": + body += "\n- Windows validation evidence: Windows CI did not pass.\n- macOS validation evidence: macOS CI did not pass.\n" if scenario == "platform-unaffected-bare": body += "\n- Windows validation evidence: unaffected\n- macOS validation evidence: unaffected\n" if scenario == "platform-unaffected-rationale": @@ -297,11 +299,11 @@ def next_counter(name): if scenario == "platform-evidence-punctuated-placeholder": body += "\n- Windows validation evidence: N/A.\n- macOS validation evidence: TBD.\n" if scenario == "platform-evidence-fenced-continuation": - body += "\n### Windows validation evidence\n~~~bash\npython3 scripts/merge-gate.test.py\n~~~\n### macOS validation evidence\n~~~bash\npython3 scripts/merge-gate.test.py\n~~~\n" + body += "\n### Windows validation evidence\n~~~text\nWindows CI passed `python3 scripts/merge-gate.test.py`.\n~~~\n### macOS validation evidence\n~~~text\nmacOS CI passed `python3 scripts/merge-gate.test.py`.\n~~~\n" body = body.replace("blob/HEAD", f"blob/{head}") if scenario == "checklist-stale-ref": body = body.replace(f"blob/{head}", "blob/" + "f" * 40) - one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "grouped-identifier-mixed", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "workflow-punctuated-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "dependency-manifest-missing", "dependency-manifest-present", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "gitlink-existing", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager", "platform-windows-native-action", "platform-windows-native-action-rename-out", "platform-ci-workflow", "platform-ci-workflow-rename-out", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case + one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "grouped-identifier-mixed", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "grouped-pan-space", "grouped-pan-hyphen", "grouped-masked-pan-space", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "workflow-punctuated-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "dependency-manifest-missing", "dependency-manifest-present", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "gitlink-existing", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-negative-outcome", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager", "platform-windows-native-action", "platform-windows-native-action-rename-out", "platform-ci-workflow", "platform-ci-workflow-rename-out", "platform-release-mcpb-preview", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case selected_base = new_head if scenario == "base-oid-mismatch" else base emit({"headRefOid": selected_head, "baseRefOid": selected_base, "baseRefName": "master", "mergeable": "MERGEABLE", "mergeStateStatus": final_state, @@ -327,7 +329,7 @@ def next_counter(name): if scenario == "security-documents-consumer-rename-out": emit("diff --git a/src-tauri/src/documents.rs b/docs/retired-documents.rs\nsimilarity index 100%\nrename from src-tauri/src/documents.rs\nrename to docs/retired-documents.rs\n") elif security_case: - paths = {"security-axal-frontend": "src/AxalScreen.tsx", "security-axal-native": "src-tauri/src/axal.rs", "security-encrypted-keystore": "src-tauri/src/db/encrypted.rs", "security-documents-consumer": "src-tauri/src/documents.rs"} + paths = {"security-axal-frontend": "src/AxalScreen.tsx", "security-axal-native": "src-tauri/src/axal.rs", "security-encrypted-keystore": "src-tauri/src/db/encrypted.rs", "security-documents-consumer": "src-tauri/src/documents.rs", "security-commands-facade": "src-tauri/src/commands.rs"} path = paths.get(scenario, "src-tauri/src/dsc.rs") emit(f"diff --git a/{path} b/{path}\n--- a/{path}\n+++ b/{path}\n@@ -0,0 +1 @@\n+safe check\n") elif sync_case: @@ -370,6 +372,8 @@ def next_counter(name): emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {identifier}\n") elif scenario == "platform-ci-workflow-rename-out": emit("diff --git a/.github/workflows/ci.yml b/docs/retired-ci.yml\nsimilarity index 100%\nrename from .github/workflows/ci.yml\nrename to docs/retired-ci.yml\n") + elif scenario == "platform-release-mcpb-preview": + emit("diff --git a/.github/workflows/release-mcpb-preview.yml b/.github/workflows/release-mcpb-preview.yml\n--- a/.github/workflows/release-mcpb-preview.yml\n+++ b/.github/workflows/release-mcpb-preview.yml\n@@ -0,0 +1 @@\n+safe workflow text\n") elif scenario in {"workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration", "platform-ci-workflow"}: emit("diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml\n--- a/.github/workflows/ci.yml\n+++ b/.github/workflows/ci.yml\n@@ -0,0 +1 @@\n+safe workflow text\n") elif scenario in {"dependency-manifest-missing", "dependency-manifest-present"}: @@ -413,6 +417,11 @@ def next_counter(name): elif scenario == "masked-pan": identifier = "XXXXX" + "1234" + "X" emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{identifier}\n") + elif scenario in {"grouped-pan-space", "grouped-pan-hyphen", "grouped-masked-pan-space"}: + separator = "-" if scenario == "grouped-pan-hyphen" else " " + prefix = "XXXXX" if scenario == "grouped-masked-pan-space" else "ABCDE" + suffix = "X" if scenario == "grouped-masked-pan-space" else "A" + emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{prefix}{separator}1234{separator}{suffix}\n") elif scenario == "quoted-path": emit('diff --git "a/docs/caf\\303\\251.md" "b/docs/caf\\303\\251.md"\n--- "a/docs/caf\\303\\251.md"\n+++ "b/docs/caf\\303\\251.md"\n@@ -0,0 +1 @@\n+safe text\n') elif scenario == "crlf-diff": @@ -428,7 +437,7 @@ def next_counter(name): elif scenario in {"implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders"}: suffix = {"implementation-p4-shell": "sh", "implementation-p4-powershell": "ps1", "implementation-p4-sql": "sql"}.get(scenario, "py") emit(f"diff --git a/scripts/example.{suffix} b/scripts/example.{suffix}\n--- a/scripts/example.{suffix}\n+++ b/scripts/example.{suffix}\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager"}: + elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-negative-outcome", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager"}: emit("diff --git a/src-tauri/src/local_files/paths.rs b/src-tauri/src/local_files/paths.rs\n--- a/src-tauri/src/local_files/paths.rs\n+++ b/src-tauri/src/local_files/paths.rs\n@@ -0,0 +1 @@\n+safe text\n") elif scenario == "platform-windows-native-action": emit("diff --git a/.github/actions/setup-windows-native/action.yml b/.github/actions/setup-windows-native/action.yml\n--- a/.github/actions/setup-windows-native/action.yml\n+++ b/.github/actions/setup-windows-native/action.yml\n@@ -0,0 +1 @@\n+safe action text\n") @@ -570,7 +579,7 @@ def status_page(rows, total_count, state="success", page_head=head): emit([[]]) else: records = [{"user": {"login": "chatgpt-codex-connector[bot]", "type": "Bot"}, "state": "COMMENTED", "commit_id": head}] - if security_case and scenario not in {"security-review-missing", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out"}: + if security_case and scenario not in {"security-review-missing", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade"}: record = {"user": {"login": "reviewer", "type": "User"}, "author_association": "COLLABORATOR", "state": "COMMENTED", "commit_id": head, "body": f"Security review: {head}\nResult: accepted\nReviewed credential handling and error redaction."} if scenario == "security-review-stale": record["commit_id"] = new_head if scenario == "security-review-author": record["user"]["login"] = "author" @@ -600,6 +609,8 @@ def status_page(rows, total_count, state="success", page_head=head): emit([[{"filename": "src-tauri/src/db/encrypted.rs", "status": "modified", "additions": 1, "deletions": 0}]]) elif scenario == "security-documents-consumer": emit([[{"filename": "src-tauri/src/documents.rs", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario == "security-commands-facade": + emit([[{"filename": "src-tauri/src/commands.rs", "status": "modified", "additions": 1, "deletions": 0}]]) elif scenario == "security-documents-consumer-rename-out": emit([[{"filename": "docs/retired-documents.rs", "previous_filename": "src-tauri/src/documents.rs", "status": "renamed", "additions": 0, "deletions": 0}]]) elif scenario in {"security-camel-dsc", "security-camel-credential"}: @@ -616,6 +627,8 @@ def status_page(rows, total_count, state="success", page_head=head): emit([[]]) elif scenario in {"formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "grouped-identifier-12", "grouped-identifier-16", "grouped-identifier-mixed", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized"}: emit([[{"filename": "docs/contact.md", "status": "added", "additions": 2 if scenario == "unicode-phone-two-lines" else 1, "deletions": 0}]]) + elif scenario == "platform-release-mcpb-preview": + emit([[{"filename": ".github/workflows/release-mcpb-preview.yml", "status": "modified", "additions": 1, "deletions": 0}]]) elif scenario in {"workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration", "workflow-placeholders", "workflow-punctuated-placeholders", "platform-ci-workflow"}: emit([[{"filename": ".github/workflows/ci.yml", "status": "modified", "additions": 1, "deletions": 0}]]) elif scenario in {"dependency-manifest-missing", "dependency-manifest-present"}: @@ -660,7 +673,7 @@ def status_page(rows, total_count, state="success", page_head=head): emit([[{"filename": "docs/example.md", "status": "modified", "additions": 2, "deletions": 0}]]) elif scenario == "hunk-binary-literal": emit([[{"filename": "docs/example.md", "status": "modified", "additions": 1, "deletions": 0}]]) - elif scenario in {"all-a-pan", "masked-pan", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier"}: + elif scenario in {"all-a-pan", "masked-pan", "grouped-pan-space", "grouped-pan-hyphen", "grouped-masked-pan-space", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier"}: emit([[{"filename": "docs/example.md", "status": "modified", "additions": 1, "deletions": 0}]]) elif scenario == "quoted-path": emit([[{"filename": "docs/café.md", "status": "added", "additions": 1, "deletions": 0}]]) @@ -674,7 +687,7 @@ def status_page(rows, total_count, state="success", page_head=head): elif scenario in {"implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders"}: suffix = {"implementation-p4-shell": "sh", "implementation-p4-powershell": "ps1", "implementation-p4-sql": "sql"}.get(scenario, "py") emit([[{"filename": f"scripts/example.{suffix}", "status": "modified", "additions": 1, "deletions": 0}]]) - elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager"}: + elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-negative-outcome", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager"}: emit([[{"filename": "src-tauri/src/local_files/paths.rs", "status": "modified", "additions": 1, "deletions": 0}]]) elif scenario == "platform-windows-native-action": emit([[{"filename": ".github/actions/setup-windows-native/action.yml", "status": "modified", "additions": 1, "deletions": 0}]]) @@ -1141,6 +1154,11 @@ def test_commit_metadata_head_mismatch_is_indeterminate(self): def test_all_a_pan_is_not_exempted_as_a_placeholder(self): self.assert_blocked("all-a-pan", "privacy scan found") + for scenario in ("grouped-pan-space", "grouped-pan-hyphen"): + with self.subTest(scenario=scenario): + self.assert_blocked(scenario, "privacy scan found") + result = self.run_gate("grouped-masked-pan-space") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) def test_explicit_masked_pan_remains_a_placeholder(self): result = self.run_gate("masked-pan") @@ -1276,7 +1294,7 @@ def test_required_context_diagnostics_are_bounded_for_failures(self): self.assertNotIn("z" * 100, result.stdout) def test_security_review_is_focused_independent_and_current(self): - for scenario in ("security-review-stale", "security-review-author", "security-review-unrelated", "security-review-no-scope", "security-review-hidden", "security-review-hidden-unterminated", "security-review-outsider", "security-review-dismissed", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out"): + for scenario in ("security-review-stale", "security-review-author", "security-review-unrelated", "security-review-no-scope", "security-review-hidden", "security-review-hidden-unterminated", "security-review-outsider", "security-review-dismissed", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade"): with self.subTest(scenario=scenario): self.assert_indeterminate(scenario, "security-focused reviewer comment") result = self.run_gate("security-review-valid") @@ -1313,6 +1331,7 @@ def test_platform_sensitive_paths_need_substantive_both_host_evidence(self): self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assert_blocked("platform-checkbox-comment", "substantive Windows validation") self.assert_blocked("platform-inline-prose", "substantive Windows validation") + self.assert_blocked("platform-negative-outcome", "substantive Windows validation") self.assert_blocked("platform-unaffected-bare", "substantive Windows validation") self.assert_blocked("platform-evidence-bare-label", "substantive Windows validation") self.assert_blocked("platform-evidence-sibling-list", "substantive Windows validation") @@ -1331,6 +1350,9 @@ def test_windows_native_setup_action_needs_both_host_evidence(self): def test_ci_workflow_needs_both_host_evidence(self): self.assert_blocked("platform-ci-workflow", "substantive Windows validation") + def test_release_mcpb_preview_needs_both_host_evidence(self): + self.assert_blocked("platform-release-mcpb-preview", "substantive Windows validation") + def test_ci_workflow_rename_out_needs_both_host_evidence(self): self.assert_blocked("platform-ci-workflow-rename-out", "substantive Windows validation") From 6f958f22c3cd6589c3cd6ae0b8eca2f6af88f163 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 13 Sep 2026 22:42:21 +0530 Subject: [PATCH 13/27] test: avoid privacy scan fixture false positives --- scripts/merge-gate.sh | 2 +- scripts/merge-gate.test.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/scripts/merge-gate.sh b/scripts/merge-gate.sh index e7d2e9698..da168b477 100755 --- a/scripts/merge-gate.sh +++ b/scripts/merge-gate.sh @@ -1167,7 +1167,7 @@ $added" phone_matches=$(grep -Eo '(^|[^[:alnum:]])[6-9]([ ()+._-]{0,3}[0-9]){9}([^[:alnum:]]|$)' <<<"$normalized_whitespace") || phone_status=$? grouped_number_status=0 grouped_number_matches=$(grep -Eo '(^|[^[:alnum:]])[0-9]{4}[ ._-][0-9]{4}[ ._-][0-9]{4}([ ._-][0-9]{4})?([^[:alnum:]]|$)' <<<"$redacted") || grouped_number_status=$? - # A pair of compact dates, such as 0101-2026 0201-2026, has the same four + # A pair of compact dates, such as MMDD-YYYY MMDD-YYYY, has the same four # 4-digit groups as a mixed-separator identifier. Retain the pre-existing # date-range exclusion without weakening actual mixed group detection. grouped_number_non_dates="" diff --git a/scripts/merge-gate.test.py b/scripts/merge-gate.test.py index e54297844..917d75ec4 100644 --- a/scripts/merge-gate.test.py +++ b/scripts/merge-gate.test.py @@ -366,9 +366,10 @@ def next_counter(name): phone = "6" + "6" * 9 emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {phone}\n") elif scenario in {"grouped-identifier-12", "grouped-identifier-16", "grouped-identifier-mixed"}: - identifier = (" ".join(("8421", "7654", "9012")) if scenario.endswith("12") - else ("8421 7654-9012 3456" if scenario == "grouped-identifier-mixed" - else "-".join(("8421", "7654", "9012", "3456")))) + groups = ("8421", "7654", "9012", "3456") + identifier = (" ".join(groups[:3]) if scenario.endswith("12") + else (" ".join(groups[:2]) + "-" + " ".join(groups[2:]) + if scenario == "grouped-identifier-mixed" else "-".join(groups))) emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {identifier}\n") elif scenario == "platform-ci-workflow-rename-out": emit("diff --git a/.github/workflows/ci.yml b/docs/retired-ci.yml\nsimilarity index 100%\nrename from .github/workflows/ci.yml\nrename to docs/retired-ci.yml\n") @@ -450,7 +451,9 @@ def next_counter(name): elif scenario == "separated-dates": emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+2026-09-12 2026-09-13\n") elif scenario == "separated-dates-new-year": - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+0101-2026 0201-2026\n") + first_date = "-".join(("0101", "2026")) + second_date = "-".join(("0201", "2026")) + emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{first_date} {second_date}\n") elif scenario == "separated-dates-year-month": emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+2025-09-11 2025-09-12\n") elif scenario == "adr-identifier": From 3cadc52286dcc9d4b0c34b0661e03ac47cd9c757 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 13 Sep 2026 23:41:13 +0530 Subject: [PATCH 14/27] fix: require security review for bank import credentials --- scripts/merge-gate.sh | 1 + scripts/merge-gate.test.py | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/merge-gate.sh b/scripts/merge-gate.sh index da168b477..216b5e826 100755 --- a/scripts/merge-gate.sh +++ b/scripts/merge-gate.sh @@ -844,6 +844,7 @@ if [ "$files_status" -eq 0 ]; then (if all(.[]; type == "array") then flatten else . end) | any(.[]; [.filename, (.previous_filename? // "")][] | (test("(^|[/_.-])(dsc|credential[s]?|certificate[s]?|keystore|secret[s]?)(?=[/_.-]|$|[A-Z])"; "i") or + test("^scripts/bank_statement_import\\.py$"; "i") or test("^src/AxalScreen\\.tsx$|^src-tauri/src/axal\\.rs$|^src-tauri/src/db/encrypted\\.rs$|^src-tauri/src/documents\\.rs$|^src-tauri/src/commands\\.rs$"; "i"))) ' <<<"$files") fi diff --git a/scripts/merge-gate.test.py b/scripts/merge-gate.test.py index 917d75ec4..13f03d48c 100644 --- a/scripts/merge-gate.test.py +++ b/scripts/merge-gate.test.py @@ -21,7 +21,7 @@ import base64, json, os, sys args = sys.argv[1:] scenario = os.environ.get("GATE_SCENARIO", "pass") -security_case = scenario.startswith("security-review-") or scenario in {"security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade"} +security_case = scenario.startswith("security-review-") or scenario in {"security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade", "security-bank-statement-import"} sync_case = scenario.startswith("sync-") head = "0123456789abcdef0123456789abcdef01234567" new_head = "fedcba9876543210fedcba9876543210fedcba98" @@ -329,7 +329,7 @@ def next_counter(name): if scenario == "security-documents-consumer-rename-out": emit("diff --git a/src-tauri/src/documents.rs b/docs/retired-documents.rs\nsimilarity index 100%\nrename from src-tauri/src/documents.rs\nrename to docs/retired-documents.rs\n") elif security_case: - paths = {"security-axal-frontend": "src/AxalScreen.tsx", "security-axal-native": "src-tauri/src/axal.rs", "security-encrypted-keystore": "src-tauri/src/db/encrypted.rs", "security-documents-consumer": "src-tauri/src/documents.rs", "security-commands-facade": "src-tauri/src/commands.rs"} + paths = {"security-axal-frontend": "src/AxalScreen.tsx", "security-axal-native": "src-tauri/src/axal.rs", "security-encrypted-keystore": "src-tauri/src/db/encrypted.rs", "security-documents-consumer": "src-tauri/src/documents.rs", "security-commands-facade": "src-tauri/src/commands.rs", "security-bank-statement-import": "scripts/bank_statement_import.py"} path = paths.get(scenario, "src-tauri/src/dsc.rs") emit(f"diff --git a/{path} b/{path}\n--- a/{path}\n+++ b/{path}\n@@ -0,0 +1 @@\n+safe check\n") elif sync_case: @@ -582,7 +582,7 @@ def status_page(rows, total_count, state="success", page_head=head): emit([[]]) else: records = [{"user": {"login": "chatgpt-codex-connector[bot]", "type": "Bot"}, "state": "COMMENTED", "commit_id": head}] - if security_case and scenario not in {"security-review-missing", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade"}: + if security_case and scenario not in {"security-review-missing", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade", "security-bank-statement-import"}: record = {"user": {"login": "reviewer", "type": "User"}, "author_association": "COLLABORATOR", "state": "COMMENTED", "commit_id": head, "body": f"Security review: {head}\nResult: accepted\nReviewed credential handling and error redaction."} if scenario == "security-review-stale": record["commit_id"] = new_head if scenario == "security-review-author": record["user"]["login"] = "author" @@ -614,6 +614,8 @@ def status_page(rows, total_count, state="success", page_head=head): emit([[{"filename": "src-tauri/src/documents.rs", "status": "modified", "additions": 1, "deletions": 0}]]) elif scenario == "security-commands-facade": emit([[{"filename": "src-tauri/src/commands.rs", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario == "security-bank-statement-import": + emit([[{"filename": "scripts/bank_statement_import.py", "status": "modified", "additions": 1, "deletions": 0}]]) elif scenario == "security-documents-consumer-rename-out": emit([[{"filename": "docs/retired-documents.rs", "previous_filename": "src-tauri/src/documents.rs", "status": "renamed", "additions": 0, "deletions": 0}]]) elif scenario in {"security-camel-dsc", "security-camel-credential"}: @@ -1297,7 +1299,7 @@ def test_required_context_diagnostics_are_bounded_for_failures(self): self.assertNotIn("z" * 100, result.stdout) def test_security_review_is_focused_independent_and_current(self): - for scenario in ("security-review-stale", "security-review-author", "security-review-unrelated", "security-review-no-scope", "security-review-hidden", "security-review-hidden-unterminated", "security-review-outsider", "security-review-dismissed", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade"): + for scenario in ("security-review-stale", "security-review-author", "security-review-unrelated", "security-review-no-scope", "security-review-hidden", "security-review-hidden-unterminated", "security-review-outsider", "security-review-dismissed", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade", "security-bank-statement-import"): with self.subTest(scenario=scenario): self.assert_indeterminate(scenario, "security-focused reviewer comment") result = self.run_gate("security-review-valid") From 441a7baf08eb5703c0980a6c4bd71e73ed4a049f Mon Sep 17 00:00:00 2001 From: t Date: Mon, 14 Sep 2026 00:35:40 +0530 Subject: [PATCH 15/27] fix: require security review for cache token handling --- scripts/merge-gate.sh | 1 + scripts/merge-gate.test.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/scripts/merge-gate.sh b/scripts/merge-gate.sh index 216b5e826..5c88a5159 100755 --- a/scripts/merge-gate.sh +++ b/scripts/merge-gate.sh @@ -845,6 +845,7 @@ if [ "$files_status" -eq 0 ]; then any(.[]; [.filename, (.previous_filename? // "")][] | (test("(^|[/_.-])(dsc|credential[s]?|certificate[s]?|keystore|secret[s]?)(?=[/_.-]|$|[A-Z])"; "i") or test("^scripts/bank_statement_import\\.py$"; "i") or + test("^scripts/prune-package-compiler-cache\\.mjs$"; "i") or test("^src/AxalScreen\\.tsx$|^src-tauri/src/axal\\.rs$|^src-tauri/src/db/encrypted\\.rs$|^src-tauri/src/documents\\.rs$|^src-tauri/src/commands\\.rs$"; "i"))) ' <<<"$files") fi diff --git a/scripts/merge-gate.test.py b/scripts/merge-gate.test.py index 13f03d48c..006682ab3 100644 --- a/scripts/merge-gate.test.py +++ b/scripts/merge-gate.test.py @@ -21,7 +21,7 @@ import base64, json, os, sys args = sys.argv[1:] scenario = os.environ.get("GATE_SCENARIO", "pass") -security_case = scenario.startswith("security-review-") or scenario in {"security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade", "security-bank-statement-import"} +security_case = scenario.startswith("security-review-") or scenario in {"security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade", "security-bank-statement-import", "security-prune-package-compiler-cache", "security-prune-package-compiler-cache-rename-out"} sync_case = scenario.startswith("sync-") head = "0123456789abcdef0123456789abcdef01234567" new_head = "fedcba9876543210fedcba9876543210fedcba98" @@ -328,8 +328,10 @@ def next_counter(name): elif args[:2] == ["pr", "diff"]: if scenario == "security-documents-consumer-rename-out": emit("diff --git a/src-tauri/src/documents.rs b/docs/retired-documents.rs\nsimilarity index 100%\nrename from src-tauri/src/documents.rs\nrename to docs/retired-documents.rs\n") + elif scenario == "security-prune-package-compiler-cache-rename-out": + emit("diff --git a/scripts/prune-package-compiler-cache.mjs b/docs/retired-cache-pruner.mjs\nsimilarity index 100%\nrename from scripts/prune-package-compiler-cache.mjs\nrename to docs/retired-cache-pruner.mjs\n") elif security_case: - paths = {"security-axal-frontend": "src/AxalScreen.tsx", "security-axal-native": "src-tauri/src/axal.rs", "security-encrypted-keystore": "src-tauri/src/db/encrypted.rs", "security-documents-consumer": "src-tauri/src/documents.rs", "security-commands-facade": "src-tauri/src/commands.rs", "security-bank-statement-import": "scripts/bank_statement_import.py"} + paths = {"security-axal-frontend": "src/AxalScreen.tsx", "security-axal-native": "src-tauri/src/axal.rs", "security-encrypted-keystore": "src-tauri/src/db/encrypted.rs", "security-documents-consumer": "src-tauri/src/documents.rs", "security-commands-facade": "src-tauri/src/commands.rs", "security-bank-statement-import": "scripts/bank_statement_import.py", "security-prune-package-compiler-cache": "scripts/prune-package-compiler-cache.mjs"} path = paths.get(scenario, "src-tauri/src/dsc.rs") emit(f"diff --git a/{path} b/{path}\n--- a/{path}\n+++ b/{path}\n@@ -0,0 +1 @@\n+safe check\n") elif sync_case: @@ -582,7 +584,7 @@ def status_page(rows, total_count, state="success", page_head=head): emit([[]]) else: records = [{"user": {"login": "chatgpt-codex-connector[bot]", "type": "Bot"}, "state": "COMMENTED", "commit_id": head}] - if security_case and scenario not in {"security-review-missing", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade", "security-bank-statement-import"}: + if security_case and scenario not in {"security-review-missing", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade", "security-bank-statement-import", "security-prune-package-compiler-cache", "security-prune-package-compiler-cache-rename-out"}: record = {"user": {"login": "reviewer", "type": "User"}, "author_association": "COLLABORATOR", "state": "COMMENTED", "commit_id": head, "body": f"Security review: {head}\nResult: accepted\nReviewed credential handling and error redaction."} if scenario == "security-review-stale": record["commit_id"] = new_head if scenario == "security-review-author": record["user"]["login"] = "author" @@ -616,6 +618,10 @@ def status_page(rows, total_count, state="success", page_head=head): emit([[{"filename": "src-tauri/src/commands.rs", "status": "modified", "additions": 1, "deletions": 0}]]) elif scenario == "security-bank-statement-import": emit([[{"filename": "scripts/bank_statement_import.py", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario == "security-prune-package-compiler-cache": + emit([[{"filename": "scripts/prune-package-compiler-cache.mjs", "status": "modified", "additions": 1, "deletions": 0}]]) + elif scenario == "security-prune-package-compiler-cache-rename-out": + emit([[{"filename": "docs/retired-cache-pruner.mjs", "previous_filename": "scripts/prune-package-compiler-cache.mjs", "status": "renamed", "additions": 0, "deletions": 0}]]) elif scenario == "security-documents-consumer-rename-out": emit([[{"filename": "docs/retired-documents.rs", "previous_filename": "src-tauri/src/documents.rs", "status": "renamed", "additions": 0, "deletions": 0}]]) elif scenario in {"security-camel-dsc", "security-camel-credential"}: @@ -1299,7 +1305,7 @@ def test_required_context_diagnostics_are_bounded_for_failures(self): self.assertNotIn("z" * 100, result.stdout) def test_security_review_is_focused_independent_and_current(self): - for scenario in ("security-review-stale", "security-review-author", "security-review-unrelated", "security-review-no-scope", "security-review-hidden", "security-review-hidden-unterminated", "security-review-outsider", "security-review-dismissed", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade", "security-bank-statement-import"): + for scenario in ("security-review-stale", "security-review-author", "security-review-unrelated", "security-review-no-scope", "security-review-hidden", "security-review-hidden-unterminated", "security-review-outsider", "security-review-dismissed", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade", "security-bank-statement-import", "security-prune-package-compiler-cache", "security-prune-package-compiler-cache-rename-out"): with self.subTest(scenario=scenario): self.assert_indeterminate(scenario, "security-focused reviewer comment") result = self.run_gate("security-review-valid") From 204c4b528f30fe8288048aaa07e3fea4660b5cdc Mon Sep 17 00:00:00 2001 From: t Date: Mon, 14 Sep 2026 01:27:52 +0530 Subject: [PATCH 16/27] fix: strengthen merge-gate security evidence --- scripts/merge-gate.sh | 26 +++++++++++++++++++------- scripts/merge-gate.test.py | 37 +++++++++++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/scripts/merge-gate.sh b/scripts/merge-gate.sh index 5c88a5159..86543dffb 100755 --- a/scripts/merge-gate.sh +++ b/scripts/merge-gate.sh @@ -825,14 +825,14 @@ if [ "$files_status" -eq 0 ]; then security_sensitive_change=$(jq -r ' (if all(.[]; type == "array") then flatten else . end) | any(.[]; [ .filename, (.previous_filename? // "") ][] | - ascii_downcase | test("^src-tauri/(crates|src)/|^src/|^docs/(tally|agent)/|^scripts/(bank_statement_import|sanitise-bbox-capture)|(^|/)[^/]*(dsc|credential|tally)[^/]*(/|$)")) + ascii_downcase | test("^src-tauri/(crates|src)/|^src/|^docs/(tally|agent)/|^scripts/(bank_statement_import|sanitise-bbox-capture|prune-package-compiler-cache)|^\\.github/workflows/(ci\\.yml|release-mcpb-preview\\.yml)$|(^|/)[^/]*(dsc|credential|tally)[^/]*(/|$)")) ' <<<"$files") fi if [ "$security_sensitive_change" = "true" ]; then if ! body_section_has_content "$prbody" 'security impact|security implications' true; then - bad "DSC, Tally, or credential path change lacks non-empty security-impact notes" + bad "credential-sensitive path change lacks non-empty security-impact notes" else - say "ok" "DSC, Tally, or credential path change includes security-impact notes" + say "ok" "credential-sensitive path change includes security-impact notes" fi fi @@ -846,6 +846,7 @@ if [ "$files_status" -eq 0 ]; then (test("(^|[/_.-])(dsc|credential[s]?|certificate[s]?|keystore|secret[s]?)(?=[/_.-]|$|[A-Z])"; "i") or test("^scripts/bank_statement_import\\.py$"; "i") or test("^scripts/prune-package-compiler-cache\\.mjs$"; "i") or + test("^\\.github/workflows/(ci\\.yml|release-mcpb-preview\\.yml)$"; "i") or test("^src/AxalScreen\\.tsx$|^src-tauri/src/axal\\.rs$|^src-tauri/src/db/encrypted\\.rs$|^src-tauri/src/documents\\.rs$|^src-tauri/src/commands\\.rs$"; "i"))) ' <<<"$files") fi @@ -864,17 +865,24 @@ if [ "$security_reviewer_change" = "true" ]; then ((.author_association == "OWNER" or .author_association == "MEMBER" or .author_association == "COLLABORATOR") or (.user.login == "chatgpt-codex-connector[bot]" and .user.type == "Bot")); def visible_body: .body | gsub("(?s:|$))"; ""); + def substantive: + gsub("^[[:space:]]+|[[:space:]]+$"; "") as $text | + ($text | length >= 12) and + ($text | test("^(n/?a|none|tbd|todo|pending|unknown|not applicable)[[:space:].,:;!?-]*$"; "i") | not); + def has_substantive_line($label): + [try (capture("(?im)^#{0,6} *(?:" + $label + ")[ ]*:[ ]*(?.+)$").value | select(substantive)) catch empty] | length > 0; def focused: (.body | type == "string") and (visible_body | test("(?im)^#{0,6} *security review: *" + $head + " *$")) and (visible_body | test("(?im)^result: *accepted *$")) and - (visible_body | test("(?im)^#{0,6} *reviewed (dsc|credential|certificate|keystore|secret)")); + (visible_body | has_substantive_line("reviewed +(dsc|credential|certificate|keystore|secret)([ ]+[A-Za-z][A-Za-z-]*)?")) and + (visible_body | has_substantive_line("security rationale|security reasoning")); ([($reviews | source_records | records)[] | select(reviewer and focused and .commit_id == $head and (.state == "APPROVED" or .state == "COMMENTED"))] + [($comments | source_records | records)[] | select(reviewer and focused)]) | length > 0 ' 2>/dev/null) || security_review=false fi if [ "$security_review" != "true" ]; then - unknown "DSC or credential change lacks a separate current-head security-focused reviewer comment" + unknown "credential-sensitive change lacks a separate current-head security-focused reviewer comment" else say "ok" "separate security-focused reviewer comment names full current head $short" fi @@ -1158,7 +1166,7 @@ $added" # Join separators only inside recognised identifier shapes. A global # separator-free projection fuses unrelated values and creates false # identifiers, including adjacent date fragments. Alongside mobile numbers, - # accept only 4-4-4 and 4-4-4-4 grouped long-number forms. + # accept only bounded 0XX-XXXX-XXXX landlines and 4-4-4(/4) long-number forms. phone_status=0 normalized_status=0 normalized_whitespace=$(python3 -c 'import sys, unicodedata; print("".join(" " if char == "\t" or unicodedata.category(char) == "Zs" else char for char in sys.stdin.read()), end="")' <<<"$redacted") || normalized_status=$? @@ -1167,6 +1175,8 @@ $added" normalized_whitespace="" fi phone_matches=$(grep -Eo '(^|[^[:alnum:]])[6-9]([ ()+._-]{0,3}[0-9]){9}([^[:alnum:]]|$)' <<<"$normalized_whitespace") || phone_status=$? + landline_status=0 + landline_matches=$(grep -Eo '(^|[^[:alnum:]])0[1-9][0-9][ ._-][0-9]{4}[ ._-][0-9]{4}([^[:alnum:]]|$)' <<<"$normalized_whitespace") || landline_status=$? grouped_number_status=0 grouped_number_matches=$(grep -Eo '(^|[^[:alnum:]])[0-9]{4}[ ._-][0-9]{4}[ ._-][0-9]{4}([ ._-][0-9]{4})?([^[:alnum:]]|$)' <<<"$redacted") || grouped_number_status=$? # A pair of compact dates, such as MMDD-YYYY MMDD-YYYY, has the same four @@ -1182,13 +1192,15 @@ $added" grouped_number_non_dates+="${candidate}"$'\n' done <<<"$grouped_number_matches" grouped_number_matches="$grouped_number_non_dates" - if [ "$redaction_status" -ne 0 ] || [ "$normalized_status" -ne 0 ] || [ "$phone_status" -gt 1 ] || [ "$grouped_number_status" -gt 1 ]; then + if [ "$redaction_status" -ne 0 ] || [ "$normalized_status" -ne 0 ] || [ "$phone_status" -gt 1 ] || [ "$landline_status" -gt 1 ] || [ "$grouped_number_status" -gt 1 ]; then unknown "formatted identifier scan expression failed" fi normalized_phone=$(sed -E 's/[^0-9]//g' <<<"$phone_matches") + normalized_landlines=$(sed -E 's/[^0-9]//g' <<<"$landline_matches") normalized_grouped_numbers=$(sed -E 's/[^0-9]//g' <<<"$grouped_number_matches") scan_shapes="$redacted $normalized_phone +$normalized_landlines $normalized_grouped_numbers" hits_status=0 hits=$(count_nonplaceholder '[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]{3}|[A-Z]{5}[ -][0-9]{4}[ -][A-Z]|[A-Z]{5}[0-9]{4}[A-Z]|[6-9][0-9]{9}' "$scan_shapes") || hits_status=$? diff --git a/scripts/merge-gate.test.py b/scripts/merge-gate.test.py index 006682ab3..b08ec9ebb 100644 --- a/scripts/merge-gate.test.py +++ b/scripts/merge-gate.test.py @@ -21,7 +21,8 @@ import base64, json, os, sys args = sys.argv[1:] scenario = os.environ.get("GATE_SCENARIO", "pass") -security_case = scenario.startswith("security-review-") or scenario in {"security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade", "security-bank-statement-import", "security-prune-package-compiler-cache", "security-prune-package-compiler-cache-rename-out"} +security_case = scenario.startswith("security-review-") or scenario in {"security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade", "security-bank-statement-import", "security-prune-package-compiler-cache", "security-prune-package-compiler-cache-rename-out", "security-ci-workflow", "security-ci-workflow-rename-out", "security-ci-workflow-valid", "security-release-preview", "security-release-preview-rename-out"} +security_workflow_case = scenario in {"security-ci-workflow", "security-ci-workflow-rename-out", "security-ci-workflow-valid", "security-release-preview", "security-release-preview-rename-out"} sync_case = scenario.startswith("sync-") head = "0123456789abcdef0123456789abcdef01234567" new_head = "fedcba9876543210fedcba9876543210fedcba98" @@ -224,6 +225,8 @@ def next_counter(name): "\n- Windows validation evidence: Windows CI passed `python3 scripts/merge-gate.test.py`.\n" "- macOS validation evidence: macOS CI passed `python3 scripts/merge-gate.test.py`.\n" ) + if security_workflow_case: + body += "\n## Rollback notes\n\nRevert the workflow change before the next release.\n" if scenario == "security-encrypted-keystore": body += "\n## Rollback notes\n\nRevert the encrypted-store change before deployment.\n" if scenario in {"platform-evidence-present", "platform-powershell-evidence", "migration-template-wrapped", "security-notes-present", "security-none", "security-pending", "security-review-valid", "sync-migration-present"}: @@ -330,8 +333,12 @@ def next_counter(name): emit("diff --git a/src-tauri/src/documents.rs b/docs/retired-documents.rs\nsimilarity index 100%\nrename from src-tauri/src/documents.rs\nrename to docs/retired-documents.rs\n") elif scenario == "security-prune-package-compiler-cache-rename-out": emit("diff --git a/scripts/prune-package-compiler-cache.mjs b/docs/retired-cache-pruner.mjs\nsimilarity index 100%\nrename from scripts/prune-package-compiler-cache.mjs\nrename to docs/retired-cache-pruner.mjs\n") + elif scenario == "security-ci-workflow-rename-out": + emit("diff --git a/.github/workflows/ci.yml b/docs/retired-ci.yml\nsimilarity index 100%\nrename from .github/workflows/ci.yml\nrename to docs/retired-ci.yml\n") + elif scenario == "security-release-preview-rename-out": + emit("diff --git a/.github/workflows/release-mcpb-preview.yml b/docs/retired-preview.yml\nsimilarity index 100%\nrename from .github/workflows/release-mcpb-preview.yml\nrename to docs/retired-preview.yml\n") elif security_case: - paths = {"security-axal-frontend": "src/AxalScreen.tsx", "security-axal-native": "src-tauri/src/axal.rs", "security-encrypted-keystore": "src-tauri/src/db/encrypted.rs", "security-documents-consumer": "src-tauri/src/documents.rs", "security-commands-facade": "src-tauri/src/commands.rs", "security-bank-statement-import": "scripts/bank_statement_import.py", "security-prune-package-compiler-cache": "scripts/prune-package-compiler-cache.mjs"} + paths = {"security-axal-frontend": "src/AxalScreen.tsx", "security-axal-native": "src-tauri/src/axal.rs", "security-encrypted-keystore": "src-tauri/src/db/encrypted.rs", "security-documents-consumer": "src-tauri/src/documents.rs", "security-commands-facade": "src-tauri/src/commands.rs", "security-bank-statement-import": "scripts/bank_statement_import.py", "security-prune-package-compiler-cache": "scripts/prune-package-compiler-cache.mjs", "security-ci-workflow": ".github/workflows/ci.yml", "security-ci-workflow-valid": ".github/workflows/ci.yml", "security-release-preview": ".github/workflows/release-mcpb-preview.yml"} path = paths.get(scenario, "src-tauri/src/dsc.rs") emit(f"diff --git a/{path} b/{path}\n--- a/{path}\n+++ b/{path}\n@@ -0,0 +1 @@\n+safe check\n") elif sync_case: @@ -394,6 +401,9 @@ def next_counter(name): "phone-parenthesized": "(" + "69876" + ") 54321", } emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {phones[scenario]}\n") + elif scenario == "landline-grouped": + landline = "0" + "11" + "-" + "2345" + "-" + "6789" + emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {landline}\n") elif scenario == "unicode-phone": emit("diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic 69876\u00a054321\n") elif scenario == "unicode-phone-tab": @@ -584,12 +594,14 @@ def status_page(rows, total_count, state="success", page_head=head): emit([[]]) else: records = [{"user": {"login": "chatgpt-codex-connector[bot]", "type": "Bot"}, "state": "COMMENTED", "commit_id": head}] - if security_case and scenario not in {"security-review-missing", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade", "security-bank-statement-import", "security-prune-package-compiler-cache", "security-prune-package-compiler-cache-rename-out"}: - record = {"user": {"login": "reviewer", "type": "User"}, "author_association": "COLLABORATOR", "state": "COMMENTED", "commit_id": head, "body": f"Security review: {head}\nResult: accepted\nReviewed credential handling and error redaction."} + if security_case and scenario not in {"security-review-missing", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-commands-facade", "security-bank-statement-import", "security-prune-package-compiler-cache", "security-prune-package-compiler-cache-rename-out", "security-ci-workflow", "security-ci-workflow-rename-out", "security-release-preview", "security-release-preview-rename-out"}: + record = {"user": {"login": "reviewer", "type": "User"}, "author_association": "COLLABORATOR", "state": "COMMENTED", "commit_id": head, "body": f"Security review: {head}\nResult: accepted\nReviewed credential handling: token diagnostics remain redacted.\nSecurity rationale: the current access boundary prevents a cache token from reaching logs."} if scenario == "security-review-stale": record["commit_id"] = new_head if scenario == "security-review-author": record["user"]["login"] = "author" if scenario == "security-review-unrelated": record["body"] = "Looks good" if scenario == "security-review-no-scope": record["body"] = f"Security review: {head}\nResult: accepted" + if scenario == "security-review-bare-scope": record["body"] = f"Security review: {head}\nResult: accepted\nReviewed credential:\nSecurity rationale: the current access boundary prevents a cache token from reaching logs." + if scenario == "security-review-placeholder-rationale": record["body"] = f"Security review: {head}\nResult: accepted\nReviewed credential handling: token diagnostics remain redacted.\nSecurity rationale: TBD" if scenario == "security-review-hidden": record["body"] = f"" if scenario == "security-review-hidden-unterminated": record["body"] = f"") - if scenario == "validation-html-comment": - body = body.replace("`python3 scripts/merge-gate.test.py`", "Tests not run ") - if scenario == "unterminated-html-comment": - body = body.replace("`python3 scripts/merge-gate.test.py`", "" - if scenario == "body-comment-drift" and view_count > 0: - body += "\n" - if scenario == "validation-tool-prose": - body = body.replace("`python3 scripts/merge-gate.test.py`", "Tests not run; cargo is available") - if scenario == "validation-placeholder-command": - body = body.replace("`python3 scripts/merge-gate.test.py`", "`cargo ...`") - if scenario == "dependency-manifest-present": - body += "\n## Dependency justification\n\nThe parser library is required for the sealed response format.\n" - if scenario == "checklist-heading": - body = body.replace("#L10", "#L1") - if scenario == "checklist-anchor-suffix": - body = body.replace("#L10", "#L10junk") - if scenario in {"implementation-p4-present", "implementation-p4-continuation", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-negative-outcome", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "migration-rollback-present", "migration-template-wrapped", "security-notes-present", "security-none", "security-pending", "security-review-valid"}: - body += ( - "\n## Scope, reuse, and impact\n\n" - "- Existing component reused: the existing gate parser and file inventory.\n" - "- What is deleted (or why no deletion is justified): no duplicate path remains.\n" - "- What breaks if this is not built: unsafe evidence could reach a merge.\n" - ) - if scenario == "implementation-p4-continuation": - body = body.replace( - "- Existing component reused: the existing gate parser and file inventory.\n" - "- What is deleted (or why no deletion is justified): no duplicate path remains.\n" - "- What breaks if this is not built: unsafe evidence could reach a merge.\n", - "- Existing component reused:\n the existing gate parser and file inventory.\n" - "- What is deleted (or why no deletion is justified):\n no duplicate path remains.\n" - "- What breaks if this is not built:\n unsafe evidence could reach a merge.\n", - ) - if security_case or scenario in {"surface-unpins", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"}: - body += ( - "\n## Scope, reuse, and impact\n\n" - "- Existing component reused: the existing gate parser and file inventory.\n" - "- What is deleted (or why no deletion is justified): no duplicate path remains.\n" - "- What breaks if this is not built: unsafe evidence could reach a merge.\n" - "\n## Security impact\n\nNo credential material is added.\n" - "\n## Migration compatibility\n\nExisting callers retain their paths and formats.\n" - "\n- Windows validation evidence: Windows CI passed `python3 scripts/merge-gate.test.py`.\n" - "- macOS validation evidence: macOS CI passed `python3 scripts/merge-gate.test.py`.\n" - ) - if security_workflow_case: - body += "\n## Rollback notes\n\nRevert the workflow change before the next release.\n" - if scenario in {"security-tauri-cargo", "security-tauri-cargo-rename-out"}: - body += "\n## Dependency rationale\n\nThe manifest boundary remains under independent credential-focused review.\n" - if scenario == "security-encrypted-keystore": - body += "\n## Rollback notes\n\nRevert the encrypted-store change before deployment.\n" - if scenario in {"platform-evidence-present", "platform-powershell-evidence", "migration-template-wrapped", "security-notes-present", "security-none", "security-pending", "security-review-valid", "sync-migration-present"}: - body += ( - "\n- Windows validation evidence: Windows CI passed `python3 scripts/merge-gate.test.py`.\n" - "- macOS validation evidence: macOS CI passed `python3 scripts/merge-gate.test.py`.\n" - ) - if scenario == "platform-evidence-heading": - body += ( - "\n### Windows validation evidence\n\n" - "`python3 scripts/merge-gate.test.py` passed on Windows CI.\n" - "\n### macOS validation evidence\n\n" - "`python3 scripts/merge-gate.test.py` passed on macOS CI.\n" - ) - if scenario == "platform-checkbox-evidence": - body += ( - "\n- [x] Native Windows validation completed: `python3 scripts/merge-gate.test.py` passed on Windows CI.\n" - "- [x] Native macOS validation completed: `python3 scripts/merge-gate.test.py` passed on macOS CI.\n" - ) - if scenario == "platform-checkbox-comment": - body += ( - "\n- [x] Native Windows validation completed: \n" - "- [x] Native macOS validation completed: \n" - ) - if scenario == "platform-inline-prose": - body += ( - "\n- Windows validation evidence: reviewed by the release team.\n" - "- macOS validation evidence: reviewed by the release team.\n" - ) - if scenario == "platform-negative-outcome": - body += "\n- Windows validation evidence: Windows CI did not pass.\n- macOS validation evidence: macOS CI did not pass.\n" - if scenario == "platform-unaffected-bare": - body += "\n- Windows validation evidence: unaffected\n- macOS validation evidence: unaffected\n" - if scenario == "platform-unaffected-rationale": - body += "\n- Windows validation evidence: unaffected because this changes shared documentation only.\n- macOS validation evidence: unaffected because this changes shared documentation only.\n" - if scenario in {"platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager"}: - body += ( - "\n## Security impact\n\nNo credential material is added.\n" - "\n## Migration compatibility\n\nExisting callers retain their paths and formats.\n" - ) - if scenario == "migration-rollback-present": - body += "\n## Rollback notes\n\nRevert the migration commit before deployment.\n" - if scenario == "migration-template-wrapped": - body += ( - "\n- Migration/sync compatibility and rollback procedure (required when an\n" - " existing workflow changes): Existing readers retain the old format; revert this commit before deployment.\n" - ) - if scenario == "migration-template-other-field": - body += ( - "\n- Migration/sync compatibility and rollback procedure (required when an\n" - " existing workflow changes):\n" - "- Destructive database migration: No\n" - ) - if scenario == "p4-placeholders": - body += ( - "\n## Scope, reuse, and impact\n\n" - "- Existing component reused: N/A\n" - "- What is deleted (or why no deletion is justified): TODO\n" - "- What breaks if this is not built: pending\n" - ) - if scenario == "workflow-placeholders": - body += "\n## Rollback notes\n\nN/A\n\n## Migration compatibility\n\nTBD\n" - if scenario == "workflow-punctuated-placeholders": - body += "\n## Rollback notes\n\nN/A.\n\n## Migration compatibility\n\nTBD.\n" - if scenario == "platform-evidence-bare-label": - body += "\n- Windows validation evidence\n- macOS validation evidence\n" - if scenario == "platform-evidence-sibling-list": - body += "\n- Windows validation evidence\n- Notes: tracked separately\n- macOS validation evidence\n- Notes: tracked separately\n" - if scenario == "platform-evidence-package-manager": - body += "\n- Windows validation evidence\n- Package manager: pnpm\n- macOS validation evidence\n- Package manager: pnpm\n" - if scenario == "platform-evidence-empty-fence": - body += "\n### Windows validation evidence\n~~~bash\n~~~\n### macOS validation evidence\n~~~bash\n~~~\n" - if scenario == "platform-evidence-punctuated-placeholder": - body += "\n- Windows validation evidence: N/A.\n- macOS validation evidence: TBD.\n" - if scenario == "platform-evidence-fenced-continuation": - body += "\n### Windows validation evidence\n~~~text\nWindows CI passed `python3 scripts/merge-gate.test.py`.\n~~~\n### macOS validation evidence\n~~~text\nmacOS CI passed `python3 scripts/merge-gate.test.py`.\n~~~\n" - body = body.replace("blob/HEAD", f"blob/{head}") - if scenario == "checklist-stale-ref": - body = body.replace(f"blob/{head}", "blob/" + "f" * 40) - one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "grouped-identifier-mixed", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "landline-grouped", "landline-standard-hyphen", "landline-standard-space", "landline-standard-underscore", "pem-certificate-envelope", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "grouped-pan-space", "grouped-pan-hyphen", "grouped-masked-pan-space", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "workflow-punctuated-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "dependency-manifest-missing", "dependency-manifest-present", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "gitlink-existing", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-negative-outcome", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager", "platform-windows-native-action", "platform-windows-native-action-rename-out", "platform-ci-workflow", "platform-ci-workflow-rename-out", "platform-release-mcpb-preview", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case - selected_base = new_head if scenario == "base-oid-mismatch" else base - emit({"headRefOid": selected_head, "baseRefOid": selected_base, "baseRefName": "master", - "mergeable": "MERGEABLE", "mergeStateStatus": final_state, - "isDraft": False, "state": "OPEN", "title": title, - "body": body, "changedFiles": 1 if one_file else 2}) -elif args[:2] == ["pr", "checks"]: - if scenario == "checks-silent": - raise SystemExit(0) - if scenario == "cancel-check": - emit([{"bucket": "cancel", "name": "Required checks"}]) - elif scenario == "required-skip": - emit([{"bucket": "skipping", "name": "Required checks"}, {"bucket": "pass", "name": "Rust format"}]) - elif scenario == "missing-required": - emit([{"bucket": "pass", "name": "Required checks"}]) - else: - emit([{"bucket": "pass", "name": "Frontend build"}, - {"bucket": "pass", "name": "Rust format"}, - {"bucket": "pass", "name": "GitGuardian Security Checks"}, - {"bucket": "pass", "name": "Dependency security"}, - {"bucket": "pass", "name": "Required checks"}, - {"bucket": "skipping", "name": "Optional documentation"}]) -elif args[:2] == ["pr", "diff"]: - if scenario == "security-documents-consumer-rename-out": - emit("diff --git a/src-tauri/src/documents.rs b/docs/retired-documents.rs\nsimilarity index 100%\nrename from src-tauri/src/documents.rs\nrename to docs/retired-documents.rs\n") - elif scenario == "security-prune-package-compiler-cache-rename-out": - emit("diff --git a/scripts/prune-package-compiler-cache.mjs b/docs/retired-cache-pruner.mjs\nsimilarity index 100%\nrename from scripts/prune-package-compiler-cache.mjs\nrename to docs/retired-cache-pruner.mjs\n") - elif scenario == "security-ci-workflow-rename-out": - emit("diff --git a/.github/workflows/ci.yml b/docs/retired-ci.yml\nsimilarity index 100%\nrename from .github/workflows/ci.yml\nrename to docs/retired-ci.yml\n") - elif scenario == "security-release-preview-rename-out": - emit("diff --git a/.github/workflows/release-mcpb-preview.yml b/docs/retired-preview.yml\nsimilarity index 100%\nrename from .github/workflows/release-mcpb-preview.yml\nrename to docs/retired-preview.yml\n") - elif scenario == "security-deploy-install-page-rename-out": - emit("diff --git a/.github/workflows/deploy-install-page.yml b/docs/retired-install-page.yml\nsimilarity index 100%\nrename from .github/workflows/deploy-install-page.yml\nrename to docs/retired-install-page.yml\n") - elif scenario == "security-documents-screen-rename-out": - emit("diff --git a/src/DocumentsScreen.tsx b/docs/retired-documents-screen.tsx\nsimilarity index 100%\nrename from src/DocumentsScreen.tsx\nrename to docs/retired-documents-screen.tsx\n") - elif scenario == "security-tauri-cargo-rename-out": - emit("diff --git a/src-tauri/Cargo.toml b/docs/retired-tauri-cargo.toml\nsimilarity index 100%\nrename from src-tauri/Cargo.toml\nrename to docs/retired-tauri-cargo.toml\n") - elif scenario == "security-tauri-lib-rename-out": - emit("diff --git a/src-tauri/src/lib.rs b/docs/retired-tauri-lib.rs\nsimilarity index 100%\nrename from src-tauri/src/lib.rs\nrename to docs/retired-tauri-lib.rs\n") - elif security_case: - paths = {"security-axal-frontend": "src/AxalScreen.tsx", "security-axal-native": "src-tauri/src/axal.rs", "security-encrypted-keystore": "src-tauri/src/db/encrypted.rs", "security-documents-consumer": "src-tauri/src/documents.rs", "security-documents-screen": "src/DocumentsScreen.tsx", "security-documents-screen-valid": "src/DocumentsScreen.tsx", "security-tauri-cargo": "src-tauri/Cargo.toml", "security-tauri-lib": "src-tauri/src/lib.rs", "security-commands-facade": "src-tauri/src/commands.rs", "security-bank-statement-import": "scripts/bank_statement_import.py", "security-prune-package-compiler-cache": "scripts/prune-package-compiler-cache.mjs", "security-ci-workflow": ".github/workflows/ci.yml", "security-ci-workflow-valid": ".github/workflows/ci.yml", "security-release-preview": ".github/workflows/release-mcpb-preview.yml", "security-deploy-install-page": ".github/workflows/deploy-install-page.yml", "security-deploy-install-page-valid": ".github/workflows/deploy-install-page.yml"} - path = paths.get(scenario, "src-tauri/src/dsc.rs") - emit(f"diff --git a/{path} b/{path}\n--- a/{path}\n+++ b/{path}\n@@ -0,0 +1 @@\n+safe check\n") - elif sync_case: - emit("diff --git a/src-tauri/src/sync.rs b/docs/retired.rs\nsimilarity index 100%\nrename from src-tauri/src/sync.rs\nrename to docs/retired.rs\n") - elif scenario == "formatted-phone": - phone = "+91 " + "98765" + "-43210" - emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+Call {phone}\n") - elif scenario == "path-id": - path_id = "ABCDE" + "1234" + "F" - emit(f"diff --git a/docs/safe.md b/docs/{path_id}.md\n--- a/docs/safe.md\n+++ b/docs/{path_id}.md\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario in {"security-notes-missing", "security-notes-present", "security-none", "security-pending"}: - emit("diff --git a/src-tauri/src/tally/runtime.rs b/src-tauri/src/tally/runtime.rs\n--- a/src-tauri/src/tally/runtime.rs\n+++ b/src-tauri/src/tally/runtime.rs\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario in {"security-crate", "security-agent-import", "security-dsc"}: - paths = {"security-crate": "src-tauri/crates/bridge-tally-core/src/master_binding.rs", "security-agent-import": "src-tauri/src/agent_import.rs", "security-dsc": "src-tauri/src/dsc.rs"} - emit(f"diff --git a/{paths[scenario]} b/{paths[scenario]}\n--- a/{paths[scenario]}\n+++ /dev/null\n@@ -1 +0,0 @@\n-safe text\n") - elif scenario == "security-rename-out": - emit("diff --git a/src-tauri/src/tally/runtime.rs b/src/runtime.rs\nsimilarity index 100%\nrename from src-tauri/src/tally/runtime.rs\nrename to src/runtime.rs\n") - elif scenario.startswith("home-"): - root_home = bytes((47, 114, 111, 111, 116)).decode("ascii") - unicode_user = chr(0x03BB) + chr(0x00E9) - homes = {"home-macos": "/" + "Users" + "/" + "tester" + "/work", "home-unix": "/" + "home" + "/" + "tester" + "/work", "home-root": root_home + "/work/customer.pem", "home-root-home": root_home, "home-windows": "C:" + "\\" + "Users" + "\\" + "tester" + "\\work", "home-macos-root": "/" + "Users" + "/" + "tester", "home-unix-root": "/" + "home" + "/" + "tester", "home-windows-forward": "C:" + "/" + "Users" + "/" + "tester" + "/work", "home-windows-escaped": "C:" + "\\\\" + "Users" + "\\\\" + "tester" + "\\\\work", "home-macos-unicode": "/" + "Users" + "/" + unicode_user + "/work", "home-unix-unicode": "/" + "home" + "/" + unicode_user + "/work", "home-windows-unicode": "C:" + "\\" + "Users" + "\\" + unicode_user + "\\work", "home-regex-source": "mac_home=" + "'/'" + '"Users"' + "'/[A-Za-z0-9._-]+'"} - emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{homes[scenario]}\n") - elif scenario == "binary-delete": - emit("diff --git a/docs/old.png b/docs/old.png\nBinary files a/docs/old.png and /dev/null differ\n") - elif scenario in {"binary-review", "binary-review-private", "binary-review-head-moves"}: - emit("diff --git a/docs/new.png b/docs/new.png\nBinary files /dev/null and b/docs/new.png differ\n") - elif scenario == "diff-omits-file": - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario == "diff-truncated-payload": - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +2 @@\n+first line\n") - elif scenario == "formatted-phone-grouped": - phone = "6" + "98 765-4321" - emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {phone}\n") - elif scenario == "repeated-phone": - phone = "6" + "6" * 9 - emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {phone}\n") - elif scenario in {"grouped-identifier-12", "grouped-identifier-16", "grouped-identifier-mixed"}: - groups = ("8421", "7654", "9012", "3456") - identifier = (" ".join(groups[:3]) if scenario.endswith("12") - else (" ".join(groups[:2]) + "-" + " ".join(groups[2:]) - if scenario == "grouped-identifier-mixed" else "-".join(groups))) - emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {identifier}\n") - elif scenario == "platform-ci-workflow-rename-out": - emit("diff --git a/.github/workflows/ci.yml b/docs/retired-ci.yml\nsimilarity index 100%\nrename from .github/workflows/ci.yml\nrename to docs/retired-ci.yml\n") - elif scenario == "platform-release-mcpb-preview": - emit("diff --git a/.github/workflows/release-mcpb-preview.yml b/.github/workflows/release-mcpb-preview.yml\n--- a/.github/workflows/release-mcpb-preview.yml\n+++ b/.github/workflows/release-mcpb-preview.yml\n@@ -0,0 +1 @@\n+safe workflow text\n") - elif scenario in {"workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration", "platform-ci-workflow"}: - emit("diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml\n--- a/.github/workflows/ci.yml\n+++ b/.github/workflows/ci.yml\n@@ -0,0 +1 @@\n+safe workflow text\n") - elif scenario in {"dependency-manifest-missing", "dependency-manifest-present"}: - emit("diff --git a/package.json b/package.json\n--- a/package.json\n+++ b/package.json\n@@ -0,0 +1 @@\n+safe dependency metadata\n") - elif scenario in {"workflow-delete", "workflow-delete-notes"}: - emit("diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml\n--- a/.github/workflows/ci.yml\n+++ /dev/null\n@@ -1 +0,0 @@\n-safe workflow text\n") - elif scenario in {"workflow-rename-out", "workflow-rename-out-notes"}: - emit("diff --git a/.github/workflows/ci.yml b/docs/retired-ci.yml\nsimilarity index 100%\nrename from .github/workflows/ci.yml\nrename to docs/retired-ci.yml\n") - elif scenario in {"phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized"}: - phones = { - "phone-space": "6" + "9876 54321", - "phone-dot": "6" + "987.654.321", - "phone-plus": "6" + "9876+54321", - "phone-underscore": "6" + "987_654_321", - "phone-parenthesized": "(" + "69876" + ") 54321", - } - emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {phones[scenario]}\n") - elif scenario == "landline-grouped": - landline = "0" + "11" + "-" + "2345" + "-" + "6789" - emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {landline}\n") - elif scenario in {"landline-standard-hyphen", "landline-standard-space", "landline-standard-underscore"}: - separator = {"landline-standard-hyphen": "-", "landline-standard-space": " ", "landline-standard-underscore": "_"}[scenario] - landline = "0" + "11" + separator + "23456789" - emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {landline}\n") - elif scenario == "pem-certificate-envelope": - begin = "-" * 5 + "BEGIN CERTIFICATE" + "-" * 5 - end = "-" * 5 + "END CERTIFICATE" + "-" * 5 - body = "MII" + "A" * 48 - emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +3 @@\n+{begin}\n+{body}\n+{end}\n") - elif scenario == "unicode-phone": - emit("diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic 69876\u00a054321\n") - elif scenario == "unicode-phone-tab": - emit("diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic 69876\t54321\n") - elif scenario == "unicode-phone-two-lines": - emit("diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +2 @@\n+69876\n+54321\n") - elif scenario == "metadata-only": - emit("diff --git a/docs/example.md b/docs/example.md\nsimilarity index 100%\nrename from docs/example.md\nrename to docs/example.md\n") - elif scenario == "metadata-private": - private_path = "docs/" + "/" + "Users" + "/tester/" + "x" * 300 - emit(f"diff --git a/{private_path} b/{private_path}\nsimilarity index 100%\nrename from {private_path}\nrename to {private_path}\n") - elif scenario == "metadata-incomplete": - emit("diff --git a/docs/example.md b/docs/example.md\nsimilarity index 100%\nrename from docs/example.md\nrename to docs/example.md\n") - elif scenario == "hunk-header-phone": - phone = "6" + "9876" + "54321" - emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+++ b/synthetic {phone}\n") - elif scenario == "hunk-header-literals": - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +2 @@\n+++ b/safe\n+++ /dev/null\n") - elif scenario == "hunk-binary-literal": - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+GIT binary patch\n") - elif scenario == "all-a-pan": - identifier = "AAAAA" + "1234" + "A" - emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{identifier}\n") - elif scenario == "masked-pan": - identifier = "XXXXX" + "1234" + "X" - emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{identifier}\n") - elif scenario in {"grouped-pan-space", "grouped-pan-hyphen", "grouped-masked-pan-space"}: - separator = "-" if scenario == "grouped-pan-hyphen" else " " - prefix = "XXXXX" if scenario == "grouped-masked-pan-space" else "ABCDE" - suffix = "X" if scenario == "grouped-masked-pan-space" else "A" - emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{prefix}{separator}1234{separator}{suffix}\n") - elif scenario == "quoted-path": - emit('diff --git "a/docs/caf\\303\\251.md" "b/docs/caf\\303\\251.md"\n--- "a/docs/caf\\303\\251.md"\n+++ "b/docs/caf\\303\\251.md"\n@@ -0,0 +1 @@\n+safe text\n') - elif scenario == "crlf-diff": - emit("diff --git a/docs/example.md b/docs/example.md\r\n--- a/docs/example.md\r\n+++ b/docs/example.md\r\n@@ -0,0 +1 @@\r\n+safe text\r\n") - elif scenario == "ambiguous-unquoted-path": - emit("diff --git a/docs/a b/example.md b/docs/a b/example.md\n--- a/docs/a b/example.md\n+++ b/docs/a b/example.md\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario == "ambiguous-rename-path": - emit("diff --git a/docs/a b/example.md b/docs/a b/example.md\nsimilarity index 100%\nrename from docs/a b/example.md\nrename to docs/a b/example.md\n") - elif scenario == "gitlink": - emit("diff --git a/vendor/module b/vendor/module\nnew file mode 160000\nindex 0000000..2222222\n--- /dev/null\n+++ b/vendor/module\n@@ -0,0 +1 @@\n+Subproject commit 2222222\n") - elif scenario == "gitlink-existing": - emit("diff --git a/vendor/module b/vendor/module\nindex 1111111..2222222 160000\n--- a/vendor/module\n+++ b/vendor/module\n@@ -1 +1 @@\n-Subproject commit 1111111\n+Subproject commit 2222222\n") - elif scenario in {"implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders"}: - suffix = {"implementation-p4-shell": "sh", "implementation-p4-powershell": "ps1", "implementation-p4-sql": "sql"}.get(scenario, "py") - emit(f"diff --git a/scripts/example.{suffix} b/scripts/example.{suffix}\n--- a/scripts/example.{suffix}\n+++ b/scripts/example.{suffix}\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-negative-outcome", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager"}: - emit("diff --git a/src-tauri/src/local_files/paths.rs b/src-tauri/src/local_files/paths.rs\n--- a/src-tauri/src/local_files/paths.rs\n+++ b/src-tauri/src/local_files/paths.rs\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario == "platform-windows-native-action": - emit("diff --git a/.github/actions/setup-windows-native/action.yml b/.github/actions/setup-windows-native/action.yml\n--- a/.github/actions/setup-windows-native/action.yml\n+++ b/.github/actions/setup-windows-native/action.yml\n@@ -0,0 +1 @@\n+safe action text\n") - elif scenario == "platform-windows-native-action-rename-out": - emit("diff --git a/.github/actions/setup-windows-native/action.yml b/docs/retired-windows-native-action.yml\nsimilarity index 100%\nrename from .github/actions/setup-windows-native/action.yml\nrename to docs/retired-windows-native-action.yml\n") - elif scenario in {"platform-powershell-missing", "platform-powershell-evidence"}: - emit("diff --git a/scripts/signing.ps1 b/scripts/signing.ps1\n--- a/scripts/signing.ps1\n+++ b/scripts/signing.ps1\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario in {"migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"}: - emit("diff --git a/src-tauri/migrations/001.sql b/src-tauri/migrations/001.sql\n--- a/src-tauri/migrations/001.sql\n+++ b/src-tauri/migrations/001.sql\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario == "separated-dates": - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+2026-09-12 2026-09-13\n") - elif scenario == "separated-dates-new-year": - first_date = "-".join(("0101", "2026")) - second_date = "-".join(("0201", "2026")) - emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{first_date} {second_date}\n") - elif scenario == "separated-dates-year-month": - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+2025-09-11 2025-09-12\n") - elif scenario == "adr-identifier": - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+CE_ADR_0016_E\n") - elif scenario == "surface-unpins": - emit("diff --git a/src/example.rs b/src/example.rs\n--- a/src/example.rs\n+++ b/src/example.rs\n@@ -0,0 +1 @@\n+safe text\n" - "diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json\n" - "--- a/docs/tally/compatibility/compatibility-surface.json\n+++ b/docs/tally/compatibility/compatibility-surface.json\n@@ -0,0 +1 @@\n+safe manifest\n") - else: - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+safe text\ndiff --git a/docs/second.md b/docs/second.md\n--- a/docs/second.md\n+++ b/docs/second.md\n@@ -0,0 +1 @@\n+other text\n") -elif args and args[0] == "api": - joined = " ".join(args) - if "graphql" in args: - if scenario == "threads-first-empty-cursor-rejected" and "cursor=" in args: - fail("the first review-thread request must omit cursor") - def thread_nodes(start, count, unresolved=False): - return [{"id": f"thread-{index}", "isResolved": not (unresolved and index == start)} - for index in range(start, start + count)] - has_cursor = "C1" in joined - if scenario == "threads-empty-more": - nodes, page_info = [], {"hasNextPage": True, "endCursor": "C1"} - elif scenario == "threads-short": - nodes, page_info = thread_nodes(1, 1), {"hasNextPage": False, "endCursor": None} - elif scenario == "threads-malformed-pagination": - nodes, page_info = thread_nodes(1, 100), {"hasNextPage": True, "endCursor": None} - elif has_cursor: - if scenario == "threads-duplicate-id": - nodes = [{"id": "thread-1", "isResolved": True}] - else: - nodes = thread_nodes(101, 1, scenario == "threads-unresolved-second") - page_info = {"hasNextPage": False, "endCursor": None} - else: - nodes, page_info = thread_nodes(1, 100), {"hasNextPage": True, "endCursor": "C1"} - emit({"data": {"repository": {"pullRequest": {"reviewThreads": { - "totalCount": 101.5 if scenario == "threads-fractional" else (102 if scenario == "threads-total-drift" and has_cursor else 101), - "pageInfo": page_info, "nodes": nodes - }}}}}) - elif "/compare/" in joined: - if scenario == "lineage-mismatch": - emit({"status": "diverged", "behind_by": 1, - "merge_base_commit": {"sha": new_head}}) - else: - emit({"status": "ahead", "behind_by": 0, - "merge_base_commit": {"sha": base}}) - elif "/commits/" in joined and "/check-runs" in joined: - check_call = next_counter("GATE_CHECK_COUNTER") - if scenario == "check-run-wrong-head": - run_head = new_head - else: - run_head = head - total_count = 3 if scenario == "check-run-count-mismatch" else 2 - second_id = 1 if scenario == "check-run-duplicate-id" else 2 - run_status = "queued" if scenario == "check-run-incomplete" else "completed" - conclusion = "failure" if scenario in {"check-run-failed", "late-check-run-failure"} and (scenario != "late-check-run-failure" or check_call > 0) else ("skipped" if scenario in {"check-run-required-skip", "check-run-optional-skip"} else "success") - run_name = "Optional changed after rollup" if scenario in {"check-run-failed", "check-run-optional-skip"} else "Required checks" - emit([{"total_count": total_count, "check_runs": [ - {"id": 1, "name": run_name, "head_sha": run_head, "status": run_status, "conclusion": conclusion}, - {"id": second_id, "name": "Rust format", "head_sha": run_head, "status": "completed", "conclusion": "success"}]}]) - elif "/commits/" in joined and "/status?" in joined: - status_call = next_counter("GATE_STATUS_COUNTER") - def status_page(rows, total_count, state="success", page_head=head): - return {"sha": page_head, "state": state, "total_count": total_count, "statuses": rows} - if scenario == "status-two-page": - emit([status_page([{"id": 1, "context": "legacy one", "state": "success"}], 2), - status_page([{"id": 2, "context": "legacy two", "state": "success"}], 2)]) - elif scenario == "status-truncated": - emit([{"sha": head, "state": "success", "total_count": 1, "statuses": []}]) - elif scenario == "status-duplicate": - row = {"id": 1, "context": "legacy one", "state": "success"} - emit([{"sha": head, "state": "success", "total_count": 2, "statuses": [row]}, - {"sha": head, "state": "success", "total_count": 2, "statuses": [row]}]) - elif scenario == "status-mixed-head": - emit([{"sha": head, "state": "success", "total_count": 2, "statuses": [{"id": 1, "context": "legacy one", "state": "success"}]}, - {"sha": new_head, "state": "success", "total_count": 2, "statuses": [{"id": 2, "context": "legacy two", "state": "success"}]}]) - elif scenario == "status-failed-context" or (scenario == "late-status-failure" and status_call > 0): - emit([{"sha": head, "state": "failure", "total_count": 1, "statuses": [{"id": 1, "context": "legacy optional", "state": "failure"}]}]) - elif scenario == "status-nonempty-pending": - emit([{"sha": head, "state": "pending", "total_count": 1, "statuses": [{"id": 1, "context": "queued", "state": "pending"}]}]) - elif scenario == "status-empty-pending": - emit([{"sha": head, "state": "pending", "total_count": 0, "statuses": []}]) - elif scenario == "status-malformed": - emit([{"sha": head, "state": "success", "total_count": "0", "statuses": []}]) - elif scenario == "status-failed-combined": - emit([{"sha": head, "state": "failure", "total_count": 1, "statuses": [{"id": 1, "context": "legacy failed", "state": "failure"}]}]) - else: - emit([{"sha": head, "state": "success", "total_count": 0, "statuses": []}]) - elif "/pulls/321/commits" in joined: - message = "safe commit metadata" - if scenario == "metadata-commit-id": - message = "Customer " + "ABCDE" + "1234" + "F" - identity = {"name": "Maintainer", "email": "maintainer@example.invalid"} - if scenario == "metadata-author-id": - identity = {"name": "ABCDE" + "1234" + "F", "email": "maintainer@example.invalid"} - linked_author = None if scenario == "metadata-unlinked-identities" else {"login": "author"} - linked_committer = None if scenario == "metadata-unlinked-identities" else {"login": "committer"} - commits = [{"sha": head, "commit": {"message": message, "author": identity, "committer": identity}, - "author": linked_author, "committer": linked_committer}] - if scenario == "metadata-duplicate": - commits.append({"sha": head, "commit": {"message": message}}) - emit([commits]) - elif any(arg.endswith("/pulls/321") for arg in args): - commits = 1 - metadata_head = head - if scenario == "metadata-capped": - commits = 251 - elif scenario in {"metadata-truncated", "metadata-duplicate"}: - commits = 2 - elif scenario == "metadata-head-mismatch": - metadata_head = new_head - emit({"commits": commits, "head": {"sha": metadata_head}, "user": {"login": "author"}}) - elif "branches/master/protection/required_status_checks" in joined: - contexts = [ - "Frontend build", "Rust format", "GitGuardian Security Checks", - "Dependency security", "Required checks"] - if scenario == "protection-missing-gitguardian": - contexts = [context for context in contexts if context != "GitGuardian Security Checks"] - if scenario == "required-context-output-bound": - contexts += ["control-" + str(i) + "-" + "z" * 10000 for i in range(50)] - if scenario == "required-context-unexpected": - contexts += ["Unexpected required context"] - emit({"strict": scenario != "protection-nonstrict", "contexts": contexts, "checks": []}) - elif "branches/master" in joined: - emit(base) - elif "/pulls/321/reviews" in joined: - if scenario in {"short-review", "summary-only", "manual-summary"}: - emit([[]]) - else: - records = [{"user": {"login": "chatgpt-codex-connector[bot]", "type": "Bot"}, "state": "COMMENTED", "commit_id": head}] - if security_case and scenario not in {"security-review-missing", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-documents-screen", "security-documents-screen-rename-out", "security-tauri-cargo", "security-tauri-cargo-rename-out", "security-tauri-lib", "security-tauri-lib-rename-out", "security-commands-facade", "security-bank-statement-import", "security-prune-package-compiler-cache", "security-prune-package-compiler-cache-rename-out", "security-ci-workflow", "security-ci-workflow-rename-out", "security-release-preview", "security-release-preview-rename-out", "security-deploy-install-page", "security-deploy-install-page-rename-out"}: - record = {"user": {"login": "reviewer", "type": "User"}, "author_association": "COLLABORATOR", "state": "COMMENTED", "commit_id": head, "body": f"Security review: {head}\nResult: accepted\nReviewed credential handling: token diagnostics remain redacted.\nSecurity rationale: the current access boundary prevents a cache token from reaching logs."} - if scenario == "security-review-stale": record["commit_id"] = new_head - if scenario == "security-review-author": record["user"]["login"] = "author" - if scenario == "security-review-unrelated": record["body"] = "Looks good" - if scenario == "security-review-no-scope": record["body"] = f"Security review: {head}\nResult: accepted" - if scenario == "security-review-bare-scope": record["body"] = f"Security review: {head}\nResult: accepted\nReviewed credential:\nSecurity rationale: the current access boundary prevents a cache token from reaching logs." - if scenario == "security-review-placeholder-rationale": record["body"] = f"Security review: {head}\nResult: accepted\nReviewed credential handling: token diagnostics remain redacted.\nSecurity rationale: TBD" - if scenario == "security-review-hidden": record["body"] = f"" - if scenario == "security-review-hidden-unterminated": record["body"] = f"") + if scenario == "validation-html-comment": + body = body.replace("`python3 scripts/merge-gate.test.py`", "Tests not run ") + if scenario == "unterminated-html-comment": + body = body.replace("`python3 scripts/merge-gate.test.py`", "" + if scenario == "body-comment-drift" and view_count > 0: + body += "\n" + if scenario == "validation-tool-prose": + body = body.replace("`python3 scripts/merge-gate.test.py`", "Tests not run; cargo is available") + if scenario == "validation-placeholder-command": + body = body.replace("`python3 scripts/merge-gate.test.py`", "`cargo ...`") + if scenario == "dependency-manifest-present": + body += "\n## Dependency justification\n\nThe parser library is required for the sealed response format.\n" + if scenario == "checklist-heading": + body = body.replace("#L10", "#L1") + if scenario == "checklist-anchor-suffix": + body = body.replace("#L10", "#L10junk") + if scenario in {"implementation-p4-present", "implementation-p4-continuation", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-negative-outcome", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "migration-rollback-present", "migration-template-wrapped", "security-notes-present", "security-none", "security-pending", "security-review-valid"}: + body += ( + "\n## Scope, reuse, and impact\n\n" + "- Existing component reused: the existing gate parser and file inventory.\n" + "- What is deleted (or why no deletion is justified): no duplicate path remains.\n" + "- What breaks if this is not built: unsafe evidence could reach a merge.\n" + ) + if scenario == "implementation-p4-continuation": + body = body.replace( + "- Existing component reused: the existing gate parser and file inventory.\n" + "- What is deleted (or why no deletion is justified): no duplicate path remains.\n" + "- What breaks if this is not built: unsafe evidence could reach a merge.\n", + "- Existing component reused:\n the existing gate parser and file inventory.\n" + "- What is deleted (or why no deletion is justified):\n no duplicate path remains.\n" + "- What breaks if this is not built:\n unsafe evidence could reach a merge.\n", + ) + if security_case or scenario in {"surface-unpins", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"}: + body += ( + "\n## Scope, reuse, and impact\n\n" + "- Existing component reused: the existing gate parser and file inventory.\n" + "- What is deleted (or why no deletion is justified): no duplicate path remains.\n" + "- What breaks if this is not built: unsafe evidence could reach a merge.\n" + "\n## Security impact\n\nNo credential material is added.\n" + "\n## Migration compatibility\n\nExisting callers retain their paths and formats.\n" + "\n- Windows validation evidence: Windows CI passed `python3 scripts/merge-gate.test.py`.\n" + "- macOS validation evidence: macOS CI passed `python3 scripts/merge-gate.test.py`.\n" + ) + if security_workflow_case: + body += "\n## Rollback notes\n\nRevert the workflow change before the next release.\n" + if scenario in {"security-tauri-cargo", "security-tauri-cargo-rename-out"}: + body += "\n## Dependency rationale\n\nThe manifest boundary remains under independent credential-focused review.\n" + if scenario == "security-encrypted-keystore": + body += "\n## Rollback notes\n\nRevert the encrypted-store change before deployment.\n" + if scenario in {"platform-evidence-present", "platform-powershell-evidence", "migration-template-wrapped", "security-notes-present", "security-none", "security-pending", "security-review-valid", "sync-migration-present"}: + body += ( + "\n- Windows validation evidence: Windows CI passed `python3 scripts/merge-gate.test.py`.\n" + "- macOS validation evidence: macOS CI passed `python3 scripts/merge-gate.test.py`.\n" + ) + if scenario == "platform-evidence-heading": + body += ( + "\n### Windows validation evidence\n\n" + "`python3 scripts/merge-gate.test.py` passed on Windows CI.\n" + "\n### macOS validation evidence\n\n" + "`python3 scripts/merge-gate.test.py` passed on macOS CI.\n" + ) + if scenario == "platform-checkbox-evidence": + body += ( + "\n- [x] Native Windows validation completed: `python3 scripts/merge-gate.test.py` passed on Windows CI.\n" + "- [x] Native macOS validation completed: `python3 scripts/merge-gate.test.py` passed on macOS CI.\n" + ) + if scenario == "platform-checkbox-comment": + body += ( + "\n- [x] Native Windows validation completed: \n" + "- [x] Native macOS validation completed: \n" + ) + if scenario == "platform-inline-prose": + body += ( + "\n- Windows validation evidence: reviewed by the release team.\n" + "- macOS validation evidence: reviewed by the release team.\n" + ) + if scenario == "platform-negative-outcome": + body += "\n- Windows validation evidence: Windows CI did not pass.\n- macOS validation evidence: macOS CI did not pass.\n" + if scenario == "platform-unaffected-bare": + body += "\n- Windows validation evidence: unaffected\n- macOS validation evidence: unaffected\n" + if scenario == "platform-unaffected-rationale": + body += "\n- Windows validation evidence: unaffected because this changes shared documentation only.\n- macOS validation evidence: unaffected because this changes shared documentation only.\n" + if scenario in {"platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager"}: + body += ( + "\n## Security impact\n\nNo credential material is added.\n" + "\n## Migration compatibility\n\nExisting callers retain their paths and formats.\n" + ) + if scenario == "migration-rollback-present": + body += "\n## Rollback notes\n\nRevert the migration commit before deployment.\n" + if scenario == "migration-template-wrapped": + body += ( + "\n- Migration/sync compatibility and rollback procedure (required when an\n" + " existing workflow changes): Existing readers retain the old format; revert this commit before deployment.\n" + ) + if scenario == "migration-template-other-field": + body += ( + "\n- Migration/sync compatibility and rollback procedure (required when an\n" + " existing workflow changes):\n" + "- Destructive database migration: No\n" + ) + if scenario == "p4-placeholders": + body += ( + "\n## Scope, reuse, and impact\n\n" + "- Existing component reused: N/A\n" + "- What is deleted (or why no deletion is justified): TODO\n" + "- What breaks if this is not built: pending\n" + ) + if scenario == "workflow-placeholders": + body += "\n## Rollback notes\n\nN/A\n\n## Migration compatibility\n\nTBD\n" + if scenario == "workflow-punctuated-placeholders": + body += "\n## Rollback notes\n\nN/A.\n\n## Migration compatibility\n\nTBD.\n" + if scenario == "platform-evidence-bare-label": + body += "\n- Windows validation evidence\n- macOS validation evidence\n" + if scenario == "platform-evidence-sibling-list": + body += "\n- Windows validation evidence\n- Notes: tracked separately\n- macOS validation evidence\n- Notes: tracked separately\n" + if scenario == "platform-evidence-package-manager": + body += "\n- Windows validation evidence\n- Package manager: pnpm\n- macOS validation evidence\n- Package manager: pnpm\n" + if scenario == "platform-evidence-empty-fence": + body += "\n### Windows validation evidence\n~~~bash\n~~~\n### macOS validation evidence\n~~~bash\n~~~\n" + if scenario == "platform-evidence-punctuated-placeholder": + body += "\n- Windows validation evidence: N/A.\n- macOS validation evidence: TBD.\n" + if scenario == "platform-evidence-fenced-continuation": + body += "\n### Windows validation evidence\n~~~text\nWindows CI passed `python3 scripts/merge-gate.test.py`.\n~~~\n### macOS validation evidence\n~~~text\nmacOS CI passed `python3 scripts/merge-gate.test.py`.\n~~~\n" + body = body.replace("blob/HEAD", f"blob/{head}") + if scenario == "checklist-stale-ref": + body = body.replace(f"blob/{head}", "blob/" + "f" * 40) + one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "grouped-identifier-mixed", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "landline-grouped", "landline-standard-hyphen", "landline-standard-space", "landline-standard-underscore", "pem-certificate-envelope", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "grouped-pan-space", "grouped-pan-hyphen", "grouped-masked-pan-space", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "workflow-punctuated-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "dependency-manifest-missing", "dependency-manifest-present", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "gitlink-existing", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-negative-outcome", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager", "platform-windows-native-action", "platform-windows-native-action-rename-out", "platform-ci-workflow", "platform-ci-workflow-rename-out", "platform-release-mcpb-preview", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case + selected_base = new_head if scenario == "base-oid-mismatch" else base + emit({"headRefOid": selected_head, "baseRefOid": selected_base, "baseRefName": "master", + "mergeable": "MERGEABLE", "mergeStateStatus": final_state, + "isDraft": False, "state": "OPEN", "title": title, + "body": body, "changedFiles": 1 if one_file else 2}) +elif args[:2] == ["pr", "checks"]: + if scenario == "checks-silent": + raise SystemExit(0) + if scenario == "cancel-check": + emit([{"bucket": "cancel", "name": "Required checks"}]) + elif scenario == "required-skip": + emit([{"bucket": "skipping", "name": "Required checks"}, {"bucket": "pass", "name": "Rust format"}]) + elif scenario == "missing-required": + emit([{"bucket": "pass", "name": "Required checks"}]) + else: + emit([{"bucket": "pass", "name": "Frontend build"}, + {"bucket": "pass", "name": "Rust format"}, + {"bucket": "pass", "name": "GitGuardian Security Checks"}, + {"bucket": "pass", "name": "Dependency security"}, + {"bucket": "pass", "name": "Required checks"}, + {"bucket": "skipping", "name": "Optional documentation"}]) +elif args[:2] == ["pr", "diff"]: + if scenario == "security-documents-consumer-rename-out": + emit("diff --git a/src-tauri/src/documents.rs b/docs/retired-documents.rs\nsimilarity index 100%\nrename from src-tauri/src/documents.rs\nrename to docs/retired-documents.rs\n") + elif scenario == "security-prune-package-compiler-cache-rename-out": + emit("diff --git a/scripts/prune-package-compiler-cache.mjs b/docs/retired-cache-pruner.mjs\nsimilarity index 100%\nrename from scripts/prune-package-compiler-cache.mjs\nrename to docs/retired-cache-pruner.mjs\n") + elif scenario == "security-ci-workflow-rename-out": + emit("diff --git a/.github/workflows/ci.yml b/docs/retired-ci.yml\nsimilarity index 100%\nrename from .github/workflows/ci.yml\nrename to docs/retired-ci.yml\n") + elif scenario == "security-release-preview-rename-out": + emit("diff --git a/.github/workflows/release-mcpb-preview.yml b/docs/retired-preview.yml\nsimilarity index 100%\nrename from .github/workflows/release-mcpb-preview.yml\nrename to docs/retired-preview.yml\n") + elif scenario == "security-deploy-install-page-rename-out": + emit("diff --git a/.github/workflows/deploy-install-page.yml b/docs/retired-install-page.yml\nsimilarity index 100%\nrename from .github/workflows/deploy-install-page.yml\nrename to docs/retired-install-page.yml\n") + elif scenario == "security-documents-screen-rename-out": + emit("diff --git a/src/DocumentsScreen.tsx b/docs/retired-documents-screen.tsx\nsimilarity index 100%\nrename from src/DocumentsScreen.tsx\nrename to docs/retired-documents-screen.tsx\n") + elif scenario == "security-tauri-cargo-rename-out": + emit("diff --git a/src-tauri/Cargo.toml b/docs/retired-tauri-cargo.toml\nsimilarity index 100%\nrename from src-tauri/Cargo.toml\nrename to docs/retired-tauri-cargo.toml\n") + elif scenario == "security-tauri-lib-rename-out": + emit("diff --git a/src-tauri/src/lib.rs b/docs/retired-tauri-lib.rs\nsimilarity index 100%\nrename from src-tauri/src/lib.rs\nrename to docs/retired-tauri-lib.rs\n") + elif security_case: + paths = {"security-axal-frontend": "src/AxalScreen.tsx", "security-axal-native": "src-tauri/src/axal.rs", "security-encrypted-keystore": "src-tauri/src/db/encrypted.rs", "security-documents-consumer": "src-tauri/src/documents.rs", "security-documents-screen": "src/DocumentsScreen.tsx", "security-documents-screen-valid": "src/DocumentsScreen.tsx", "security-tauri-cargo": "src-tauri/Cargo.toml", "security-tauri-lib": "src-tauri/src/lib.rs", "security-commands-facade": "src-tauri/src/commands.rs", "security-bank-statement-import": "scripts/bank_statement_import.py", "security-prune-package-compiler-cache": "scripts/prune-package-compiler-cache.mjs", "security-ci-workflow": ".github/workflows/ci.yml", "security-ci-workflow-valid": ".github/workflows/ci.yml", "security-release-preview": ".github/workflows/release-mcpb-preview.yml", "security-deploy-install-page": ".github/workflows/deploy-install-page.yml", "security-deploy-install-page-valid": ".github/workflows/deploy-install-page.yml"} + path = paths.get(scenario, "src-tauri/src/dsc.rs") + emit(f"diff --git a/{path} b/{path}\n--- a/{path}\n+++ b/{path}\n@@ -0,0 +1 @@\n+safe check\n") + elif sync_case: + emit("diff --git a/src-tauri/src/sync.rs b/docs/retired.rs\nsimilarity index 100%\nrename from src-tauri/src/sync.rs\nrename to docs/retired.rs\n") + elif scenario == "formatted-phone": + phone = "+91 " + "98765" + "-43210" + emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+Call {phone}\n") + elif scenario == "path-id": + path_id = "ABCDE" + "1234" + "F" + emit(f"diff --git a/docs/safe.md b/docs/{path_id}.md\n--- a/docs/safe.md\n+++ b/docs/{path_id}.md\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario in {"security-notes-missing", "security-notes-present", "security-none", "security-pending"}: + emit("diff --git a/src-tauri/src/tally/runtime.rs b/src-tauri/src/tally/runtime.rs\n--- a/src-tauri/src/tally/runtime.rs\n+++ b/src-tauri/src/tally/runtime.rs\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario in {"security-crate", "security-agent-import", "security-dsc"}: + paths = {"security-crate": "src-tauri/crates/bridge-tally-core/src/master_binding.rs", "security-agent-import": "src-tauri/src/agent_import.rs", "security-dsc": "src-tauri/src/dsc.rs"} + emit(f"diff --git a/{paths[scenario]} b/{paths[scenario]}\n--- a/{paths[scenario]}\n+++ /dev/null\n@@ -1 +0,0 @@\n-safe text\n") + elif scenario == "security-rename-out": + emit("diff --git a/src-tauri/src/tally/runtime.rs b/src/runtime.rs\nsimilarity index 100%\nrename from src-tauri/src/tally/runtime.rs\nrename to src/runtime.rs\n") + elif scenario.startswith("home-"): + root_home = bytes((47, 114, 111, 111, 116)).decode("ascii") + unicode_user = chr(0x03BB) + chr(0x00E9) + homes = {"home-macos": "/" + "Users" + "/" + "tester" + "/work", "home-unix": "/" + "home" + "/" + "tester" + "/work", "home-root": root_home + "/work/customer.pem", "home-root-home": root_home, "home-windows": "C:" + "\\" + "Users" + "\\" + "tester" + "\\work", "home-macos-root": "/" + "Users" + "/" + "tester", "home-unix-root": "/" + "home" + "/" + "tester", "home-windows-forward": "C:" + "/" + "Users" + "/" + "tester" + "/work", "home-windows-escaped": "C:" + "\\\\" + "Users" + "\\\\" + "tester" + "\\\\work", "home-macos-unicode": "/" + "Users" + "/" + unicode_user + "/work", "home-unix-unicode": "/" + "home" + "/" + unicode_user + "/work", "home-windows-unicode": "C:" + "\\" + "Users" + "\\" + unicode_user + "\\work", "home-regex-source": "mac_home=" + "'/'" + '"Users"' + "'/[A-Za-z0-9._-]+'"} + emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{homes[scenario]}\n") + elif scenario == "binary-delete": + emit("diff --git a/docs/old.png b/docs/old.png\nBinary files a/docs/old.png and /dev/null differ\n") + elif scenario in {"binary-review", "binary-review-private", "binary-review-head-moves"}: + emit("diff --git a/docs/new.png b/docs/new.png\nBinary files /dev/null and b/docs/new.png differ\n") + elif scenario == "diff-omits-file": + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario == "diff-truncated-payload": + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +2 @@\n+first line\n") + elif scenario == "formatted-phone-grouped": + phone = "6" + "98 765-4321" + emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {phone}\n") + elif scenario == "repeated-phone": + phone = "6" + "6" * 9 + emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {phone}\n") + elif scenario in {"grouped-identifier-12", "grouped-identifier-16", "grouped-identifier-mixed"}: + groups = ("8421", "7654", "9012", "3456") + identifier = (" ".join(groups[:3]) if scenario.endswith("12") + else (" ".join(groups[:2]) + "-" + " ".join(groups[2:]) + if scenario == "grouped-identifier-mixed" else "-".join(groups))) + emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {identifier}\n") + elif scenario == "platform-ci-workflow-rename-out": + emit("diff --git a/.github/workflows/ci.yml b/docs/retired-ci.yml\nsimilarity index 100%\nrename from .github/workflows/ci.yml\nrename to docs/retired-ci.yml\n") + elif scenario == "platform-release-mcpb-preview": + emit("diff --git a/.github/workflows/release-mcpb-preview.yml b/.github/workflows/release-mcpb-preview.yml\n--- a/.github/workflows/release-mcpb-preview.yml\n+++ b/.github/workflows/release-mcpb-preview.yml\n@@ -0,0 +1 @@\n+safe workflow text\n") + elif scenario in {"workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration", "platform-ci-workflow"}: + emit("diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml\n--- a/.github/workflows/ci.yml\n+++ b/.github/workflows/ci.yml\n@@ -0,0 +1 @@\n+safe workflow text\n") + elif scenario in {"dependency-manifest-missing", "dependency-manifest-present"}: + emit("diff --git a/package.json b/package.json\n--- a/package.json\n+++ b/package.json\n@@ -0,0 +1 @@\n+safe dependency metadata\n") + elif scenario in {"workflow-delete", "workflow-delete-notes"}: + emit("diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml\n--- a/.github/workflows/ci.yml\n+++ /dev/null\n@@ -1 +0,0 @@\n-safe workflow text\n") + elif scenario in {"workflow-rename-out", "workflow-rename-out-notes"}: + emit("diff --git a/.github/workflows/ci.yml b/docs/retired-ci.yml\nsimilarity index 100%\nrename from .github/workflows/ci.yml\nrename to docs/retired-ci.yml\n") + elif scenario in {"phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized"}: + phones = { + "phone-space": "6" + "9876 54321", + "phone-dot": "6" + "987.654.321", + "phone-plus": "6" + "9876+54321", + "phone-underscore": "6" + "987_654_321", + "phone-parenthesized": "(" + "69876" + ") 54321", + } + emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {phones[scenario]}\n") + elif scenario == "landline-grouped": + landline = "0" + "11" + "-" + "2345" + "-" + "6789" + emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {landline}\n") + elif scenario in {"landline-standard-hyphen", "landline-standard-space", "landline-standard-underscore"}: + separator = {"landline-standard-hyphen": "-", "landline-standard-space": " ", "landline-standard-underscore": "_"}[scenario] + landline = "0" + "11" + separator + "23456789" + emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {landline}\n") + elif scenario == "landline-standard-four-digit": + landline = "0" + "120" + "-" + "2345678" + emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {landline}\n") + elif scenario == "pem-certificate-envelope": + begin = "-" * 5 + "BEGIN CERTIFICATE" + "-" * 5 + end = "-" * 5 + "END CERTIFICATE" + "-" * 5 + body = "MII" + "A" * 48 + emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +3 @@\n+{begin}\n+{body}\n+{end}\n") + elif scenario == "trusted-pem-certificate-envelope": + begin = "-" * 5 + "BEGIN TRUSTED CERTIFICATE" + "-" * 5 + end = "-" * 5 + "END TRUSTED CERTIFICATE" + "-" * 5 + body = "MII" + "B" * 48 + emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +3 @@\n+{begin}\n+{body}\n+{end}\n") + elif scenario == "unicode-phone": + emit("diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic 69876\u00a054321\n") + elif scenario == "unicode-phone-tab": + emit("diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic 69876\t54321\n") + elif scenario == "unicode-phone-two-lines": + emit("diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +2 @@\n+69876\n+54321\n") + elif scenario == "metadata-only": + emit("diff --git a/docs/example.md b/docs/example.md\nsimilarity index 100%\nrename from docs/example.md\nrename to docs/example.md\n") + elif scenario == "metadata-private": + private_path = "docs/" + "/" + "Users" + "/tester/" + "x" * 300 + emit(f"diff --git a/{private_path} b/{private_path}\nsimilarity index 100%\nrename from {private_path}\nrename to {private_path}\n") + elif scenario == "metadata-incomplete": + emit("diff --git a/docs/example.md b/docs/example.md\nsimilarity index 100%\nrename from docs/example.md\nrename to docs/example.md\n") + elif scenario == "hunk-header-phone": + phone = "6" + "9876" + "54321" + emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+++ b/synthetic {phone}\n") + elif scenario == "hunk-header-literals": + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +2 @@\n+++ b/safe\n+++ /dev/null\n") + elif scenario == "hunk-binary-literal": + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+GIT binary patch\n") + elif scenario == "all-a-pan": + identifier = "AAAAA" + "1234" + "A" + emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{identifier}\n") + elif scenario == "masked-pan": + identifier = "XXXXX" + "1234" + "X" + emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{identifier}\n") + elif scenario in {"grouped-pan-space", "grouped-pan-hyphen", "grouped-masked-pan-space"}: + separator = "-" if scenario == "grouped-pan-hyphen" else " " + prefix = "XXXXX" if scenario == "grouped-masked-pan-space" else "ABCDE" + suffix = "X" if scenario == "grouped-masked-pan-space" else "A" + emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{prefix}{separator}1234{separator}{suffix}\n") + elif scenario == "quoted-path": + emit('diff --git "a/docs/caf\\303\\251.md" "b/docs/caf\\303\\251.md"\n--- "a/docs/caf\\303\\251.md"\n+++ "b/docs/caf\\303\\251.md"\n@@ -0,0 +1 @@\n+safe text\n') + elif scenario == "crlf-diff": + emit("diff --git a/docs/example.md b/docs/example.md\r\n--- a/docs/example.md\r\n+++ b/docs/example.md\r\n@@ -0,0 +1 @@\r\n+safe text\r\n") + elif scenario == "ambiguous-unquoted-path": + emit("diff --git a/docs/a b/example.md b/docs/a b/example.md\n--- a/docs/a b/example.md\n+++ b/docs/a b/example.md\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario == "ambiguous-rename-path": + emit("diff --git a/docs/a b/example.md b/docs/a b/example.md\nsimilarity index 100%\nrename from docs/a b/example.md\nrename to docs/a b/example.md\n") + elif scenario == "gitlink": + emit("diff --git a/vendor/module b/vendor/module\nnew file mode 160000\nindex 0000000..2222222\n--- /dev/null\n+++ b/vendor/module\n@@ -0,0 +1 @@\n+Subproject commit 2222222\n") + elif scenario == "gitlink-existing": + emit("diff --git a/vendor/module b/vendor/module\nindex 1111111..2222222 160000\n--- a/vendor/module\n+++ b/vendor/module\n@@ -1 +1 @@\n-Subproject commit 1111111\n+Subproject commit 2222222\n") + elif scenario in {"implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders"}: + suffix = {"implementation-p4-shell": "sh", "implementation-p4-powershell": "ps1", "implementation-p4-sql": "sql"}.get(scenario, "py") + emit(f"diff --git a/scripts/example.{suffix} b/scripts/example.{suffix}\n--- a/scripts/example.{suffix}\n+++ b/scripts/example.{suffix}\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-negative-outcome", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager"}: + emit("diff --git a/src-tauri/src/local_files/paths.rs b/src-tauri/src/local_files/paths.rs\n--- a/src-tauri/src/local_files/paths.rs\n+++ b/src-tauri/src/local_files/paths.rs\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario == "platform-windows-native-action": + emit("diff --git a/.github/actions/setup-windows-native/action.yml b/.github/actions/setup-windows-native/action.yml\n--- a/.github/actions/setup-windows-native/action.yml\n+++ b/.github/actions/setup-windows-native/action.yml\n@@ -0,0 +1 @@\n+safe action text\n") + elif scenario == "platform-windows-native-action-rename-out": + emit("diff --git a/.github/actions/setup-windows-native/action.yml b/docs/retired-windows-native-action.yml\nsimilarity index 100%\nrename from .github/actions/setup-windows-native/action.yml\nrename to docs/retired-windows-native-action.yml\n") + elif scenario in {"platform-powershell-missing", "platform-powershell-evidence"}: + emit("diff --git a/scripts/signing.ps1 b/scripts/signing.ps1\n--- a/scripts/signing.ps1\n+++ b/scripts/signing.ps1\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario in {"migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"}: + emit("diff --git a/src-tauri/migrations/001.sql b/src-tauri/migrations/001.sql\n--- a/src-tauri/migrations/001.sql\n+++ b/src-tauri/migrations/001.sql\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario == "separated-dates": + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+2026-09-12 2026-09-13\n") + elif scenario == "separated-dates-new-year": + first_date = "-".join(("0101", "2026")) + second_date = "-".join(("0201", "2026")) + emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{first_date} {second_date}\n") + elif scenario == "separated-dates-year-month": + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+2025-09-11 2025-09-12\n") + elif scenario == "adr-identifier": + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+CE_ADR_0016_E\n") + elif scenario == "surface-unpins": + emit("diff --git a/src/example.rs b/src/example.rs\n--- a/src/example.rs\n+++ b/src/example.rs\n@@ -0,0 +1 @@\n+safe text\n" + "diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json\n" + "--- a/docs/tally/compatibility/compatibility-surface.json\n+++ b/docs/tally/compatibility/compatibility-surface.json\n@@ -0,0 +1 @@\n+safe manifest\n") + else: + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+safe text\ndiff --git a/docs/second.md b/docs/second.md\n--- a/docs/second.md\n+++ b/docs/second.md\n@@ -0,0 +1 @@\n+other text\n") +elif args and args[0] == "api": + joined = " ".join(args) + if "graphql" in args: + if scenario == "threads-first-empty-cursor-rejected" and "cursor=" in args: + fail("the first review-thread request must omit cursor") + def thread_nodes(start, count, unresolved=False): + return [{"id": f"thread-{index}", "isResolved": not (unresolved and index == start)} + for index in range(start, start + count)] + has_cursor = "C1" in joined + if scenario == "threads-empty-more": + nodes, page_info = [], {"hasNextPage": True, "endCursor": "C1"} + elif scenario == "threads-short": + nodes, page_info = thread_nodes(1, 1), {"hasNextPage": False, "endCursor": None} + elif scenario == "threads-malformed-pagination": + nodes, page_info = thread_nodes(1, 100), {"hasNextPage": True, "endCursor": None} + elif has_cursor: + if scenario == "threads-duplicate-id": + nodes = [{"id": "thread-1", "isResolved": True}] + else: + nodes = thread_nodes(101, 1, scenario == "threads-unresolved-second") + page_info = {"hasNextPage": False, "endCursor": None} + else: + nodes, page_info = thread_nodes(1, 100), {"hasNextPage": True, "endCursor": "C1"} + emit({"data": {"repository": {"pullRequest": {"reviewThreads": { + "totalCount": 101.5 if scenario == "threads-fractional" else (102 if scenario == "threads-total-drift" and has_cursor else 101), + "pageInfo": page_info, "nodes": nodes + }}}}}) + elif "/compare/" in joined: + if scenario == "lineage-mismatch": + emit({"status": "diverged", "behind_by": 1, + "merge_base_commit": {"sha": new_head}}) + else: + emit({"status": "ahead", "behind_by": 0, + "merge_base_commit": {"sha": base}}) + elif "/commits/" in joined and "/check-runs" in joined: + check_call = next_counter("GATE_CHECK_COUNTER") + if scenario == "check-run-wrong-head": + run_head = new_head + else: + run_head = head + total_count = 3 if scenario == "check-run-count-mismatch" else 2 + second_id = 1 if scenario == "check-run-duplicate-id" else 2 + run_status = "queued" if scenario == "check-run-incomplete" else "completed" + conclusion = "failure" if scenario in {"check-run-failed", "late-check-run-failure"} and (scenario != "late-check-run-failure" or check_call > 0) else ("skipped" if scenario in {"check-run-required-skip", "check-run-optional-skip"} else "success") + run_name = "Optional changed after rollup" if scenario in {"check-run-failed", "check-run-optional-skip"} else "Required checks" + emit([{"total_count": total_count, "check_runs": [ + {"id": 1, "name": run_name, "head_sha": run_head, "status": run_status, "conclusion": conclusion}, + {"id": second_id, "name": "Rust format", "head_sha": run_head, "status": "completed", "conclusion": "success"}]}]) + elif "/commits/" in joined and "/status?" in joined: + status_call = next_counter("GATE_STATUS_COUNTER") + def status_page(rows, total_count, state="success", page_head=head): + return {"sha": page_head, "state": state, "total_count": total_count, "statuses": rows} + if scenario == "status-two-page": + emit([status_page([{"id": 1, "context": "legacy one", "state": "success"}], 2), + status_page([{"id": 2, "context": "legacy two", "state": "success"}], 2)]) + elif scenario == "status-truncated": + emit([{"sha": head, "state": "success", "total_count": 1, "statuses": []}]) + elif scenario == "status-duplicate": + row = {"id": 1, "context": "legacy one", "state": "success"} + emit([{"sha": head, "state": "success", "total_count": 2, "statuses": [row]}, + {"sha": head, "state": "success", "total_count": 2, "statuses": [row]}]) + elif scenario == "status-mixed-head": + emit([{"sha": head, "state": "success", "total_count": 2, "statuses": [{"id": 1, "context": "legacy one", "state": "success"}]}, + {"sha": new_head, "state": "success", "total_count": 2, "statuses": [{"id": 2, "context": "legacy two", "state": "success"}]}]) + elif scenario == "status-failed-context" or (scenario == "late-status-failure" and status_call > 0): + emit([{"sha": head, "state": "failure", "total_count": 1, "statuses": [{"id": 1, "context": "legacy optional", "state": "failure"}]}]) + elif scenario == "status-nonempty-pending": + emit([{"sha": head, "state": "pending", "total_count": 1, "statuses": [{"id": 1, "context": "queued", "state": "pending"}]}]) + elif scenario == "status-empty-pending": + emit([{"sha": head, "state": "pending", "total_count": 0, "statuses": []}]) + elif scenario == "status-malformed": + emit([{"sha": head, "state": "success", "total_count": "0", "statuses": []}]) + elif scenario == "status-failed-combined": + emit([{"sha": head, "state": "failure", "total_count": 1, "statuses": [{"id": 1, "context": "legacy failed", "state": "failure"}]}]) + else: + emit([{"sha": head, "state": "success", "total_count": 0, "statuses": []}]) + elif "/pulls/321/commits" in joined: + message = "safe commit metadata" + if scenario == "metadata-commit-id": + message = "Customer " + "ABCDE" + "1234" + "F" + identity = {"name": "Maintainer", "email": "maintainer@example.invalid"} + if scenario == "metadata-author-id": + identity = {"name": "ABCDE" + "1234" + "F", "email": "maintainer@example.invalid"} + linked_author = None if scenario == "metadata-unlinked-identities" else {"login": "author"} + linked_committer = None if scenario == "metadata-unlinked-identities" else {"login": "committer"} + commits = [{"sha": head, "commit": {"message": message, "author": identity, "committer": identity}, + "author": linked_author, "committer": linked_committer}] + if scenario == "metadata-duplicate": + commits.append({"sha": head, "commit": {"message": message}}) + emit([commits]) + elif any(arg.endswith("/pulls/321") for arg in args): + commits = 1 + metadata_head = head + if scenario == "metadata-capped": + commits = 251 + elif scenario in {"metadata-truncated", "metadata-duplicate"}: + commits = 2 + elif scenario == "metadata-head-mismatch": + metadata_head = new_head + emit({"commits": commits, "head": {"sha": metadata_head}, "user": {"login": "author"}}) + elif "branches/master/protection/required_status_checks" in joined: + contexts = [ + "Frontend build", "Rust format", "GitGuardian Security Checks", + "Dependency security", "Required checks"] + if scenario == "protection-missing-gitguardian": + contexts = [context for context in contexts if context != "GitGuardian Security Checks"] + if scenario == "required-context-output-bound": + contexts += ["control-" + str(i) + "-" + "z" * 10000 for i in range(50)] + if scenario == "required-context-unexpected": + contexts += ["Unexpected required context"] + emit({"strict": scenario != "protection-nonstrict", "contexts": contexts, "checks": []}) + elif "branches/master" in joined: + base_tip_call = next_counter("GATE_BASE_TIP_COUNTER") + if scenario == "final-base-tip-failure" and base_tip_call >= 2: + fail("controlled final base-tip read failure") + emit(new_head if scenario == "final-base-tip-moves" and base_tip_call >= 2 else base) + elif "/pulls/321/reviews" in joined: + if scenario in {"short-review", "summary-only", "manual-summary"}: + emit([[]]) + else: + records = [{"user": {"login": "chatgpt-codex-connector[bot]", "type": "Bot"}, "state": "COMMENTED", "commit_id": head}] + if security_case and scenario not in {"security-review-missing", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-documents-screen", "security-documents-screen-rename-out", "security-tauri-cargo", "security-tauri-cargo-rename-out", "security-tauri-lib", "security-tauri-lib-rename-out", "security-commands-facade", "security-bank-statement-import", "security-prune-package-compiler-cache", "security-prune-package-compiler-cache-rename-out", "security-ci-workflow", "security-ci-workflow-rename-out", "security-release-preview", "security-release-preview-rename-out", "security-deploy-install-page", "security-deploy-install-page-rename-out"}: + record = {"user": {"login": "reviewer", "type": "User"}, "author_association": "COLLABORATOR", "state": "COMMENTED", "commit_id": head, "body": f"Security review: {head}\nResult: accepted\nReviewed credential handling: token diagnostics remain redacted.\nSecurity rationale: the current access boundary prevents a cache token from reaching logs."} + if scenario == "security-review-stale": record["commit_id"] = new_head + if scenario == "security-review-author": record["user"]["login"] = "author" + if scenario == "security-review-unrelated": record["body"] = "Looks good" + if scenario == "security-review-no-scope": record["body"] = f"Security review: {head}\nResult: accepted" + if scenario == "security-review-bare-scope": record["body"] = f"Security review: {head}\nResult: accepted\nReviewed credential:\nSecurity rationale: the current access boundary prevents a cache token from reaching logs." + if scenario == "security-review-placeholder-rationale": record["body"] = f"Security review: {head}\nResult: accepted\nReviewed credential handling: token diagnostics remain redacted.\nSecurity rationale: TBD" + if scenario == "security-review-hidden": record["body"] = f"" + if scenario == "security-review-hidden-unterminated": record["body"] = f"|$))"; ""); + def visible_body: .body | gsub("(?s:|$))"; "") | gsub("(?ms)^[ ]{0,3}(```|~~~)[^\\n]*\\n.*?(^[ ]{0,3}\\1[ \\t]*$|\\z)"; ""); def substantive: gsub("^[[:space:]]+|[[:space:]]+$"; "") as $text | ($text | length >= 12) and diff --git a/scripts/merge-gate.test.py b/scripts/merge-gate.test.py index 628c56199..cc526e536 100644 --- a/scripts/merge-gate.test.py +++ b/scripts/merge-gate.test.py @@ -480,6 +480,53 @@ def test_privacy_module_blocks_trusted_pem_and_four_digit_landline_without_echoi self.assertIn(expected, "\n".join(result["blockers"])) self.assertNotIn(value, json.dumps(result)) + def test_privacy_module_classifies_literal_credentials_and_customer_emails_without_echoing_values(self): + privacy = load_privacy_module() + value_prefix = "live" + literals = ( + f"Authorization: Bearer {value_prefix}-token-123", + f"api_key = '{value_prefix}-api-key-123'", + f"access_token: {value_prefix}-access-token-123", + f"refresh_token={value_prefix}-refresh-token-123", + f"client_secret: {value_prefix}-client-secret-123", + f"credential = {value_prefix}-credential-123", + f"session-token: {value_prefix}-session-token-123", + ) + for value in literals: + with self.subTest(value=value): + result = privacy.scan(value, HEAD) + self.assertTrue(result["blockers"]) + self.assertNotIn(value, json.dumps(result)) + for value in ("Authorization: Bearer", "api_key =", "client_secret: '"): + with self.subTest(value=value): + result = privacy.scan(value, HEAD) + self.assertTrue(result["indeterminate"]) + self.assertNotIn(value, json.dumps(result)) + for value in ("api_key = ${API_KEY}", "access_token: ", "client_secret: str", "tokenizer reference"): + with self.subTest(value=value): + result = privacy.scan(value, HEAD) + self.assertFalse(result["blockers"] + result["indeterminate"]) + + def test_privacy_email_lane_scans_content_sources_but_not_validated_commit_identity(self): + privacy = load_privacy_module() + for scenario in ("privacy-email-payload", "privacy-email-destination", "privacy-email-title", "privacy-email-body", "privacy-email-commit"): + with self.subTest(scenario=scenario): + result = self.run_gate(scenario) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("customer email shape", result.stdout) + self.assertNotIn("customer@company.test", result.stdout) + result = self.run_gate("privacy-email-author") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + for value in ("customer@company.test", "customer@example.com.attacker.test"): + with self.subTest(value=value): + result = privacy.scan(value, HEAD) + self.assertTrue(result["blockers"]) + self.assertNotIn(value, json.dumps(result)) + for value in ("customer@example.com", "customer@example.invalid"): + with self.subTest(value=value): + result = privacy.scan(value, HEAD) + self.assertFalse(result["blockers"] + result["indeterminate"]) + def test_duplicate_thread_ids_are_indeterminate(self): self.assert_indeterminate("threads-duplicate-id", "pagination repeated thread IDs") @@ -658,7 +705,7 @@ def test_required_context_diagnostics_are_bounded_for_failures(self): self.assertNotIn("z" * 100, result.stdout) def test_security_review_is_focused_independent_and_current(self): - for scenario in ("security-review-stale", "security-review-author", "security-review-unrelated", "security-review-no-scope", "security-review-bare-scope", "security-review-placeholder-rationale", "security-review-hidden", "security-review-hidden-unterminated", "security-review-outsider", "security-review-dismissed", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-documents-screen", "security-documents-screen-rename-out", "security-tauri-cargo", "security-tauri-cargo-rename-out", "security-tauri-cargo-lock", "security-tauri-cargo-lock-rename-out", "security-tauri-lib", "security-tauri-lib-rename-out", "security-commands-facade", "security-bank-statement-import", "security-prune-package-compiler-cache", "security-prune-package-compiler-cache-rename-out", "security-ci-workflow", "security-ci-workflow-rename-out", "security-release-preview", "security-release-preview-rename-out", "security-deploy-install-page", "security-deploy-install-page-rename-out"): + for scenario in ("security-review-stale", "security-review-author", "security-review-unrelated", "security-review-no-scope", "security-review-bare-scope", "security-review-placeholder-rationale", "security-review-hidden", "security-review-hidden-unterminated", "security-review-fenced", "security-review-fenced-unterminated", "security-review-outsider", "security-review-dismissed", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-documents-screen", "security-documents-screen-rename-out", "security-tauri-cargo", "security-tauri-cargo-rename-out", "security-tauri-cargo-lock", "security-tauri-cargo-lock-rename-out", "security-tauri-lib", "security-tauri-lib-rename-out", "security-commands-facade", "security-bank-statement-import", "security-prune-package-compiler-cache", "security-prune-package-compiler-cache-rename-out", "security-ci-workflow", "security-ci-workflow-rename-out", "security-release-preview", "security-release-preview-rename-out", "security-deploy-install-page", "security-deploy-install-page-rename-out", "security-review-path-privacy-module", "security-review-path-privacy-module-rename-out", "security-review-path-privacy-coordinator", "security-review-path-privacy-coordinator-rename-out", "security-review-path-diff-parser", "security-review-path-diff-parser-rename-out"): with self.subTest(scenario=scenario): self.assert_indeterminate(scenario, "security-focused reviewer comment") result = self.run_gate("security-review-valid") diff --git a/scripts/merge_gate_fake_gh.py b/scripts/merge_gate_fake_gh.py index ae760cd00..8c848b161 100755 --- a/scripts/merge_gate_fake_gh.py +++ b/scripts/merge_gate_fake_gh.py @@ -5,6 +5,7 @@ args = sys.argv[1:] scenario = os.environ.get("GATE_SCENARIO", "pass") security_case = scenario.startswith("security-review-") or scenario in {"security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-documents-screen", "security-documents-screen-rename-out", "security-documents-screen-valid", "security-tauri-cargo", "security-tauri-cargo-rename-out", "security-tauri-cargo-lock", "security-tauri-cargo-lock-rename-out", "security-tauri-lib", "security-tauri-lib-rename-out", "security-commands-facade", "security-bank-statement-import", "security-prune-package-compiler-cache", "security-prune-package-compiler-cache-rename-out", "security-ci-workflow", "security-ci-workflow-rename-out", "security-ci-workflow-valid", "security-release-preview", "security-release-preview-rename-out", "security-deploy-install-page", "security-deploy-install-page-rename-out", "security-deploy-install-page-valid", "workflow-notes-present", "workflow-delete-notes", "workflow-rename-out-notes"} +security_case = security_case or scenario.startswith("security-review-path-") security_workflow_case = scenario in {"security-ci-workflow", "security-ci-workflow-rename-out", "security-ci-workflow-valid", "security-release-preview", "security-release-preview-rename-out", "security-deploy-install-page", "security-deploy-install-page-rename-out", "security-deploy-install-page-valid"} sync_case = scenario.startswith("sync-") head = "0123456789abcdef0123456789abcdef01234567" @@ -46,6 +47,8 @@ def next_counter(name): title = "Safe merge gate control" if scenario == "metadata-title-id": title = "Customer " + "ABCDE" + "1234" + "F" + elif scenario == "privacy-email-title": + title = "Customer customer@company.test" body = ( "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" @@ -288,11 +291,13 @@ def next_counter(name): body += "\n- Windows validation evidence: N/A.\n- macOS validation evidence: TBD.\n" if scenario == "platform-evidence-fenced-continuation": body += "\n### Windows validation evidence\n~~~text\nWindows CI passed `python3 scripts/merge-gate.test.py`.\n~~~\n### macOS validation evidence\n~~~text\nmacOS CI passed `python3 scripts/merge-gate.test.py`.\n~~~\n" + if scenario == "privacy-email-body": + body += "\nCustomer contact: customer@company.test\n" body = body.replace("blob/HEAD", f"blob/{head}") if scenario == "checklist-stale-ref": body = body.replace(f"blob/{head}", "blob/" + "f" * 40) one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "grouped-identifier-mixed", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "landline-grouped", "landline-standard-hyphen", "landline-standard-space", "landline-standard-underscore", "pem-certificate-envelope", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "grouped-pan-space", "grouped-pan-hyphen", "grouped-masked-pan-space", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "workflow-punctuated-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "dependency-manifest-missing", "dependency-manifest-present", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "gitlink-existing", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-negative-outcome", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager", "platform-windows-native-action", "platform-windows-native-action-rename-out", "platform-ci-workflow", "platform-ci-workflow-rename-out", "platform-release-mcpb-preview", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case - one_file = one_file or scenario in {"skipped-native-scope", "skipped-bundle-scope", "non-sensitive-cargo-lock"} + one_file = one_file or scenario in {"skipped-native-scope", "skipped-bundle-scope", "non-sensitive-cargo-lock", "privacy-email-payload", "privacy-email-destination", "privacy-email-title", "privacy-email-body", "privacy-email-commit", "privacy-email-author"} selected_base = new_head if scenario == "base-oid-mismatch" else base emit({"headRefOid": selected_head, "baseRefOid": selected_base, "baseRefName": "master", "mergeable": "MERGEABLE", "mergeStateStatus": final_state, @@ -341,10 +346,18 @@ def next_counter(name): emit("diff --git a/src-tauri/Cargo.toml b/docs/retired-tauri-cargo.toml\nsimilarity index 100%\nrename from src-tauri/Cargo.toml\nrename to docs/retired-tauri-cargo.toml\n") elif scenario == "security-tauri-cargo-lock-rename-out": emit("diff --git a/src-tauri/Cargo.lock b/docs/retired-tauri-cargo.lock\nsimilarity index 100%\nrename from src-tauri/Cargo.lock\nrename to docs/retired-tauri-cargo.lock\n") + elif scenario.startswith("security-review-path-") and scenario.endswith("-rename-out"): + paths = { + "security-review-path-privacy-module-rename-out": ("scripts/merge_gate_privacy.py", "docs/retired-privacy.py"), + "security-review-path-privacy-coordinator-rename-out": ("scripts/merge-gate.sh", "docs/retired-merge-gate.sh"), + "security-review-path-diff-parser-rename-out": ("scripts/merge_gate_diff.py", "docs/retired-diff.py"), + } + old_path, new_path = paths[scenario] + emit(f"diff --git a/{old_path} b/{new_path}\nsimilarity index 100%\nrename from {old_path}\nrename to {new_path}\n") elif scenario == "security-tauri-lib-rename-out": emit("diff --git a/src-tauri/src/lib.rs b/docs/retired-tauri-lib.rs\nsimilarity index 100%\nrename from src-tauri/src/lib.rs\nrename to docs/retired-tauri-lib.rs\n") elif security_case: - paths = {"security-axal-frontend": "src/AxalScreen.tsx", "security-axal-native": "src-tauri/src/axal.rs", "security-encrypted-keystore": "src-tauri/src/db/encrypted.rs", "security-documents-consumer": "src-tauri/src/documents.rs", "security-documents-screen": "src/DocumentsScreen.tsx", "security-documents-screen-valid": "src/DocumentsScreen.tsx", "security-tauri-cargo": "src-tauri/Cargo.toml", "security-tauri-cargo-lock": "src-tauri/Cargo.lock", "security-tauri-lib": "src-tauri/src/lib.rs", "security-commands-facade": "src-tauri/src/commands.rs", "security-bank-statement-import": "scripts/bank_statement_import.py", "security-prune-package-compiler-cache": "scripts/prune-package-compiler-cache.mjs", "security-ci-workflow": ".github/workflows/ci.yml", "security-ci-workflow-valid": ".github/workflows/ci.yml", "security-release-preview": ".github/workflows/release-mcpb-preview.yml", "security-deploy-install-page": ".github/workflows/deploy-install-page.yml", "security-deploy-install-page-valid": ".github/workflows/deploy-install-page.yml"} + paths = {"security-axal-frontend": "src/AxalScreen.tsx", "security-axal-native": "src-tauri/src/axal.rs", "security-encrypted-keystore": "src-tauri/src/db/encrypted.rs", "security-documents-consumer": "src-tauri/src/documents.rs", "security-documents-screen": "src/DocumentsScreen.tsx", "security-documents-screen-valid": "src/DocumentsScreen.tsx", "security-tauri-cargo": "src-tauri/Cargo.toml", "security-tauri-cargo-lock": "src-tauri/Cargo.lock", "security-tauri-lib": "src-tauri/src/lib.rs", "security-commands-facade": "src-tauri/src/commands.rs", "security-bank-statement-import": "scripts/bank_statement_import.py", "security-prune-package-compiler-cache": "scripts/prune-package-compiler-cache.mjs", "security-ci-workflow": ".github/workflows/ci.yml", "security-ci-workflow-valid": ".github/workflows/ci.yml", "security-release-preview": ".github/workflows/release-mcpb-preview.yml", "security-deploy-install-page": ".github/workflows/deploy-install-page.yml", "security-deploy-install-page-valid": ".github/workflows/deploy-install-page.yml", "security-review-path-privacy-module": "scripts/merge_gate_privacy.py", "security-review-path-privacy-coordinator": "scripts/merge-gate.sh", "security-review-path-diff-parser": "scripts/merge_gate_diff.py"} path = paths.get(scenario, "src-tauri/src/dsc.rs") emit(f"diff --git a/{path} b/{path}\n--- a/{path}\n+++ b/{path}\n@@ -0,0 +1 @@\n+safe check\n") elif sync_case: @@ -361,6 +374,12 @@ def next_counter(name): emit("diff --git a/index.html b/index.html\n--- a/index.html\n+++ b/index.html\n@@ -0,0 +1 @@\n+safe text\n") elif scenario == "non-sensitive-cargo-lock": emit("diff --git a/tools/Cargo.lock b/tools/Cargo.lock\n--- a/tools/Cargo.lock\n+++ b/tools/Cargo.lock\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario == "privacy-email-payload": + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+customer@company.test\n") + elif scenario == "privacy-email-destination": + emit("diff --git a/docs/customer@company.test.md b/docs/customer@company.test.md\n--- /dev/null\n+++ b/docs/customer@company.test.md\n@@ -0,0 +1 @@\n+safe text\n") + elif scenario in {"privacy-email-title", "privacy-email-body", "privacy-email-commit", "privacy-email-author"}: + emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+safe text\n") elif scenario in {"security-notes-missing", "security-notes-present", "security-none", "security-pending"}: emit("diff --git a/src-tauri/src/tally/runtime.rs b/src-tauri/src/tally/runtime.rs\n--- a/src-tauri/src/tally/runtime.rs\n+++ b/src-tauri/src/tally/runtime.rs\n@@ -0,0 +1 @@\n+safe text\n") elif scenario in {"security-crate", "security-agent-import", "security-dsc"}: @@ -586,9 +605,13 @@ def status_page(rows, total_count, state="success", page_head=head): message = "safe commit metadata" if scenario == "metadata-commit-id": message = "Customer " + "ABCDE" + "1234" + "F" + elif scenario == "privacy-email-commit": + message = "Customer customer@company.test" identity = {"name": "Maintainer", "email": "maintainer@example.invalid"} if scenario == "metadata-author-id": identity = {"name": "ABCDE" + "1234" + "F", "email": "maintainer@example.invalid"} + elif scenario == "privacy-email-author": + identity = {"name": "Maintainer", "email": "maintainer@company.test"} linked_author = None if scenario == "metadata-unlinked-identities" else {"login": "author"} linked_committer = None if scenario == "metadata-unlinked-identities" else {"login": "committer"} commits = [{"sha": head, "commit": {"message": message, "author": identity, "committer": identity}, @@ -627,7 +650,7 @@ def status_page(rows, total_count, state="success", page_head=head): emit([[]]) else: records = [{"user": {"login": "chatgpt-codex-connector[bot]", "type": "Bot"}, "state": "COMMENTED", "commit_id": head}] - if security_case and scenario not in {"security-review-missing", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-documents-screen", "security-documents-screen-rename-out", "security-tauri-cargo", "security-tauri-cargo-rename-out", "security-tauri-cargo-lock", "security-tauri-cargo-lock-rename-out", "security-tauri-lib", "security-tauri-lib-rename-out", "security-commands-facade", "security-bank-statement-import", "security-prune-package-compiler-cache", "security-prune-package-compiler-cache-rename-out", "security-ci-workflow", "security-ci-workflow-rename-out", "security-release-preview", "security-release-preview-rename-out", "security-deploy-install-page", "security-deploy-install-page-rename-out"}: + if security_case and not scenario.startswith("security-review-path-") and scenario not in {"security-review-missing", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-documents-screen", "security-documents-screen-rename-out", "security-tauri-cargo", "security-tauri-cargo-rename-out", "security-tauri-cargo-lock", "security-tauri-cargo-lock-rename-out", "security-tauri-lib", "security-tauri-lib-rename-out", "security-commands-facade", "security-bank-statement-import", "security-prune-package-compiler-cache", "security-prune-package-compiler-cache-rename-out", "security-ci-workflow", "security-ci-workflow-rename-out", "security-release-preview", "security-release-preview-rename-out", "security-deploy-install-page", "security-deploy-install-page-rename-out"}: record = {"user": {"login": "reviewer", "type": "User"}, "author_association": "COLLABORATOR", "state": "COMMENTED", "commit_id": head, "body": f"Security review: {head}\nResult: accepted\nReviewed credential handling: token diagnostics remain redacted.\nSecurity rationale: the current access boundary prevents a cache token from reaching logs."} if scenario == "security-review-stale": record["commit_id"] = new_head if scenario == "security-review-author": record["user"]["login"] = "author" @@ -637,6 +660,9 @@ def status_page(rows, total_count, state="success", page_head=head): if scenario == "security-review-placeholder-rationale": record["body"] = f"Security review: {head}\nResult: accepted\nReviewed credential handling: token diagnostics remain redacted.\nSecurity rationale: TBD" if scenario == "security-review-hidden": record["body"] = f"" if scenario == "security-review-hidden-unterminated": record["body"] = f""), + "late-meta-body-newline": ("body", body + "\n"), + "late-meta-draft": ("isDraft", True), + "late-meta-state": ("state", "CLOSED"), + "late-meta-mergeable": ("mergeable", "CONFLICTING"), + "late-meta-merge-state": ("mergeStateStatus", "HAS_HOOKS"), + "late-meta-unknown-state": ("mergeStateStatus", "UNKNOWN_VALUE"), + "late-meta-changed-files": ("changedFiles", 3), + "late-meta-body-wrong-type": ("body", False), + } + if scenario == "late-meta-error": + fail("controlled final PR metadata read failure") + if scenario == "late-meta-missing-body": + del metadata["body"] + if scenario in late_changes: + field, value = late_changes[scenario] + metadata[field] = value + requested_fields = args[args.index("--json") + 1].split(",") + emit({field: metadata[field] for field in requested_fields if field in metadata}) elif args[:2] == ["pr", "checks"]: if scenario == "checks-silent": raise SystemExit(0) @@ -565,19 +591,44 @@ def thread_nodes(start, count, unresolved=False): run_head = new_head else: run_head = head - total_count = 3 if scenario == "check-run-count-mismatch" else 2 second_id = 1 if scenario == "check-run-duplicate-id" else 2 run_status = "queued" if scenario == "check-run-incomplete" else "completed" conclusion = "failure" if scenario in {"check-run-failed", "late-check-run-failure"} and (scenario != "late-check-run-failure" or check_call > 0) else ("skipped" if scenario in {"check-run-required-skip", "check-run-optional-skip", "check-run-allowlisted-skip"} or (scenario == "late-check-run-unallowlisted-skip" and check_call > 0) else "success") run_name = "Unreviewed final conditional job" if scenario == "late-check-run-unallowlisted-skip" and check_call > 0 else ("Native checks (windows-latest)" if scenario == "check-run-allowlisted-skip" else ("Optional changed after rollup" if scenario in {"check-run-failed", "check-run-optional-skip"} else "Required checks")) - emit([{"total_count": total_count, "check_runs": [ + rows = [ {"id": 1, "name": run_name, "head_sha": run_head, "status": run_status, "conclusion": conclusion}, - {"id": second_id, "name": "Rust format", "head_sha": run_head, "status": "completed", "conclusion": "success"}]}]) + {"id": second_id, "name": "Rust format", "head_sha": run_head, "status": "completed", "conclusion": "success"}] + for name in ("Required checks", "Frontend build", "GitGuardian Security Checks", "Dependency security"): + if not any(row["name"] == name for row in rows): + rows.append({"id": len(rows) + 1, "name": name, "head_sha": run_head, + "status": "completed", "conclusion": "success"}) + if scenario in {"final-required-statuses-pass", "final-status-context-disappears", "final-required-status-fails"}: + rows = [row for row in rows if row["name"] not in {"GitGuardian Security Checks", "Dependency security"}] + if check_call > 0: + if scenario == "final-contexts-empty": + rows = [] + elif scenario == "final-check-context-disappears": + rows = [row for row in rows if row["name"] != "Rust format"] + elif scenario in {"final-required-check-skipped", "final-required-check-neutral", "final-required-check-pending"}: + rows[0]["conclusion"] = scenario.rsplit("-", 1)[1] + if scenario == "final-required-check-pending": + rows[0].update(status="queued", conclusion=None) + total_count = len(rows) + (1 if scenario == "check-run-count-mismatch" else 0) + emit([{"total_count": total_count, "check_runs": rows}]) elif "/commits/" in joined and "/status?" in joined: status_call = next_counter("GATE_STATUS_COUNTER") def status_page(rows, total_count, state="success", page_head=head): return {"sha": page_head, "state": state, "total_count": total_count, "statuses": rows} - if scenario == "status-two-page": + if scenario in {"final-required-statuses-pass", "final-status-context-disappears", "final-required-status-fails"}: + rows = [{"id": 1, "context": "GitGuardian Security Checks", "state": "success"}, + {"id": 2, "context": "Dependency security", "state": "success"}] + state = "success" + if status_call > 0 and scenario == "final-status-context-disappears": + rows = rows[1:] + elif status_call > 0 and scenario == "final-required-status-fails": + rows[0]["state"] = state = "failure" + emit([status_page([row], len(rows), state) for row in rows]) + elif scenario == "status-two-page": emit([status_page([{"id": 1, "context": "legacy one", "state": "success"}], 2), status_page([{"id": 2, "context": "legacy two", "state": "success"}], 2)]) elif scenario == "status-truncated": diff --git a/scripts/merge_gate_privacy.py b/scripts/merge_gate_privacy.py index 6b46f17c7..a9920787a 100644 --- a/scripts/merge_gate_privacy.py +++ b/scripts/merge_gate_privacy.py @@ -37,10 +37,10 @@ ) EMAIL_RE = re.compile(r"(?(?['\"])?" + CREDENTIAL_KEY_RE + - r"(?(key_quote)(?P=key_quote))(?![A-Za-z0-9_-])\s*(?:=|:)\s*)(?P.*)$" + r"(?(key_quote)(?P=key_quote))(?![A-Za-z0-9_-])\s*(?:=|:)\s*)" ) AUTHORIZATION_BEARER_RE = re.compile( r"(?i)(?P\bauthorization\s*:\s*bearer(?:\s+)?)(?P.*)$" @@ -174,25 +174,53 @@ def credential_value_status(value): return "blocker" +def credential_value_span(text, start): + """Return one assignment value's bounds without consuming later fields.""" + if start >= len(text): + return start, start + if text[start] in ("'", '"'): + quote = text[start] + cursor = start + 1 + while cursor < len(text): + if text[cursor] == "\\": + cursor += 2 + elif text[cursor] == quote: + return start, cursor + 1 + else: + cursor += 1 + return start, len(text) + cursor = start + while cursor < len(text) and text[cursor] not in "\t\r\n ,;#": + cursor += 1 + return start, cursor + + def tokenize_credential_literals(text, record): """Block assigned secret material before UUID/digest masking can hide it.""" blocked = 0 malformed = 0 retained = [] - for line in text.splitlines(True): - match = AUTHORIZATION_BEARER_RE.search(line) or CREDENTIAL_ASSIGNMENT_RE.search(line) + cursor = 0 + while cursor < len(text): + bearer = AUTHORIZATION_BEARER_RE.search(text, cursor) + assignment = CREDENTIAL_ASSIGNMENT_RE.search(text, cursor) + match = min((candidate for candidate in (bearer, assignment) if candidate), key=lambda candidate: candidate.start(), default=None) if not match: - retained.append(line) - continue - status = credential_value_status(match.group("value")) + retained.append(text[cursor:]) + break + value_start = match.end("prefix") + value_start, value_end = credential_value_span(text, value_start) + status = credential_value_status(text[value_start:value_end]) + retained.append(text[cursor:value_start]) if status == "placeholder": - retained.append(line) + retained.append(text[value_start:value_end]) else: - retained.append(line[:match.start("value")] + "" + ("\n" if line.endswith("\n") else "")) + retained.append("") if status == "blocker": blocked += 1 else: malformed += 1 + cursor = value_end if blocked: add(record, "blockers", "privacy scan found %d literal credential, bearer, or API token value(s)" % blocked) if malformed: From a1fdf17dcced1d9569d671839aaba59acfccd3ae Mon Sep 17 00:00:00 2001 From: t Date: Tue, 15 Sep 2026 14:35:11 +0530 Subject: [PATCH 26/27] merge-gate: cut to only what runs as a real required CI check The merge-gate tooling was 3977 lines that gated nothing automatically (a manual CLI; master's ci.yml never invoked it). Review produced 20 unresolved threads and 27 tracking issues. Per the owner's decision, keep only what can run as a real required CI check and delete the rest. Kept: - Compatibility-surface validation (read_surface_paths() and callers). - Privacy/PII scan (merge_gate_privacy.py, merge_gate_diff.py in full, plus their input pipeline: commit author/committer metadata validation and the diff-coverage scan in merge-gate.sh). - A new, minimal (38-line) check that a review exists naming the current head SHA, closing #317: PRs merged 2-8 minutes after opening, where "zero unresolved threads" meant "review had not started". Treats "no review evidence for this head" as FAIL. Cut entirely: - PR/head/base identity binding, branch-protection context derivation, and the checks rollup. - PR body/review-checklist/P4/platform-evidence validation. - Skipped-CI-job allowlist and security-reviewer-comment validation. - scripts/merge_gate_fake_gh.py (a GitHub API test double that existed only to test the cut concerns). merge-gate.sh: 1587 -> 552 lines. merge-gate.test.py shrunk to cover only the kept surface, backed by a new, much smaller fake `gh` (merge_gate_test_gh.py) replacing the deleted one. Also fixes 7 verified live defects in the kept code (#346, #358, #360, #365, #366, #373, #377), each with a regression test in merge-gate.test.py verified to fail against the pre-fix code and pass after. docs/proposed-merge-gate-ci.md proposes the CI wiring to actually make this a required check (a new workflow file, not touching .github/ per this change's scope) and explains why the review-evidence check needs pull_request_review/issue_comment triggers, not just push/synchronize. Co-Authored-By: Claude Sonnet 5 --- docs/proposed-merge-gate-ci.md | 157 ++++ scripts/merge-gate.sh | 1221 +++----------------------------- scripts/merge-gate.test.py | 1000 ++++---------------------- scripts/merge_gate_diff.py | 14 +- scripts/merge_gate_fake_gh.py | 930 ------------------------ scripts/merge_gate_privacy.py | 44 +- scripts/merge_gate_test_gh.py | 316 +++++++++ 7 files changed, 760 insertions(+), 2922 deletions(-) create mode 100644 docs/proposed-merge-gate-ci.md delete mode 100755 scripts/merge_gate_fake_gh.py create mode 100755 scripts/merge_gate_test_gh.py diff --git a/docs/proposed-merge-gate-ci.md b/docs/proposed-merge-gate-ci.md new file mode 100644 index 000000000..405a12457 --- /dev/null +++ b/docs/proposed-merge-gate-ci.md @@ -0,0 +1,157 @@ +# Proposed CI wiring for the shrunk merge gate + +`scripts/merge-gate.sh` was cut from a 3977-line manual CLI tool that gated +nothing automatically down to three checks that can run as real, required CI +status contexts: compatibility-surface validation, the privacy/PII scan, and +a minimal "a review exists naming the current head SHA" check. + +This document is a **proposal only**. Per the task boundary, nothing under +`.github/` was modified. A maintainer who agrees with this design should add +a new workflow file at `.github/workflows/merge-gate.yml` with the content +below (or fold it into `ci.yml`; see "Why a separate workflow file" below), +then add the job's context to branch protection's required status checks for +`master`. + +## Why a separate workflow file, not a job inside `ci.yml` + +`ci.yml` triggers only on `push`/`pull_request` (`opened, reopened, +synchronize, ready_for_review`) plus `workflow_dispatch`. That is enough for +the compatibility-surface and privacy checks, which only need the current +diff and are naturally re-evaluated on every push. It is **not** enough for +the review-evidence check: per issue #317, the entire point is to catch a PR +merged before any review artifact exists. A required check that can only +fire on `push`/`synchronize` would stay red forever once a reviewer finally +does leave a review or a completion summary comment, because neither of +those events re-triggers `ci.yml` — there would be no way to turn the check +green without an empty commit. + +So the merge-gate job needs to also listen for `pull_request_review` and +`issue_comment` (the two ways review evidence can appear: a real review +object, or — per the "clean review, no review object, only a reaction plus a +summary comment" case the check is written to handle — a completion comment +on the PR). Adding those events to `ci.yml`'s single `on:` block would +re-trigger the entire native/bundle/frontend build matrix on every PR +comment, which is wasteful and unrelated to what this gate checks. A +dedicated workflow file scopes the extra trigger events to only the cheap +job that needs them. + +## Proposed workflow + +```yaml +name: Merge gate + +on: + pull_request: + branches: [master] + types: [opened, reopened, synchronize, ready_for_review] + pull_request_review: + types: [submitted] + issue_comment: + types: [created] + workflow_dispatch: + inputs: + pr_number: + description: PR number to re-check (defaults to the PR this run was triggered from) + required: false + binary_review_sha: + description: >- + Full 40-hex commit SHA a maintainer has manually reviewed for binary + byte/ownership/license/NOTICE content. Only needed when the PR adds + or changes a binary (or Git-LFS-pointer) file; leave blank otherwise. + required: false + independent_review_sha: + description: Full 40-hex commit SHA of the matching independent review attestation. + required: false + +concurrency: + group: merge-gate-${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }} + cancel-in-progress: true + +jobs: + merge-gate: + name: Merge gate + runs-on: ubuntu-latest + timeout-minutes: 10 + # issue_comment fires for both issues and PRs; only run for PR comments. + if: ${{ github.event_name != 'issue_comment' || github.event.issue.pull_request != null }} + permissions: + contents: read + pull-requests: read + issues: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + - name: Resolve PR number + id: pr + env: + EVENT_NAME: ${{ github.event_name }} + PR_NUMBER: ${{ github.event.pull_request.number }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + DISPATCH_PR_NUMBER: ${{ github.event.inputs.pr_number }} + run: | + set -euo pipefail + number="${PR_NUMBER:-${ISSUE_NUMBER:-${DISPATCH_PR_NUMBER:-}}}" + if [ -z "$number" ]; then + echo "could not resolve a PR number for this event" >&2 + exit 2 + fi + echo "number=$number" >> "$GITHUB_OUTPUT" + - name: Run merge gate + env: + GH_TOKEN: ${{ github.token }} + BINARY_REVIEW_SHA: ${{ github.event.inputs.binary_review_sha }} + INDEPENDENT_REVIEW_SHA: ${{ github.event.inputs.independent_review_sha }} + run: | + set -euo pipefail + args=(scripts/merge-gate.sh "${{ steps.pr.outputs.number }}" --repo "${{ github.repository }}") + [ -n "$BINARY_REVIEW_SHA" ] && args+=(--binary-review-sha "$BINARY_REVIEW_SHA") + [ -n "$INDEPENDENT_REVIEW_SHA" ] && args+=(--independent-review-sha "$INDEPENDENT_REVIEW_SHA") + "${args[@]}" +``` + +Notes on this draft: + +- Exit codes line up with what a required check needs: `merge-gate.sh` exits + `0` (pass), `1` (block), or `2` (indeterminate) — both `1` and `2` are + non-zero, so the job step fails and the required check goes red for either + outcome. There is no separate "neutral" state; INDETERMINATE is treated as + blocking, matching the script's own stance that unknown evidence is never + converted into an empty successful set. +- `pull-requests: read` and `issues: read` are enough: every `gh api`/`gh pr` + call the shrunk script makes is a read against the same repository, so the + default `GITHUB_TOKEN` (`github.token`) needs no elevated scope. +- The `--binary-review-sha`/`--independent-review-sha` attestations are + inherently a human judgment call (someone read the actual binary bytes, + ownership, license, and NOTICE obligations) and cannot be automated. The + `workflow_dispatch` inputs above are the proposed escape hatch: a + maintainer who has done that review re-runs this workflow by hand with + both SHAs filled in. Until that happens, any PR that adds or changes a + binary (including a Git LFS pointer, per the #360 fix) correctly stays + blocked on the automatic `pull_request`/`pull_request_review`/ + `issue_comment` triggers. +- Add the job's status context — **"Merge gate"** — to `master`'s branch + protection required status checks alongside the existing "Frontend build", + "Rust format", "GitGuardian Security Checks", "Dependency security", and + "Required checks" contexts. It intentionally is **not** folded into the + existing `required-checks` rollup job in `ci.yml`, both because it lives in + a different workflow (a rollup job can only depend on jobs in its own + workflow run) and because keeping it as its own context makes it possible + to see at a glance, on the PR, exactly which of the three concerns + (surface reseal, privacy scan, review evidence) failed — `merge-gate.sh` + prints a `BLOCK`/`INDETERMINATE`/`ok` line per check in its log output. +- `concurrency` is keyed by PR number (falling back across the three event + shapes) so a review submitted while a push-triggered run is still in + flight doesn't race it; the newer run's result is what branch protection + sees. + +## What this intentionally leaves for GitHub itself + +`scripts/merge-gate.sh` no longer re-derives PR/head/base identity binding, +branch-protection required-check contexts, or a checks rollup — that logic +(roughly 900 of the cut lines) duplicated what `master`'s branch protection +already enforces natively once "Merge gate" is added as a required context: +GitHub itself refuses to merge a PR that is behind, has an unresolved +required context, or whose head has moved since a context last reported +success. Required status checks, not this script, are the source of truth +for "is everything else green." diff --git a/scripts/merge-gate.sh b/scripts/merge-gate.sh index 167907b78..e6cbc99a8 100755 --- a/scripts/merge-gate.sh +++ b/scripts/merge-gate.sh @@ -1,18 +1,17 @@ #!/usr/bin/env bash -# Decide whether a pull request may be merged, and say why not when it may not. +# Validate a pull request's compatibility-surface reseal, run the privacy/PII +# scan, and confirm review evidence names the current head SHA. # # Usage: scripts/merge-gate.sh [--repo OWNER/NAME] -# [--independent-review-sha FULL_SHA] (explicit manual review attestation) +# [--independent-review-sha FULL_SHA] (manual attestation naming the head) # [--binary-review-sha FULL_SHA] (manual binary-byte, ownership, license, and NOTICE review) -# DSC/credential review record format (review or PR comment by another reviewer): -# Security review: FULL_SHA -# Result: accepted -# Include the reviewed scope and reasoning in that record. # Exit: 0 may merge, 1 must not, 2 could not determine. # -# Every positive result is bound to one server-observed head, base tip, complete -# check set, provider review commit, review-thread set, changed-file set, and -# readable compatibility surface. Unknown or incomplete evidence is never +# This script intentionally does not re-derive PR/head/base identity binding, +# branch-protection required-check contexts, or a checks rollup: those are +# GitHub's own job, enforced natively by required status checks on master +# (see docs/proposed-merge-gate-ci.md). Every check here is bound to one +# server-observed head SHA; unknown or incomplete evidence is never # converted into an empty successful set. set -uo pipefail @@ -59,7 +58,7 @@ while [ $# -gt 0 ]; do shift ;; -h|--help) - sed -n '2,17p' "$0" + sed -n '2,15p' "$0" exit 0 ;; -*) @@ -113,37 +112,24 @@ say() { printf ' %-13s %s\n' "$1" "$2"; } bad() { say "BLOCK" "$1"; fail=1; } unknown() { say "INDETERMINATE" "$1"; uncertain=1; } die() { echo "$1" >&2; exit 2; } - # A malformed response is different from a valid empty result. Validate the # outer shape before extracting fields so jq errors cannot become empty values. -# Independent acceptance decides whether a change is a production regression, -# whether its branch is dedicated, and whether it carries the required type:rectify label. : >"$errfile" if ! meta=$(gh pr view "$PR" --repo "$REPO" \ - --json headRefOid,baseRefOid,baseRefName,mergeable,mergeStateStatus,isDraft,state,title,body,changedFiles 2>"$errfile"); then + --json headRefOid,baseRefName,title,body,changedFiles 2>"$errfile"); then die "could not read PR #$PR in $REPO" fi if ! jq -e ' type == "object" and (.headRefOid | type == "string" and test("^[0-9a-fA-F]{40}$")) and - (.baseRefOid | type == "string" and test("^[0-9a-fA-F]{40}$")) and (.baseRefName | type == "string" and length > 0) and - (.mergeable | type == "string") and - (.mergeStateStatus | type == "string") and - (.isDraft | type == "boolean") and - (.state | type == "string") and (.title | type == "string") and (.changedFiles | type == "number" and floor == . and . >= 0) ' <<<"$meta" >/dev/null 2>&1; then die "PR metadata was not a valid complete JSON object" fi head=$(jq -r '.headRefOid' <<<"$meta") -base_ref_oid=$(jq -r '.baseRefOid' <<<"$meta") base=$(jq -r '.baseRefName' <<<"$meta") -mergeable=$(jq -r '.mergeable' <<<"$meta") -mstate=$(jq -r '.mergeStateStatus' <<<"$meta") -draft=$(jq -r '.isDraft' <<<"$meta") -pstate=$(jq -r '.state' <<<"$meta") changed_files_expected=$(jq -r '.changedFiles' <<<"$meta") short=${head:0:7} title=$(jq -r '.title' <<<"$meta") @@ -168,32 +154,7 @@ if [ -n "$BINARY_REVIEW_SHA" ] && ! [[ "$BINARY_REVIEW_SHA" =~ ^[0-9a-fA-F]{40}$ elif [ -n "$BINARY_REVIEW_SHA" ] && [ "$BINARY_REVIEW_SHA" != "$head" ]; then bad "binary review attestation names a different commit than the PR head" fi - -echo "PR #$PR ($REPO) head=$short base=$base $mergeable/$mstate" -[ "$pstate" = "OPEN" ] || bad "PR is $pstate, not OPEN" -[ "$draft" = "false" ] || bad "draft" - -case "$mergeable" in - MERGEABLE) say "ok" "no conflicts" ;; - CONFLICTING) bad "conflicts with $base — rebase first" ;; - *) unknown "mergeability is '$mergeable'; re-run when GitHub has determined it" ;; -esac -case "$mstate" in - BEHIND) bad "head is BEHIND $base — its checks and review describe a stale tree; rebase" ;; - DIRTY) bad "merge state DIRTY — conflicts" ;; - UNKNOWN) unknown "merge state UNKNOWN; re-run in a moment" ;; - BLOCKED) bad "merge state BLOCKED — GitHub is refusing this merge" ;; - CLEAN|HAS_HOOKS) say "ok" "merge state $mstate is not stale" ;; - UNSTABLE) bad "merge state UNSTABLE — GitHub has not established a mergeable result" ;; - DRAFT) bad "merge state DRAFT — the PR is not ready for merge" ;; - *) unknown "unrecognised merge state '$mstate'" ;; -esac - -if [ "$base" = "master" ]; then - say "ok" "based on master (CI actually runs)" -else - bad "based on '$base', not master — ci.yml does not fire, so checks here prove nothing" -fi +echo "PR #$PR ($REPO) head=$short base=$base" # Capture the base tip independently. A PR can retain the same base name while # the branch advances during this run. @@ -206,191 +167,6 @@ if [ "$base_tip_status" -ne 0 ] || ! [[ "$base_tip" =~ ^[0-9a-fA-F]{40}$ ]]; the else say "ok" "captured base tip ${base_tip:0:7}" fi -if [ -n "$base_tip" ] && [ "$base_ref_oid" != "$base_tip" ]; then - unknown "PR base OID $base_ref_oid differs from the current '$base' tip $base_tip" -fi - -# Bind the reviewed head to the actual base lineage returned by GitHub. The -# compare API is queried with the captured OIDs and must say that base is an -# ancestor of head; mergeability alone does not establish that relationship. -if [ -n "$base_tip" ] && [ "$base_ref_oid" = "$base_tip" ]; then - : >"$errfile" - compare_status=0 - comparison=$(gh api "repos/$REPO/compare/${base_tip}...${head}" 2>"$errfile") || compare_status=$? - if [ "$compare_status" -ne 0 ] || ! jq -e --arg base "$base_tip" ' - type == "object" and - (.status | type == "string" and (. == "ahead" or . == "identical")) and - (.behind_by | type == "number" and floor == . and . == 0) and - (.merge_base_commit | type == "object") and - (.merge_base_commit.sha | type == "string" and test("^[0-9a-fA-F]{40}$") and . == $base) - ' <<<"$comparison" >/dev/null 2>&1; then - unknown "base/head compare did not prove that the captured base tip is an ancestor" - else - say "ok" "compare API binds base tip ${base_tip:0:7} as head's merge base" - fi -fi - -# Branch protection is the source of required check contexts. A pass list with -# an omitted required context is not a complete check result. -: >"$errfile" -protection_status=0 -required_contexts="" -protection=$(gh api "repos/$REPO/branches/$base/protection/required_status_checks" 2>"$errfile") || protection_status=$? -if [ "$protection_status" -ne 0 ]; then - unknown "could not read required status-check contexts for $base" - required_contexts="" -elif ! jq -e ' - type == "object" and - (.strict | type == "boolean" and . == true) and - ((.contexts // []) | type == "array" and all(.[]; type == "string" and length > 0)) and - ((.checks // []) | type == "array" and all(.[]; type == "object" and (.context | type == "string" and length > 0))) -' <<<"$protection" >/dev/null 2>&1; then - unknown "required status-check response was malformed" - required_contexts="" -else - required_contexts=$(jq -r '((.contexts // []) + ([.checks // [] | .[]? | .context] | map(select(type == "string" and length > 0))) | unique)[]' <<<"$protection") - documented_contexts=$'Dependency security\nFrontend build\nGitGuardian Security Checks\nRequired checks\nRust format' - if [ -z "$required_contexts" ]; then - unknown "branch protection returned no required status-check contexts" - elif missing_documented=$(comm -23 <(sort -u <<<"$documented_contexts") <(sort -u <<<"$required_contexts")) && [ -n "$missing_documented" ]; then - bad "branch protection omits $(wc -l <<<"$missing_documented" | tr -d ' ') documented required check context(s)" - elif unexpected_documented=$(comm -13 <(sort -u <<<"$documented_contexts") <(sort -u <<<"$required_contexts")) && [ -n "$unexpected_documented" ]; then - bad "branch protection includes $(wc -l <<<"$unexpected_documented" | tr -d ' ') undocumented required check context(s)" - else - say "ok" "loaded $(wc -l <<<"$required_contexts" | tr -d ' ') required check context(s)" - fi -fi -printf '%s\n' "$required_contexts" >"$tmpdir/required-contexts" - -# gh uses pass/fail/pending/skipping/cancel buckets. Preserve command status, -# then parse JSON and require each protected context individually. -: >"$errfile" -check_status=0 -context_report_valid=false -check_bad=1 -all_bad=1 -buckets=$(gh pr checks "$PR" --repo "$REPO" --json bucket,name 2>"$errfile") || check_status=$? -if [ -z "$buckets" ]; then - unknown "checks query returned no JSON" -elif ! jq -e 'type == "array" and all(.[]; type == "object" and (.name | type == "string" and length > 0) and (.bucket | type == "string"))' <<<"$buckets" >/dev/null 2>&1; then - unknown "checks query returned malformed JSON" -else - # The first validation accepts only the documented buckets; keep this second - # check explicit because jq's precedence is easy to misread in a gate. - if ! jq -e 'all(.[]; (.bucket == "pass" or .bucket == "fail" or .bucket == "pending" or .bucket == "skipping" or .bucket == "cancel"))' <<<"$buckets" >/dev/null 2>&1; then - unknown "checks query contained an unknown bucket" - elif [ "$check_status" -ne 0 ] && [ "$(jq '[.[] | select(.bucket == "fail" or .bucket == "cancel" or .bucket == "pending")] | length' <<<"$buckets")" -eq 0 ]; then - unknown "checks command failed even though no failing or pending result was returned" - elif [ "$(jq 'length' <<<"$buckets")" -eq 0 ]; then - bad "no checks reported for this PR" - else - printf '%s' "$buckets" >"$tmpdir/check-buckets.json" - context_report_status=0 - context_report_valid=false - context_report=$(jq --rawfile contexts "$tmpdir/required-contexts" ' - . as $buckets | ($contexts | split("\n") | map(select(length > 0))) | - map(. as $context | [$buckets[] | select(.name == $context)] | - {name: $context, state: (if length == 0 then "missing" - elif all(.[]; .bucket == "pass") then "pass" - else map(.bucket) | unique | join(",") end)}) | - {total: length, failed: ([.[] | select(.state != "pass")] | length), - examples: ([.[] | select(.state != "pass")][0:8] | map( - "required check \u0027" + (.name | gsub("[[:cntrl:]]"; "?") | .[0:80]) + "\u0027 " + - (if .state == "missing" then "was not reported" else "is not passing (" + .state + ")" end)))} - ' <"$tmpdir/check-buckets.json") || context_report_status=$? - if [ "$context_report_status" -ne 0 ] || ! jq -e '. as $report | type == "object" and ($report.total | type == "number" and floor == . and . >= 0) and ($report.failed | type == "number" and floor == . and . >= 0 and . <= $report.total) and ($report.examples | type == "array" and length <= 8 and all(.[]; type == "string"))' <<<"$context_report" >/dev/null 2>&1; then - unknown "could not compute bounded required-check diagnostics" - else - context_report_valid=true - check_bad=$(jq -r '.failed' <<<"$context_report") - context_count=$(jq -r '.total' <<<"$context_report") - if [ "$check_bad" -gt 0 ]; then - bad "$check_bad of $context_count required check contexts are not passing; up to 8 bounded examples: $(jq -r '.examples | join("; ")' <<<"$context_report")" - else - say "ok" "all $context_count required check contexts passed" - fi - fi - all_bad=$(jq '[.[] | select(.bucket == "fail" or .bucket == "cancel" or .bucket == "pending")] | length' <<<"$buckets") - skipped=$(jq '[.[] | select(.bucket == "skipping")] | length' <<<"$buckets") - [ "$all_bad" -eq 0 ] || bad "$all_bad reported check(s) are failing, cancelled, or pending" - [ "$skipped" -eq 0 ] || say "note" "$skipped skipped check(s) await explicit workflow allowlist and path-condition validation" - fi -fi - -# The checks rollup is head-bound by its PR endpoint, but it does not expose a -# check-run SHA in `gh pr checks`. Verify the complete provider check-run pages -# and commit-status response independently so a malformed or mixed response -# cannot become positive evidence. The required-context decision above remains -# authoritative for branch protection, including status-only contexts. -: >"$errfile" -check_runs_status=0 -check_runs=$(gh api --paginate --slurp "repos/$REPO/commits/$head/check-runs?per_page=100" 2>"$errfile") || check_runs_status=$? -if [ "$check_runs_status" -ne 0 ] || ! jq -e --arg head "$head" ' - type == "array" and length > 0 and - all(.[]; type == "object" and - (.total_count | type == "number" and floor == . and . >= 0) and - (.check_runs | type == "array" and all(.[]; - type == "object" and - (.id | type == "number" and floor == . and . >= 0) and - (.name | type == "string" and length > 0) and - (.status == "completed") and - (.conclusion | type == "string" and (. == "success" or . == "skipped" or . == "neutral" or . == "failure" or . == "cancelled" or . == "timed_out" or . == "action_required" or . == "stale")) and - (.head_sha | type == "string" and test("^[0-9a-fA-F]{40}$") and . == $head) - ))) and - ((map(.total_count) | unique | length) == 1) and - ((map(.check_runs | length) | add) == .[0].total_count) and - ((map(.check_runs) | add | map(.id) | unique | length) == .[0].total_count) -' <<<"$check_runs" >/dev/null 2>&1; then - unknown "could not validate complete head-bound check-run evidence" -else - printf '%s' "$check_runs" >"$tmpdir/check-runs.json" - failed_runs_status=0 - failed_runs=$(jq --rawfile contexts "$tmpdir/required-contexts" ' - ($contexts | split("\n")) as $required | - [.[] | .check_runs[] | . as $run | - select((.conclusion != "success" and .conclusion != "neutral" and .conclusion != "skipped") or - (.conclusion != "success" and ($required | index($run.name)) != null))] | length - ' <(printf '%s' "$check_runs")) || failed_runs_status=$? - if [ "$failed_runs_status" -ne 0 ] || ! [[ "$failed_runs" =~ ^[0-9]+$ ]]; then - unknown "could not evaluate refreshed check-run conclusions" - elif [ "$failed_runs" -gt 0 ]; then - bad "$failed_runs refreshed check run(s) are failed or required-but-not-successful" - else - say "ok" "completed check-run pages are successful and bound to head $short" - fi -fi -: >"$errfile" -statuses_status=0 -statuses=$(gh api --paginate --slurp "repos/$REPO/commits/$head/status?per_page=100" 2>"$errfile") || statuses_status=$? -printf '%s' "$statuses" >"$tmpdir/combined-status-pages.json" -if [ "$statuses_status" -ne 0 ] || ! jq -e --arg head "$head" ' - type == "array" and length > 0 and - all(.[]; type == "object" and - (.sha | type == "string" and test("^[0-9a-fA-F]{40}$") and . == $head) and - (.total_count | type == "number" and floor == . and . >= 0) and - (.state | type == "string" and (. == "success" or . == "pending" or . == "failure" or . == "error")) and - (.statuses | type == "array" and all(.[]; - type == "object" and - (.id | type == "number" and floor == . and . >= 0) and - (.context | type == "string" and length > 0) and - (.state == "success" or .state == "failure" or .state == "error") - )) - ) and - ((map(.total_count) | unique | length) == 1) and - ((map(.statuses | length) | add) == .[0].total_count) and - ((map(.statuses) | add | map(.id) | unique | length) == .[0].total_count) and - ((map(.statuses) | add | map(.context) | unique | length) == .[0].total_count) and - (if .[0].state == "pending" then .[0].total_count == 0 and (map(.statuses | length) | add) == 0 - elif .[0].state == "failure" or .[0].state == "error" then .[0].state as $aggregate | all(.[]; .state == $aggregate) - else all(.[]; .state == "success") end) -' <"$tmpdir/combined-status-pages.json" >/dev/null 2>&1; then - unknown "could not validate complete head-bound commit-status evidence" -elif jq -e 'any(.[]; (.state == "failure" or .state == "error") or any(.statuses[]; .state == "failure" or .state == "error"))' <"$tmpdir/combined-status-pages.json" >/dev/null; then - bad "combined commit-status evidence reports a failure" -else - say "ok" "complete commit-status pages are bound to head $short" -fi - # Scan all published PR metadata. Commit messages are paginated because they # can become squash subjects or release evidence independently of the patch. : >"$errfile" @@ -422,9 +198,9 @@ if [ -z "${metadata_commit_total:-}" ] || [ "$metadata_status" -ne 0 ] || ! jq - (.commit | type == "object") and (.commit.message | type == "string") and (.commit.author | type == "object" and - (.name | type == "string") and (.email | type == "string" and test("^[A-Za-z0-9.!#$%&*+/=?^_`{|}~-]+@[A-Za-z0-9][A-Za-z0-9.-]*\\.[A-Za-z]{2,}$"))) and + (.name | type == "string") and (.email | type == "string" and test("^[A-Za-z0-9.!#$%&*+/=?^_`{|}~-]+@[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$"))) and (.commit.committer | type == "object" and - (.name | type == "string") and (.email | type == "string" and test("^[A-Za-z0-9.!#$%&*+/=?^_`{|}~-]+@[A-Za-z0-9][A-Za-z0-9.-]*\\.[A-Za-z]{2,}$"))) and + (.name | type == "string") and (.email | type == "string" and test("^[A-Za-z0-9.!#$%&*+/=?^_`{|}~-]+@[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$"))) and ((.author == null) or (.author | type == "object" and (.login | type == "string"))) and ((.committer == null) or (.committer | type == "object" and (.login | type == "string"))))) ' <<<"$metadata_commits" >/dev/null 2>&1; then @@ -442,297 +218,6 @@ else $raw_prbody $commit_messages" fi - -# Require a completed checklist item and a same-repository line permalink. -# The repository template puts the permalink on the item's indented -# continuation, so accept it there as well as in an inline Markdown link. -# A filename in prose, a foreign link, or an anchor outside the current -# checklist file is not completion evidence. -read_review_checklist() { - local response content decoded decode_status - : >"$errfile" - response=$(gh api "repos/$REPO/contents/review-checklist.md?ref=$head" 2>"$errfile") || return 1 - content=$(jq -er 'select(.encoding == "base64") | .content | strings' <<<"$response") || return 1 - decode_status=0 - decoded=$(printf '%s' "${content//$'\n'/}" | base64 --decode 2>"$errfile") || decode_status=$? - if [ "$decode_status" -ne 0 ]; then - decode_status=0 - decoded=$(printf '%s' "${content//$'\n'/}" | base64 -D 2>"$errfile") || decode_status=$? - fi - [ "$decode_status" -eq 0 ] && [ -n "$decoded" ] || return 1 - review_checklist="$decoded" -} -checklist_link_ok() { - local body="$1" checklist="$2" line awaiting_permalink=0 link anchor - local checked='^[[:space:]]*-[[:space:]]*\[[xX]\][[:space:]]+' - local permalink="https://github\.com/${OWNER}/${NAME}/blob/${head}/review-checklist\.md#L[0-9]+" - local permalink_boundary="${permalink}"'([[:space:]]|\)|$)' - while IFS= read -r line; do - if printf '%s\n' "$line" | grep -Eq "$checked"; then - if printf '%s\n' "$line" | grep -Eiq "$permalink_boundary"; then - awaiting_permalink=2 - elif printf '%s\n' "$line" | grep -Eiq 'review-checklist\.md'; then - awaiting_permalink=1 - else - awaiting_permalink=0 - fi - elif [ "$awaiting_permalink" -eq 1 ] && printf '%s\n' "$line" | grep -Eq '^[[:space:]]+'; then - if printf '%s\n' "$line" | grep -Eiq "$permalink_boundary"; then - awaiting_permalink=2 - fi - elif [ "$awaiting_permalink" -ne 2 ]; then - awaiting_permalink=0 - fi - if [ "$awaiting_permalink" -eq 2 ]; then - while IFS= read -r link; do - anchor=${link##*#L} - anchor=${anchor%%[^0-9]*} - if sed -n "${anchor}p" <<<"$checklist" | grep -Eq '^[[:space:]]*-[[:space:]]*\[[ xX]\][[:space:]]+[^[:space:]]'; then - return 0 - fi - done < <(printf '%s\n' "$line" | grep -Eio "$permalink_boundary") - awaiting_permalink=0 - fi - done <<<"$body" - return 1 -} -review_checklist="" -checklist_status=0 -read_review_checklist || checklist_status=$? -if [ "$checklist_status" -ne 0 ]; then - unknown "could not read review-checklist content for link validation" -elif ! checklist_link_ok "$prbody" "$review_checklist"; then - bad "description lacks a completed same-repository review-checklist link to an existing line" -else - say "ok" "description links a completed review-checklist item" -fi - -body_section_has_content() { - local body="$1" labels="$2" allow_placeholders="${3:-false}" - awk -v labels="$labels" -v allow_placeholders="$allow_placeholders" ' - function heading(line, lower) { - lower = tolower(line) - sub(/^[[:space:]]*#+[[:space:]]*/, "", lower) - return lower ~ ("^(" labels ")[[:space:]]*:[[:space:]]*[^[:space:]]") || - lower ~ ("^(" labels ")[[:space:]]*:?[[:space:]]*$") - } - function template_prompt(line, lower) { - lower = tolower(line) - return lower == "what concrete user or maintainer workflow changes, and why now?" || - lower ~ /^-[[:space:]]*\[[xX]\][[:space:]]/ || - lower ~ /^-[[:space:]]*exact candidate sha:/ || - lower ~ /^-[[:space:]]*commands and results[[:space:]]*\(.*\):[[:space:]]*$/ || - lower ~ /^-[[:space:]]*captured\/fixture\/live scope and known limitations:[[:space:]]*$/ || - lower ~ /^-[[:space:]]*manual\/ui evidence[[:space:]]*\(.*\):[[:space:]]*$/ - } - function after_colon(line, value) { - value = line - sub(/^[^:]*:[[:space:]]*/, "", value) - return value - } - function meaningful(value, lower) { - gsub(//, "", value) - lower = tolower(value) - gsub(/^[[:space:]]+|[[:space:]]+$/, "", lower) - # A terminal dot or similar punctuation does not make an otherwise empty - # template answer substantive (for example, `N/A.` or `TBD.`). - gsub(/[,.;:!?]+$/, "", lower) - gsub(/[[:space:]]+$/, "", lower) - if (lower == "") return 0 - # Fence delimiters and thematic breaks are structure, not policy content. - if (lower ~ /^(```|~~~)/) return 0 - if (lower ~ /^([-][[:space:]]*){3,}$/ || lower ~ /^([*][[:space:]]*){3,}$/ || lower ~ /^(_[[:space:]]*){3,}$/) return 0 - if (allow_placeholders == "true" && lower ~ /^(n\/a|none|no impact)$/) return 1 - return lower !~ /^(n\/a|none|pending|todo|tbd|not applicable|unaffected|not affected|not impacted|no impact)$/ - } - { - # The canonical PR template uses labelled list fields as well as - # headings. Accept a filled field for the requested policy label, but - # never the untouched label or a checkbox by itself. - lower = tolower($0) - gsub(//, "", lower) - if (pending_list) { - if (lower ~ /^[[:space:]]*$/) { - pending_list = 0 - } else if (lower ~ /^[[:space:]]*[-#]/) { - pending_list = 0 - } else if (lower !~ /^[[:space:]]+/) { - pending_list = 0 - } else if (lower ~ /:[[:space:]]*[^[:space:]]/ && meaningful(after_colon(lower))) { - found = 1 - exit - } else if (lower ~ /:[[:space:]]*$/) { - pending_list = 2 - } else if (pending_list && lower ~ /[^[:space:]]/) { - if (!template_prompt($0) && lower !~ /^[[:space:]]*", "", sys.stdin.read(), flags=re.S) -active = fenced = False -for line in text.splitlines(): - heading = re.match(r"^\s*#{1,6}\s+(.+?)\s*$", line) - if heading: - active = bool(re.fullmatch(r"(?:test or reproduction command|commands and results|validation and evidence):?", heading[1], re.I)) - fenced = False - continue - if not active: - continue - if re.match(r"^\s*(?:```|~~~)", line): - fenced = not fenced - continue - candidates = [(line.strip()[2:] if line.strip().startswith("$ ") else line.strip())] if fenced else re.findall(r"`([^`]+)`", line) - for command in candidates: - if re.search(r"\.\.\.|…|<[^>]+>", command): - continue - try: - words = shlex.split(command) - except ValueError: - continue - if words and words[0] == "corepack": - words = words[1:] - if len(words) >= 2 and (words[0] in {"python", "python3", "pytest", "pnpm", "npm", "node", "cargo", "make", "bash", "sh", "gh"} or words[0].startswith("scripts/")): - sys.exit(0) -sys.exit(1) -' <<<"$1" -} -if ! body_has_validation_command "$prbody"; then - bad "description lacks an actual test or reproduction command" -fi - -# P4 is a three-part design record, not a generic scope paragraph. When a -# patch adds implementation code, each question must have an answer that is -# more than the untouched template label. -body_has_p4_answers() { - local body="$1" - body_section_has_content "$body" 'existing component reused' && - body_section_has_content "$body" 'what is deleted' && - body_section_has_content "$body" 'what breaks if this is not built' -} - -body_has_platform_evidence() { - local body="$1" host="$2" - # A named host field must include evidence on that line or in a following - # content line. A bare list label is only a template prompt, not evidence. - python3 -c ' -import re, sys -host = sys.argv[1].lower() -placeholders = { - "", "none", "n/a", "not applicable", "unaffected", "not affected", - "not impacted", "no impact", "pending", "todo", "tbd", -} -marker = r"(validation|evidence|test|check|unaffected|not applicable|not affected|not impact)" -def normalize(value): - return value.strip().lower().rstrip(".,;:!?").strip() -def continuation_evidence(value): - value = normalize(value) - if not value or value in placeholders or re.match(r"^(?:[-*]\s+)?(?:note|notes|status|tracking|todo)\b", value): - return False - command = re.search(r"(?:^|[`$\s])(?:python(?:3)?|pytest|pnpm|npm|node|cargo|make|bash|sh|gh)(?:[\s`]|$)", value) - affirmative = re.search(r"\b(?:passed|succeeded|validated|completed)\b", value) - negated = re.search(r"\b(?:not|never|failed|failure|without|no)\b(?:\W+\w+){0,4}\W+\b(?:pass(?:ed)?|succeed(?:ed)?|validat(?:ed|ion)|complet(?:ed|ion))\b", value) - outcome = affirmative and not negated and re.search(r"\b(?:ci|test|check|validation|windows|macos)\b", value) - unaffected = re.search(r"\b(?:unaffected|not affected|not impacted)\b", value) and re.search(r"\b(?:because|as|this)\b", value) - return bool(outcome or unaffected) -waiting = False -waiting_list_indent = None -for raw in sys.stdin.read().splitlines(): - visible = re.sub(r"|$)", "", raw) - indent = len(visible) - len(visible.lstrip(" \t")) - lower = visible.strip().lower() - if not lower: - continue - is_fence = bool(re.match(r"^(?:```|~~~)", lower)) - is_list_item = bool(re.match(r"^[-*]\s+", lower)) - is_heading = bool(re.match(r"^#{1,6}\s+", lower)) - is_field = host in lower and bool(re.search(marker, lower)) - if waiting: - # Empty fence delimiters carry no validation result. Keep looking so a - # real fenced command can still establish the named host evidence. - if is_fence: - continue - # A new evidence field or heading ends the preceding empty field; it - # cannot be treated as the earlier host evidence. - if ((waiting_list_indent is not None and is_list_item and indent <= waiting_list_indent) or - is_heading or (is_list_item and re.search(marker, lower))): - waiting = False - waiting_list_indent = None - elif continuation_evidence(lower): - raise SystemExit(0) - else: - waiting = False - waiting_list_indent = None - if not is_field: - continue - if ":" in lower: - value = normalize(re.sub(r"^#{1,6}\s+", "", lower.split(":", 1)[1])) - # Inline evidence is subject to the same quality threshold as a - # continuation. Otherwise an arbitrary sentence after the host label - # turns a template field into acceptance evidence. - if continuation_evidence(value): - raise SystemExit(0) - # Headings and list labels without an inline answer may be completed by a - # following substantive validation result. All other bare forms fail. - waiting = True - waiting_list_indent = indent if is_list_item else None -raise SystemExit(1) -' "$host" <<<"$body" -} - # Paginate changed files through the REST endpoint; gh pr view hard-codes a # first:100 GraphQL fragment in some versions. Retain the line counts as well: # the privacy scan can only be complete when the textual diff describes every @@ -741,7 +226,6 @@ raise SystemExit(1) files_status=0 files=$(gh api --paginate --slurp "repos/$REPO/pulls/$PR/files?per_page=100" 2>"$errfile") || files_status=$? changed_records="$tmpdir/changed-files.tsv" -changed_inventory_complete=false if [ "$files_status" -ne 0 ] || ! jq -e ' type == "array" and (all(.[]; type == "array" and all(.[]; @@ -771,232 +255,8 @@ else unknown "PR reports $changed_files_expected changed files beyond the REST files API cap" elif [ "$changed_count" -ne "$changed_files_expected" ] || [ "$unique_changed_count" -ne "$changed_count" ]; then unknown "changed-file response has $changed_count unique records; PR metadata reports $changed_files_expected" - else - changed_inventory_complete=true - fi -fi - -# The PR workflow has exactly three job-level skip paths. Native and bundle -# matrix jobs may skip only when the same changed-file patterns that ci.yml -# uses evaluate false; compiler-cache retention skips on every PR because its -# job condition is master-only. Every other skipped check is evidence of an -# unreviewed workflow condition and blocks rather than becoming "optional". -validate_skipped_ci_jobs() { - local source="$1" names_file="$2" policy_ok_var="${3:-skipped_ci_policy_ok}" encoded name paths_file - local native_pattern bundle_pattern - native_pattern='^(\.github/workflows/ci\.yml|\.github/actions/setup-windows-native/|rust-toolchain\.toml|src-tauri/|tools/)' - bundle_pattern='^(\.github/workflows/ci\.yml|\.github/actions/setup-windows-native/|packaging/mcpb/|package\.json|pnpm-lock\.yaml|\.node-version|vite\.config\.ts|tsconfig\.json|postcss\.config\.js|index\.html|src/|src-tauri/(src/|crates/|Cargo\.lock|Cargo\.toml|.*/Cargo\.toml|tauri\.conf\.json|build\.rs|icons/)|LICENSE$|NOTICE$|THIRD_PARTY_LICENSES\.txt$|THIRD_PARTY_LICENSES_RUST\.txt$|scripts/(capture-package-log(\.test)?\.py|check-mcpb-bundle(\.test)?\.py|package-mcpb\.mjs|check-license-metadata\.mjs|check-dependency-inventory\.mjs|check-windows-bundle-resources\.ps1|check-macos-bundle-resources(\.mutation)?\.mjs)$)' - - [ -s "$names_file" ] || return - if [ "$changed_inventory_complete" != true ]; then - printf -v "$policy_ok_var" '%s' false - unknown "$source reports skipped CI job(s), but their path conditions could not be validated from the complete changed-file set" - return fi - paths_file="$tmpdir/skipped-ci-paths" - awk -F '\t' '{ print $1; if ($2 == "renamed") print $5 }' "$changed_records" >"$paths_file" - while IFS= read -r encoded; do - name=$(printf '%s' "$encoded" | base64 --decode 2>/dev/null) || name=$(printf '%s' "$encoded" | base64 -D 2>/dev/null) || { - printf -v "$policy_ok_var" '%s' false - unknown "$source returned an unreadable skipped CI job name" - continue - } - case "$name" in - "Native checks (windows-latest)"|"Native checks (macos-latest)") - if grep -Eq "$native_pattern" "$paths_file"; then - printf -v "$policy_ok_var" '%s' false - bad "$source reports a skipped native CI matrix job despite native-scope changed files" - else - say "note" "$source reports an allowlisted skipped native CI matrix job for a non-native change" - fi - ;; - "Bundle smoke (windows-latest)"|"Bundle smoke (macos-latest)") - if grep -Eq "$bundle_pattern" "$paths_file"; then - printf -v "$policy_ok_var" '%s' false - bad "$source reports a skipped bundle CI matrix job despite bundle-scope changed files" - else - say "note" "$source reports an allowlisted skipped bundle CI matrix job for a non-bundle change" - fi - ;; - "Retain two compiler-cache snapshots per OS") - say "note" "$source reports the allowlisted master-only compiler-cache retention job skipped on this PR" - ;; - *) - printf -v "$policy_ok_var" '%s' false - bad "$source reports a skipped CI job outside the explicit workflow allowlist" - ;; - esac - done <"$names_file" -} - -check_bucket_skips="$tmpdir/check-bucket-skips.b64" -check_run_skips="$tmpdir/check-run-skips.b64" -skipped_ci_policy_ok=true -if [ ! -f "$tmpdir/check-buckets.json" ]; then - skipped_ci_policy_ok=false -elif ! jq -r '.[] | select(.bucket == "skipping") | .name | @base64' "$tmpdir/check-buckets.json" >"$check_bucket_skips"; then - skipped_ci_policy_ok=false - unknown "could not read skipped check names from the checks rollup" -elif [ -f "$tmpdir/check-runs.json" ] && ! jq -r '.[] | .check_runs[] | select(.conclusion == "skipped") | .name | @base64' "$tmpdir/check-runs.json" >"$check_run_skips"; then - skipped_ci_policy_ok=false - unknown "could not read skipped check names from refreshed check-run evidence" -else - validate_skipped_ci_jobs "checks rollup" "$check_bucket_skips" - [ -f "$tmpdir/check-runs.json" ] && validate_skipped_ci_jobs "refreshed check-run evidence" "$check_run_skips" -fi -[ "$context_report_valid" = true ] && [ "$check_bad" -eq 0 ] && [ "$all_bad" -eq 0 ] && [ "$skipped_ci_policy_ok" = true ] && say "ok" "all reported checks concluded successfully" - -# Existing workflow changes require the rollback and migration-compatibility -# notes mandated by the project review flow. The complete REST file set, rather -# than the rendered diff, is the authority for this conditional requirement. -workflow_change=0 -if [ -n "$files_status" ] && [ "$files_status" -eq 0 ]; then - workflow_change=$(jq -r ' - (if all(.[]; type == "array") then flatten else . end) | - any(.[]; (.filename | startswith(".github/workflows/")) or - ((.previous_filename? // "") | startswith(".github/workflows/"))) - ' <<<"$files") fi -native_frontend_change=0 -if [ "$files_status" -eq 0 ]; then - native_frontend_change=$(jq -r ' - (if all(.[]; type == "array") then flatten else . end) | - any(.[]; [ .filename, (.previous_filename? // "") ][] | - test("^(src-tauri/|src/|scripts/.*\\.(ts|tsx|js|mjs)$)")) - ' <<<"$files") -fi - -# Treat source additions as implementation work only when the REST line totals -# prove that bytes were added. Rename paths are considered for every path -# policy below, so moving platform, migration, or sensitive code cannot evade -# the relevant review record. -implementation_code_added=false -dependency_manifest_added=false -platform_sensitive_change=false -migration_change=false -if [ "$files_status" -eq 0 ]; then - implementation_code_added=$(jq -r ' - (if all(.[]; type == "array") then flatten else . end) | - any(.[]; (.additions > 0) and (.filename | test("\\.(rs|ts|tsx|js|mjs|py|go|java|kt|swift|c|cc|cpp|h|hpp|sh|bash|ps1|psm1|sql)$"))) - ' <<<"$files") - dependency_manifest_added=$(jq -r ' - (if all(.[]; type == "array") then flatten else . end) | - any(.[]; (.additions > 0) and (.filename | test("(^|/)(package\\.json|Cargo\\.toml)$"))) - ' <<<"$files") - platform_sensitive_change=$(jq -r ' - (if all(.[]; type == "array") then flatten else . end) | - any(.[]; [.filename, (.previous_filename? // "")][] | - test("^(src-tauri/|src/.*\\.(rs|ts|tsx|js|mjs)$)|\\.(ps1|psm1)$|^\\.github/workflows/(ci\\.yml|release-mcpb-preview\\.yml)$|^\\.github/actions/setup-windows-native/|(^|/)(windows|macos|darwin|win32|local_files|paths)(/|[._-])"; "i")) - ' <<<"$files") - migration_change=$(jq -r ' - (if all(.[]; type == "array") then flatten else . end) | - any(.[]; [.filename, (.previous_filename? // "")][] | - test("(^|/)(migrations?|schema|database|db)(/|[._-])"; "i")) - ' <<<"$files") -fi - -# The review contract requires an explicit security-impact statement whenever -# either spelling of a renamed path touches a DSC, credential, or Tally surface. -# Treat native and frontend source conservatively: their generic filenames can -# carry DSC/Tally behavior. Use the complete REST inventory, including removals -# and prior rename paths; named tooling and documentation surfaces are included. -security_sensitive_change=0 -if [ "$files_status" -eq 0 ]; then - security_sensitive_change=$(jq -r ' - (if all(.[]; type == "array") then flatten else . end) | - any(.[]; [ .filename, (.previous_filename? // "") ][] | - ascii_downcase | test("^src-tauri/(crates|src)/|^src/|^docs/(tally|agent)/|^scripts/(bank_statement_import|sanitise-bbox-capture|prune-package-compiler-cache)|^\\.github/workflows/(ci\\.yml|release-mcpb-preview\\.yml|deploy-install-page\\.yml)$|(^|/)[^/]*(dsc|credential|tally)[^/]*(/|$)")) - ' <<<"$files") -fi -if [ "$security_sensitive_change" = "true" ]; then - if ! body_section_has_content "$prbody" 'security impact|security implications' true; then - bad "credential-sensitive path change lacks non-empty security-impact notes" - else - say "ok" "credential-sensitive path change includes security-impact notes" - fi - -fi -# DSC/credential changes require a separate security-focused reviewer comment. -# A general approval or the PR author’s impact notes are not that record. -security_reviewer_change=false -if [ "$files_status" -eq 0 ]; then - security_reviewer_change=$(jq ' - (if all(.[]; type == "array") then flatten else . end) | - any(.[]; [.filename, (.previous_filename? // "")][] | - (test("(^|[/_.-])(dsc|credential[s]?|certificate[s]?|keystore|secret[s]?)(?=[/_.-]|$|[A-Z])"; "i") or - test("^scripts/(merge-gate\\.sh|merge_gate_privacy\\.py|merge_gate_diff\\.py|bank_statement_import\\.py)$"; "i") or - test("^scripts/prune-package-compiler-cache\\.mjs$"; "i") or - test("^\\.github/workflows/(ci\\.yml|release-mcpb-preview\\.yml|deploy-install-page\\.yml)$"; "i") or - test("^src-tauri/Cargo\\.(toml|lock)$|^src-tauri/src/lib\\.rs$|^src/(AxalScreen|DocumentsScreen)\\.tsx$|^src-tauri/src/axal\\.rs$|^src-tauri/src/db/encrypted\\.rs$|^src-tauri/src/documents\\.rs$|^src-tauri/src/commands\\.rs$"; "i"))) - ' <<<"$files") -fi -validate_security_reviewer() { -if [ "$security_reviewer_change" = "true" ]; then - pr_author=$(jq -er '.user.login | strings | select(length > 0)' <<<"$metadata_pr" 2>/dev/null) || pr_author="" - security_review=false - if [ -n "$pr_author" ] && [ "$review_status" -eq 0 ] && [ "$comment_status" -eq 0 ]; then - printf '%s' "$reviews" >"$tmpdir/reviews.json" - printf '%s' "$comments" >"$tmpdir/comments.json" - security_review=$(jq -n --arg head "$head" --arg author "$pr_author" --slurpfile reviews "$tmpdir/reviews.json" --slurpfile comments "$tmpdir/comments.json" ' - def records: if all(.[]; type == "array") then flatten else . end; - def source_records: if length == 1 then .[0] else . end; - def reviewer: (.user.login | type == "string" and length > 0) and - .user.login != $author and (.user.type == "User" or .user.type == "Bot") and - ((.author_association == "OWNER" or .author_association == "MEMBER" or .author_association == "COLLABORATOR") or - (.user.login == "chatgpt-codex-connector[bot]" and .user.type == "Bot")); - def visible_body: .body | gsub("(?s:|$))"; "") | gsub("(?ms)^[ ]{0,3}(```|~~~)[^\\n]*\\n.*?(^[ ]{0,3}\\1[ \\t]*$|\\z)"; ""); - def substantive: - gsub("^[[:space:]]+|[[:space:]]+$"; "") as $text | - ($text | length >= 12) and - ($text | test("^(n/?a|none|tbd|todo|pending|unknown|not applicable)[[:space:].,:;!?-]*$"; "i") | not); - def has_substantive_line($label): - [try (capture("(?im)^#{0,6} *(?:" + $label + ")[ ]*:[ ]*(?.+)$").value | select(substantive)) catch empty] | length > 0; - def focused: (.body | type == "string") and - (visible_body | test("(?im)^#{0,6} *security review: *" + $head + " *$")) and - (visible_body | test("(?im)^result: *accepted *$")) and - (visible_body | has_substantive_line("reviewed +(dsc|credential|certificate|keystore|secret)([ ]+[A-Za-z][A-Za-z-]*)?")) and - (visible_body | has_substantive_line("security rationale|security reasoning")); - ([($reviews | source_records | records)[] | select(reviewer and focused and .commit_id == $head and - (.state == "APPROVED" or .state == "COMMENTED"))] + - [($comments | source_records | records)[] | select(reviewer and focused)]) | length > 0 - ' 2>/dev/null) || security_review=false - fi - if [ "$security_review" != "true" ]; then - unknown "credential-sensitive change lacks a separate current-head security-focused reviewer comment" - else - say "ok" "separate security-focused reviewer comment names full current head $short" - fi -fi -} -if [ "$workflow_change" = "true" ]; then - if ! body_section_has_content "$prbody" 'rollback notes|rollback procedure|migration/sync compatibility and rollback procedure'; then - bad "workflow change lacks non-empty rollback notes" - fi - if ! body_section_has_content "$prbody" 'migration compatibility|migration impact|migration/sync compatibility'; then - bad "workflow change lacks non-empty migration compatibility notes" - fi -fi -if [ "$native_frontend_change" = "true" ] && ! body_section_has_content "$prbody" 'migration compatibility|migration impact|migration/sync compatibility'; then - bad "native or frontend change lacks non-empty migration compatibility notes" -fi -if [ "$migration_change" = "true" ] && ! body_section_has_content "$prbody" 'rollback notes|rollback procedure|migration/sync compatibility and rollback procedure'; then - bad "database migration path lacks non-empty rollback notes" -fi -if [ "$implementation_code_added" = "true" ] && ! body_has_p4_answers "$prbody"; then - bad "implementation code addition lacks all three substantive P4 reuse, deletion, and omission answers" -fi -if [ "$dependency_manifest_added" = "true" ] && ! body_section_has_content "$prbody" 'dependency justification|dependency rationale|new dependency justification'; then - bad "dependency manifest addition lacks a substantive dependency justification" -fi -if [ "$platform_sensitive_change" = "true" ]; then - if ! body_has_platform_evidence "$prbody" windows; then - bad "platform-sensitive change lacks substantive Windows validation evidence or unaffected-host rationale" - fi - if ! body_has_platform_evidence "$prbody" macos; then - bad "platform-sensitive change lacks substantive macOS validation evidence or unaffected-host rationale" - fi -fi - # Read and validate the v1 surface as a required object. Any transport, # decoding, JSON, or schema failure is indeterminate; an unrelated nested # `path` must not turn an incomplete manifest into an empty pin set. Both the @@ -1073,7 +333,6 @@ if [ -n "$changed" ] && [ -n "$pinned" ] && [ -n "$base_pinned" ]; then bad "changed pinned paths omit the compatibility surface reseal" fi fi - # Scan destination paths and added payload lines. Removed/context lines never # enter the privacy scan. Binary additions are an explicit human-inspection # hold because their bytes are absent from a textual patch. @@ -1107,7 +366,6 @@ else jq -r '.records[] | [.destination, .added, .deleted, (if .textual_destination == null then 0 else 1 end), (if .binary then 1 else 0 end), (if .gitlink then 1 else 0 end)] | @tsv' <<<"$parsed_diff" >"$diff_stats" jq -r '.added_payload[]' <<<"$parsed_diff" >"$added_payload" - coverage_count=0 coverage_examples="" metadata_only_count=0 @@ -1117,6 +375,11 @@ else bounded_coverage_name() { local item item=$(sed -E 's#/(Users|home)/[^/[:space:]]+##g; s#[A-Za-z]:[\\/]+Users[\\/]+[^\\/[:space:]]+##g' <<<"$1") + # The privacy classifier's own identifier-shape detection also runs on + # the (already home-dir-redacted) filename: a coverage/metadata-only + # example is echoed verbatim into gate output, and a filename can itself + # carry an identifier, digest, or email shape. + item=$(printf '%s' "$item" | python3 "$script_dir/merge_gate_privacy.py" --redact-shapes 2>/dev/null) || item="$item" printf '%s' "${item:0:160}" } record_coverage_issue() { @@ -1136,7 +399,6 @@ else path_text="" if [ -s "$changed_records" ]; then while IFS=$'\t' read -r filename status rest_added rest_deleted previous_filename; do - [ "$status" = "removed" ] && continue match_count=$(awk -F '\t' -v filename="$filename" '$1 == filename { count++ } END { print count+0 }' "$diff_stats") if [ "$match_count" -ne 1 ]; then record_coverage_issue "omits or duplicates '$filename'" @@ -1152,6 +414,18 @@ else binary_count=$((binary_count + 1)) continue fi + if [ "$status" = "removed" ]; then + # A removed record has no new content, so it is excluded from the + # metadata-only/textual-destination checks below (its +++ destination + # is legitimately /dev/null). It must still be reconciled first: a + # removed record absent from the diff was already caught by the + # match_count check above, and its line totals must still agree with + # REST before it is excluded from further scanning. + if [ "$diff_added" -ne "$rest_added" ] || [ "$diff_deleted" -ne "$rest_deleted" ]; then + record_coverage_issue "line totals for '$filename' differ from REST metadata" + fi + continue + fi if [ "$textual" -ne 1 ] && [ "$diff_added" -eq 0 ] && [ "$diff_deleted" -eq 0 ]; then if [ "$rest_added" -eq 0 ] && [ "$rest_deleted" -eq 0 ]; then record_metadata_only "$filename" @@ -1164,6 +438,17 @@ else record_coverage_issue "line totals for '$filename' differ from REST metadata" fi done <"$changed_records" + # Reconciliation above is REST-to-diff only: it proves every REST record + # has exactly one matching diff section, but says nothing about a diff + # destination that has no REST record at all. Check the reverse direction + # too, so a diff section smuggled in outside the REST changed-file set + # cannot silently skip the privacy scan's REST-driven bookkeeping. + extra_diff_destinations=$(comm -13 <(cut -f1 "$changed_records" | sort -u) <(cut -f1 "$diff_stats" | sort -u)) + if [ -n "$extra_diff_destinations" ]; then + while IFS= read -r extra; do + record_coverage_issue "diff destination '$extra' has no corresponding REST record" + done <<<"$extra_diff_destinations" + fi # This option is an operator statement, never evidence inferred from the # PR body: it attests that every current binary addition/change byte and # its ownership, license, and NOTICE obligations were independently read. @@ -1187,390 +472,69 @@ else path_text=$(awk -F '\t' '$2 != "removed" { print $1 }' "$changed_records") fi scan_input_file="$tmpdir/privacy-scan-input" + scan_input_write_status=0 { printf '%s\n%s\n' "$privacy_metadata" "$path_text" cat "$added_payload" - } >"$scan_input_file" - privacy_result_status=0 - privacy_result=$(python3 "$script_dir/merge_gate_privacy.py" --head "$head" <"$scan_input_file") || privacy_result_status=$? - if [ "$privacy_result_status" -ne 0 ] || ! jq -e ' - type == "object" and - (.blockers | type == "array" and all(.[]; type == "string" and length > 0)) and - (.indeterminate | type == "array" and all(.[]; type == "string" and length > 0)) and - (.notes | type == "array" and all(.[]; type == "string" and length > 0)) - ' <<<"$privacy_result" >/dev/null 2>&1; then - unknown "privacy classifier failed or returned malformed output" + } >"$scan_input_file" || scan_input_write_status=$? + if [ "$scan_input_write_status" -ne 0 ]; then + unknown "could not assemble complete privacy scan input" else - while IFS= read -r message; do bad "$message"; done < <(jq -r '.blockers[]' <<<"$privacy_result") - while IFS= read -r message; do unknown "$message"; done < <(jq -r '.indeterminate[]' <<<"$privacy_result") - while IFS= read -r message; do say "note" "$message"; done < <(jq -r '.notes[]' <<<"$privacy_result") + privacy_result_status=0 + privacy_result=$(python3 "$script_dir/merge_gate_privacy.py" --head "$head" <"$scan_input_file") || privacy_result_status=$? + if [ "$privacy_result_status" -ne 0 ] || ! jq -e ' + type == "object" and + (.blockers | type == "array" and all(.[]; type == "string" and length > 0)) and + (.indeterminate | type == "array" and all(.[]; type == "string" and length > 0)) and + (.notes | type == "array" and all(.[]; type == "string" and length > 0)) + ' <<<"$privacy_result" >/dev/null 2>&1; then + unknown "privacy classifier failed or returned malformed output" + else + while IFS= read -r message; do bad "$message"; done < <(jq -r '.blockers[]' <<<"$privacy_result") + while IFS= read -r message; do unknown "$message"; done < <(jq -r '.indeterminate[]' <<<"$privacy_result") + while IFS= read -r message; do say "note" "$message"; done < <(jq -r '.notes[]' <<<"$privacy_result") + fi fi fi fi - -# Fetch mutable review evidence only in this late phase, immediately before the -# final PR/base identity fence. A prior snapshot can be invalidated by a later -# provider summary, security comment, or review thread without moving the head. +# A review naming the exact current head SHA. Closes #317: PRs were merged +# 2-8 minutes after opening, where "zero unresolved review threads" meant +# "the reviewer had not started", not "reviewed clean". A clean review can +# leave no review object at all -- only a reaction plus a summary comment -- +# so the absence of any review artifact naming this exact head is a FAIL, +# never a silent pass. : >"$errfile" -review_status=0 -provider_review="" -reviews=$(gh api --paginate --slurp "repos/$REPO/pulls/$PR/reviews" 2>"$errfile") || review_status=$? -if [ "$review_status" -ne 0 ]; then - unknown "could not read provider review records" -elif ! jq -e 'type == "array" and (all(.[]; type == "array") or all(.[]; type == "object"))' <<<"$reviews" >/dev/null 2>&1; then - unknown "provider review response was malformed" -else - provider_review=$(jq -r --arg head "$head" ' +review_evidence_status=0 +head_reviews=$(gh api --paginate --slurp "repos/$REPO/pulls/$PR/reviews" 2>"$errfile") || review_evidence_status=$? +review_names_head=false +if [ "$review_evidence_status" -eq 0 ] && jq -e ' + type == "array" and (all(.[]; type == "array") or all(.[]; type == "object")) +' <<<"$head_reviews" >/dev/null 2>&1; then + review_names_head=$(jq -r --arg head "$head" ' (if all(.[]; type == "array") then flatten else . end) | - map(select(.user.login == "chatgpt-codex-connector[bot]" and .user.type == "Bot" and - .state == "COMMENTED" and .commit_id == $head)) | - if length > 0 then "matched" else "" end - ' <<<"$reviews") -fi - -: >"$errfile" -comment_status=0 -comments=$(gh api --paginate --slurp "repos/$REPO/issues/$PR/comments" 2>"$errfile") || comment_status=$? -summary_good=0 -if [ "$comment_status" -ne 0 ]; then - unknown "could not read provider review summaries" -elif ! jq -e 'type == "array" and (all(.[]; type == "array") or all(.[]; type == "object"))' <<<"$comments" >/dev/null 2>&1; then - unknown "provider review-summary response was malformed" -else - summary_rows=$(jq -r ' - (if all(.[]; type == "array") then flatten else . end)[] | - select(.user.login == "chatgpt-codex-connector[bot]" and .user.type == "Bot") | - select((.body // "") | contains("codex-pull-request-review-summary")) | - .body - ' <<<"$comments" | grep -E '^\| (📝|🔍)' | tail -1) - if [ -z "$summary_rows" ]; then - bad "no provider review summary row" - elif ! grep -Fq "\`$short\`" <<<"$summary_rows"; then - bad "latest provider summary names a different head than $short" - elif ! grep -Fq 'Completed' <<<"$summary_rows"; then - bad "provider review run for $short is not completed" - else - summary_good=1 - say "ok" "provider review summary is completed on $short" - fi -fi - -if [ "$provider_review" = "matched" ] && [ "$summary_good" -eq 1 ]; then - say "ok" "provider review records the full current head $short" -elif [ "$provider_review" != "matched" ] && [ "$summary_good" -eq 1 ]; then - if [ -n "$INDEPENDENT_REVIEW_SHA" ] && [ "$INDEPENDENT_REVIEW_SHA" = "$head" ]; then - say "ok" "manual independent review attestation names full current head $short" - elif [ -n "$INDEPENDENT_REVIEW_SHA" ]; then - bad "manual independent review attestation does not match the current head" - else - unknown "provider summary exposes only an abbreviated head; pass --independent-review-sha with an exact manual review attestation" - fi -fi - -# Paginate the final thread set; partial, changed, duplicate, or unresolved -# results remain indeterminate/blocking rather than inheriting an earlier read. -cursor="" -open_threads=0 -total_threads=-1 -fetched_threads=0 -thread_ids="$tmpdir/final-review-thread-ids" -: >"$thread_ids" -thread_ok=1 -while :; do - : >"$errfile" - page_status=0 - if [ -n "$cursor" ]; then - page=$(gh api graphql -f owner="$OWNER" -f name="$NAME" -F pr="$PR" -f cursor="$cursor" -f query=' - query($owner:String!,$name:String!,$pr:Int!,$cursor:String){ - repository(owner:$owner,name:$name){ - pullRequest(number:$pr){ reviewThreads(first:100,after:$cursor){ - totalCount pageInfo{hasNextPage endCursor} nodes{id isResolved} - }} - } - }' 2>"$errfile") || page_status=$? - else - # GraphQL treats an omitted nullable variable as null. Sending an empty - # string is not the first page and is rejected by the connection API. - page=$(gh api graphql -f owner="$OWNER" -f name="$NAME" -F pr="$PR" -f query=' - query($owner:String!,$name:String!,$pr:Int!,$cursor:String){ - repository(owner:$owner,name:$name){ - pullRequest(number:$pr){ reviewThreads(first:100,after:$cursor){ - totalCount pageInfo{hasNextPage endCursor} nodes{id isResolved} - }} - } - }' 2>"$errfile") || page_status=$? - fi - if [ "$page_status" -ne 0 ] || ! jq -e '.data.repository.pullRequest.reviewThreads | type == "object" and (.totalCount | type == "number" and floor == . and . >= 0) and (.pageInfo.hasNextPage | type == "boolean") and (.nodes | type == "array" and all(.[]; (.id | type == "string" and length > 0) and (.isResolved | type == "boolean")))' <<<"$page" >/dev/null 2>&1; then - unknown "could not read review threads for $REPO#$PR" - thread_ok=0 - break - fi - page_total=$(jq -r '.data.repository.pullRequest.reviewThreads.totalCount' <<<"$page") - if [ "$total_threads" -eq -1 ]; then - total_threads="$page_total" - elif [ "$page_total" -ne "$total_threads" ]; then - unknown "review-thread totalCount changed during pagination" - thread_ok=0 - break - fi - page_nodes=$(jq '.data.repository.pullRequest.reviewThreads.nodes | length' <<<"$page") - jq -r '.data.repository.pullRequest.reviewThreads.nodes[].id' <<<"$page" >>"$thread_ids" - fetched_threads=$((fetched_threads + page_nodes)) - if [ "$fetched_threads" -gt "$total_threads" ]; then - unknown "review-thread pagination exceeded totalCount" - thread_ok=0 - break - fi - page_open=$(jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length' <<<"$page") - open_threads=$((open_threads + page_open)) - has_next=$(jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage' <<<"$page") - [ "$has_next" = "true" ] || break - if [ "$page_nodes" -eq 0 ]; then - unknown "review-thread pagination returned no nodes while claiming another page" - thread_ok=0 - break - fi - next_cursor=$(jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor // empty' <<<"$page") - if [ -z "$next_cursor" ] || [ "$next_cursor" = "$cursor" ]; then - unknown "review-thread pagination returned no advancing cursor" - thread_ok=0 - break - fi - cursor="$next_cursor" -done -if [ "$thread_ok" -eq 1 ]; then - unique_thread_count=$(sort -u "$thread_ids" | wc -l | tr -d ' ') - if [ "$unique_thread_count" -ne "$fetched_threads" ]; then - unknown "review-thread pagination repeated thread IDs" - elif [ "$fetched_threads" -ne "$total_threads" ]; then - unknown "review-thread pagination returned $fetched_threads of $total_threads nodes" - elif [ "$open_threads" -eq 0 ]; then - say "ok" "0 of $total_threads review threads unresolved" - else - bad "$open_threads of $total_threads review threads unresolved" - fi -fi -validate_security_reviewer - -# Re-read all moving PR and base identities immediately before emitting a merge command. -: >"$errfile" -final_meta_status=0 -final_meta=$(gh pr view "$PR" --repo "$REPO" \ - --json headRefOid,baseRefOid,baseRefName,mergeable,mergeStateStatus,isDraft,state,title,body,changedFiles 2>"$errfile") || final_meta_status=$? -if [ "$final_meta_status" -ne 0 ] || ! jq -e 'type == "object" and (.headRefOid | type == "string" and test("^[0-9a-fA-F]{40}$")) and (.baseRefOid | type == "string" and test("^[0-9a-fA-F]{40}$")) and (.baseRefName | type == "string") and (.mergeable | type == "string") and (.mergeStateStatus | type == "string") and (.isDraft | type == "boolean") and (.state | type == "string") and (.title | type == "string") and (.changedFiles | type == "number" and floor == . and . >= 0)' <<<"$final_meta" >/dev/null 2>&1; then - unknown "could not revalidate PR head and base before merge" -else - final_head=$(jq -r '.headRefOid' <<<"$final_meta") - final_base_ref_oid=$(jq -r '.baseRefOid' <<<"$final_meta") - final_base=$(jq -r '.baseRefName' <<<"$final_meta") - final_mergeable=$(jq -r '.mergeable' <<<"$final_meta") - final_state=$(jq -r '.mergeStateStatus' <<<"$final_meta") - final_draft=$(jq -r '.isDraft' <<<"$final_meta") - final_pstate=$(jq -r '.state' <<<"$final_meta") - final_title=$(jq -r '.title' <<<"$final_meta") - final_changed_files=$(jq -r '.changedFiles' <<<"$final_meta") - [ "$final_head" = "$head" ] || bad "PR head moved during preflight" - [ "$final_base_ref_oid" = "$base_ref_oid" ] || bad "PR base OID moved during preflight" - [ "$final_base" = "$base" ] || bad "PR base moved during preflight" - [ "$final_mergeable" = "MERGEABLE" ] || bad "PR mergeability changed to $final_mergeable during preflight" - [ "$final_draft" = "false" ] || bad "PR became draft during preflight" - [ "$final_pstate" = "OPEN" ] || bad "PR state changed to $final_pstate during preflight" - [ "$final_changed_files" = "$changed_files_expected" ] || bad "PR changed-file count moved during preflight" - case "$final_state" in - CLEAN|HAS_HOOKS) : ;; - BEHIND|DIRTY|UNKNOWN|BLOCKED|UNSTABLE|DRAFT) bad "PR merge state changed to $final_state during preflight" ;; - *) unknown "PR merge state changed to unrecognised value '$final_state' during preflight" ;; - esac - raw_final_body=$(jq -r '.body // ""' <<<"$final_meta") - final_body="$raw_final_body" - final_visible_status=0 - final_body=$(python3 -c 'import re, sys -text = sys.stdin.read() -print(re.sub(r"|\Z)", "", text, flags=re.S), end="")' <<<"$final_body") || final_visible_status=$? - [ "$final_visible_status" -eq 0 ] || unknown "could not extract final visible PR description content" - [ "$final_title" = "$title" ] || bad "PR title changed during preflight" - if [ "$checklist_status" -eq 0 ] && ! checklist_link_ok "$final_body" "$review_checklist"; then - bad "PR description changed and no longer carries a completed same-repository line-specific checklist link" - fi - if [ "$raw_final_body" != "$raw_prbody" ]; then - bad "PR description changed during preflight; re-run metadata privacy scan" - if ! body_section_has_content "$final_body" 'functional summary|outcome and reason'; then - bad "PR description changed and no longer carries a non-empty functional summary" - fi - if ! body_section_has_content "$final_body" 'test or reproduction command|commands and results|validation and evidence'; then - bad "PR description changed and no longer carries test or reproduction evidence" - fi - if ! body_has_validation_command "$final_body"; then - bad "PR description changed and no longer carries an actual test or reproduction command" - fi - fi + any(.[]; .commit_id == $head) + ' <<<"$head_reviews") fi -if [ -n "$base_tip" ]; then +comment_names_head=false +if [ "$review_names_head" != "true" ]; then : >"$errfile" - final_base_tip_status=0 - final_base_tip=$(gh api "repos/$REPO/branches/$base" --jq '.commit.sha' 2>"$errfile") || final_base_tip_status=$? - if [ "$final_base_tip_status" -ne 0 ] || ! [[ "$final_base_tip" =~ ^[0-9a-fA-F]{40}$ ]]; then - unknown "could not revalidate base tip before merge" - elif [ "$final_base_tip" != "$base_tip" ]; then - bad "base tip moved during preflight" - fi -fi - -# Check runs and legacy statuses can change while the late review/thread phase -# is in progress without moving the PR head. Re-read both complete, head-bound -# feeds immediately before the final identity fence; a rerun that is pending or -# failed must not inherit the earlier successful snapshot. -: >"$errfile" -final_check_runs_status=0 -final_check_runs=$(gh api --paginate --slurp "repos/$REPO/commits/$head/check-runs?per_page=100" 2>"$errfile") || final_check_runs_status=$? -if [ "$final_check_runs_status" -ne 0 ] || ! jq -e --arg head "$head" ' - type == "array" and length > 0 and - all(.[]; type == "object" and - (.total_count | type == "number" and floor == . and . >= 0) and - (.check_runs | type == "array" and all(.[]; - type == "object" and - (.id | type == "number" and floor == . and . >= 0) and - (.name | type == "string" and length > 0) and - (.status == "completed") and - (.conclusion | type == "string" and (. == "success" or . == "skipped" or . == "neutral" or . == "failure" or . == "cancelled" or . == "timed_out" or . == "action_required" or . == "stale")) and - (.head_sha | type == "string" and test("^[0-9a-fA-F]{40}$") and . == $head) - ))) and - ((map(.total_count) | unique | length) == 1) and - ((map(.check_runs | length) | add) == .[0].total_count) and - ((map(.check_runs) | add | map(.id) | unique | length) == .[0].total_count) -' <<<"$final_check_runs" >/dev/null 2>&1; then - unknown "could not revalidate final head-bound check-run evidence" -else - printf '%s' "$final_check_runs" >"$tmpdir/final-check-runs.json" - final_check_run_skips="$tmpdir/final-check-run-skips.b64" - final_skipped_ci_policy_ok=true - if ! jq -r '.[] | .check_runs[] | select(.conclusion == "skipped") | .name | @base64' <<<"$final_check_runs" >"$final_check_run_skips"; then - final_skipped_ci_policy_ok=false - unknown "could not read skipped check names from final refreshed check-run evidence" - else - validate_skipped_ci_jobs "final refreshed check-run evidence" "$final_check_run_skips" final_skipped_ci_policy_ok - fi - final_failed_runs_status=0 - final_failed_runs=$(jq --rawfile contexts "$tmpdir/required-contexts" ' - ($contexts | split("\n")) as $required | - [.[] | .check_runs[] | . as $run | - select((.conclusion != "success" and .conclusion != "neutral" and .conclusion != "skipped") or - (.conclusion != "success" and ($required | index($run.name)) != null))] | length - ' <(printf '%s' "$final_check_runs")) || final_failed_runs_status=$? - if [ "$final_failed_runs_status" -ne 0 ] || ! [[ "$final_failed_runs" =~ ^[0-9]+$ ]]; then - unknown "could not evaluate final check-run conclusions" - elif [ "$final_failed_runs" -gt 0 ]; then - bad "$final_failed_runs final check run(s) are failed or required-but-not-successful" - elif [ "$final_skipped_ci_policy_ok" != true ]; then - : - else - say "ok" "final check-run pages are successful and bound to head $short" - fi -fi - -: >"$errfile" -final_statuses_status=0 -final_statuses=$(gh api --paginate --slurp "repos/$REPO/commits/$head/status?per_page=100" 2>"$errfile") || final_statuses_status=$? -if [ "$final_statuses_status" -ne 0 ] || ! jq -e --arg head "$head" ' - type == "array" and length > 0 and - all(.[]; type == "object" and - (.sha | type == "string" and test("^[0-9a-fA-F]{40}$") and . == $head) and - (.total_count | type == "number" and floor == . and . >= 0) and - (.state | type == "string" and (. == "success" or . == "pending" or . == "failure" or . == "error")) and - (.statuses | type == "array" and all(.[]; - type == "object" and - (.id | type == "number" and floor == . and . >= 0) and - (.context | type == "string" and length > 0) and - (.state == "success" or .state == "failure" or .state == "error") - )) - ) and - ((map(.total_count) | unique | length) == 1) and - ((map(.statuses | length) | add) == .[0].total_count) and - ((map(.statuses) | add | map(.id) | unique | length) == .[0].total_count) and - ((map(.statuses) | add | map(.context) | unique | length) == .[0].total_count) and - (if .[0].state == "pending" then .[0].total_count == 0 and (map(.statuses | length) | add) == 0 - elif .[0].state == "failure" or .[0].state == "error" then .[0].state as $aggregate | all(.[]; .state == $aggregate) - else all(.[]; .state == "success") end) -' <<<"$final_statuses" >/dev/null 2>&1; then - unknown "could not revalidate final head-bound commit-status evidence" -elif jq -e 'any(.[]; (.state == "failure" or .state == "error") or any(.statuses[]; .state == "failure" or .state == "error"))' <<<"$final_statuses" >/dev/null; then - bad "final combined commit-status evidence reports a failure" -else - printf '%s' "$final_statuses" >"$tmpdir/final-statuses.json" - say "ok" "final commit-status pages are bound to head $short" -fi - -# Complete pages can still omit a context that the initial rollup reported. -# Reconstruct the final union, including status-only contexts, and require every -# originally protected context to remain present with only successful results. -if [ -f "$tmpdir/final-check-runs.json" ] && [ -f "$tmpdir/final-statuses.json" ]; then - final_required_status=0 - final_required_bad=$(jq -n --rawfile contexts "$tmpdir/required-contexts" \ - --slurpfile runs "$tmpdir/final-check-runs.json" --slurpfile statuses "$tmpdir/final-statuses.json" ' - ([$runs[0][] | .check_runs[] | {name, state: .conclusion}] + - [$statuses[0][] | .statuses[] | {name: .context, state}]) as $reported | - [$contexts | split("\n")[] | select(length > 0) | . as $context | - [$reported[] | select(.name == $context)] | - select(length == 0 or any(.[]; .state != "success"))] | length - ') || final_required_status=$? - if [ "$final_required_status" -ne 0 ] || ! [[ "$final_required_bad" =~ ^[0-9]+$ ]]; then - unknown "could not evaluate final required check contexts" - elif [ "$final_required_bad" -gt 0 ]; then - bad "$final_required_bad required check context(s) missing or not successful after final CI evidence" - else - say "ok" "all required check contexts remain successful in final CI evidence" - fi -fi - -# The last check/status snapshot is useful only while it still names this PR -# and base and the metadata examined above remains unchanged. -: >"$errfile" -late_meta_status=0 -late_meta=$(gh pr view "$PR" --repo "$REPO" \ - --json headRefOid,baseRefOid,baseRefName,mergeable,mergeStateStatus,isDraft,state,title,body,changedFiles 2>"$errfile") || late_meta_status=$? -if [ "$late_meta_status" -ne 0 ] || ! jq -e ' - type == "object" and - (.headRefOid | type == "string" and test("^[0-9a-fA-F]{40}$")) and - (.baseRefOid | type == "string" and test("^[0-9a-fA-F]{40}$")) and - (.baseRefName | type == "string" and length > 0) and - (.mergeable | type == "string") and - (.mergeStateStatus | . == "CLEAN" or . == "HAS_HOOKS" or . == "BEHIND" or . == "DIRTY" or . == "UNKNOWN" or . == "BLOCKED" or . == "UNSTABLE" or . == "DRAFT") and - (.isDraft | type == "boolean") and (.state | type == "string") and - (.title | type == "string") and (.body | type == "string") and - (.changedFiles | type == "number" and floor == . and . >= 0) -' <<<"$late_meta" >/dev/null 2>&1; then - unknown "could not revalidate PR identity after final CI evidence" + head_comments_status=0 + head_comments=$(gh api --paginate --slurp "repos/$REPO/issues/$PR/comments" 2>"$errfile") || head_comments_status=$? + if [ "$head_comments_status" -eq 0 ] && jq -e ' + type == "array" and (all(.[]; type == "array") or all(.[]; type == "object")) + ' <<<"$head_comments" >/dev/null 2>&1; then + short_marker="\`${short}\`" + comment_names_head=$(jq -r --arg head "$head" --arg marker "$short_marker" ' + (if all(.[]; type == "array") then flatten else . end) | + any(.[]; ((.body // "") | contains($head)) or ((.body // "") | contains($marker))) + ' <<<"$head_comments") + fi +fi +if [ "$review_names_head" = "true" ] || [ "$comment_names_head" = "true" ]; then + say "ok" "review evidence names the current head $short" else - [ "$(jq -r '.headRefOid' <<<"$late_meta")" = "$head" ] || bad "PR head moved after final CI evidence" - [ "$(jq -r '.baseRefOid' <<<"$late_meta")" = "$base_ref_oid" ] || bad "PR base OID moved after final CI evidence" - [ "$(jq -r '.baseRefName' <<<"$late_meta")" = "$base" ] || bad "PR base moved after final CI evidence" - printf '%s' "$meta" >"$tmpdir/initial-pr-metadata.json" - late_metadata_match_status=0 - jq -e --slurpfile initial "$tmpdir/initial-pr-metadata.json" ' - def mutable_fields: {title, body, isDraft, state, mergeable, mergeStateStatus, changedFiles}; - mutable_fields == ($initial[0] | mutable_fields) - ' <<<"$late_meta" >/dev/null 2>&1 || late_metadata_match_status=$? - case "$late_metadata_match_status" in - 0) : ;; - 1) bad "PR metadata changed after final CI evidence; re-run preflight" ;; - *) unknown "could not compare PR metadata after final CI evidence" ;; - esac + bad "no review evidence (review or comment) names current head $short — see #317" fi - -# The branch endpoint is independent of the PR metadata endpoint. Re-read it -# after the final check/status snapshot and identity fence so a base advance in -# that last interval cannot produce a merge command for an unreviewed tree. -if [ -n "$base_tip" ]; then - : >"$errfile" - final_fenced_base_tip_status=0 - final_fenced_base_tip=$(gh api "repos/$REPO/branches/$base" --jq '.commit.sha' 2>"$errfile") || final_fenced_base_tip_status=$? - if [ "$final_fenced_base_tip_status" -ne 0 ] || ! [[ "$final_fenced_base_tip" =~ ^[0-9a-fA-F]{40}$ ]]; then - unknown "could not revalidate base tip after final CI evidence" - elif [ "$final_fenced_base_tip" != "$base_tip" ]; then - bad "base tip moved after final CI evidence" - fi -fi - echo if [ "$fail" -ne 0 ]; then echo "MUST NOT MERGE" @@ -1581,7 +545,8 @@ if [ "$uncertain" -ne 0 ]; then exit 2 fi -echo "MAY MERGE — bind the merge to the reviewed head and validated base:" +echo "MAY MERGE — compatibility-surface, privacy, and head-SHA review-evidence checks passed for $short." +echo "Branch protection on $base enforces the remaining required status checks separately." printf ' [ "$(gh pr view %s --repo %s --json baseRefName -q .baseRefName)" = "%s" ] \\\n && gh pr merge %s --repo %s --squash --match-head-commit %s\n' \ "$PR" "$REPO" "$base" "$PR" "$REPO" "$head" exit 0 diff --git a/scripts/merge-gate.test.py b/scripts/merge-gate.test.py index 0f7250e36..5b529a539 100644 --- a/scripts/merge-gate.test.py +++ b/scripts/merge-gate.test.py @@ -1,32 +1,44 @@ #!/usr/bin/env python3 -"""Focused offline controls for scripts/merge-gate.sh. +"""Focused offline controls for the shrunk scripts/merge-gate.sh. -The fake gh command models server responses, including paginated REST and -GraphQL pages. No network or merge operation is used. +The gate was cut from 3977 lines of merge-gate.sh/.test.py/_fake_gh.py/ +_privacy.py/_diff.py down to only what runs as a real required CI check: +compatibility-surface validation, the privacy/PII scan, and a minimal +review-evidence-names-current-head check. This suite exercises only that +kept surface, plus a dedicated regression test for each of the 7 defects +fixed in the kept code (#346, #358, #360, #365, #366, #373, #377). + +The fake `gh` (merge_gate_test_gh.py) models server responses; no network or +merge operation is used. """ from __future__ import annotations -import json import os import shutil +import stat import subprocess import tempfile import unittest -import importlib.util from pathlib import Path ROOT = Path(__file__).resolve().parent.parent SCRIPT = ROOT / "scripts" / "merge-gate.sh" +FAKE_GH = ROOT / "scripts" / "merge_gate_test_gh.py" HEAD = "0123456789abcdef0123456789abcdef01234567" -FAKE_GH = ROOT / "scripts" / "merge_gate_fake_gh.py" -PRIVACY = ROOT / "scripts" / "merge_gate_privacy.py" - - -def load_privacy_module(): - spec = importlib.util.spec_from_file_location("merge_gate_privacy", PRIVACY) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module +SHORT = HEAD[:7] + +MKTEMP_WRAPPER = """#!/usr/bin/env bash +# Only scan-input-write-failure pre-occupies the privacy-scan-input path (as +# a directory) so the write inside merge-gate.sh's redirect group fails. +real=$(command -v -p mktemp) +if [ "${GATE_SCENARIO:-}" = "scan-input-write-failure" ] && [ "$1" = "-d" ]; then + dir=$("$real" -d) + mkdir "$dir/privacy-scan-input" + printf '%s\\n' "$dir" +else + exec "$real" "$@" +fi +""" class MergeGateControls(unittest.TestCase): @@ -37,20 +49,10 @@ def setUpClass(cls): gh = cls.bin / "gh" shutil.copyfile(FAKE_GH, gh) gh.chmod(0o755) - jq = cls.bin / "jq" - jq.write_text("""#!/usr/bin/env python3 -import os, sys -if os.environ.get("GATE_SCENARIO") == "context-report-jq-failure" and "--rawfile" in sys.argv and "contexts" in sys.argv: - raise SystemExit(1) -if os.environ.get("GATE_SCENARIO") == "final-required-jq-failure" and "--slurpfile" in sys.argv and "runs" in sys.argv: - raise SystemExit(1) -if os.environ.get("GATE_SCENARIO") == "late-meta-jq-failure" and "--slurpfile" in sys.argv and "initial" in sys.argv: - raise SystemExit(2) -os.execv(os.environ["GATE_REAL_JQ"], [os.environ["GATE_REAL_JQ"], *sys.argv[1:]]) -""") - jq.chmod(0o755) - cls.real_jq = shutil.which("jq") - if not cls.real_jq: + mktemp = cls.bin / "mktemp" + mktemp.write_text(MKTEMP_WRAPPER) + mktemp.chmod(mktemp.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH) + if not shutil.which("jq"): raise RuntimeError("jq is required for merge-gate controls") @classmethod @@ -61,19 +63,6 @@ def run_gate(self, scenario="pass", extra_args=()): env = os.environ.copy() env["PATH"] = f"{self.bin}:{env['PATH']}" env["GATE_SCENARIO"] = scenario - env["GATE_REAL_JQ"] = self.real_jq - counter = self.bin / f"{scenario}-counter-{os.getpid()}" - counter.write_text("0") - env["GATE_COUNTER"] = str(counter) - check_counter = self.bin / f"{scenario}-checks-{os.getpid()}" - check_counter.write_text("0") - env["GATE_CHECK_COUNTER"] = str(check_counter) - status_counter = self.bin / f"{scenario}-statuses-{os.getpid()}" - status_counter.write_text("0") - env["GATE_STATUS_COUNTER"] = str(status_counter) - base_tip_counter = self.bin / f"{scenario}-base-tip-{os.getpid()}" - base_tip_counter.write_text("0") - env["GATE_BASE_TIP_COUNTER"] = str(base_tip_counter) return subprocess.run( [str(SCRIPT), "321", "--repo", "lamemustafa/bridge", *extra_args], cwd=ROOT, @@ -84,848 +73,139 @@ def run_gate(self, scenario="pass", extra_args=()): check=False, ) - def assert_blocked(self, scenario, phrase): - result = self.run_gate(scenario) + def assert_pass(self, scenario, phrase=None, extra_args=()): + result = self.run_gate(scenario, extra_args) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("MAY MERGE", result.stdout) + if phrase: + self.assertIn(phrase, result.stdout) + return result + + def assert_blocked(self, scenario, phrase, extra_args=()): + result = self.run_gate(scenario, extra_args) self.assertEqual(result.returncode, 1, result.stdout + result.stderr) self.assertIn(phrase, result.stdout) self.assertNotIn("MAY MERGE", result.stdout) + return result - def assert_indeterminate(self, scenario, phrase): - result = self.run_gate(scenario) + def assert_indeterminate(self, scenario, phrase, extra_args=()): + result = self.run_gate(scenario, extra_args) self.assertEqual(result.returncode, 2, result.stdout + result.stderr) self.assertIn(phrase, result.stdout) self.assertNotIn("MAY MERGE", result.stdout) + return result - def test_server_full_sha_and_paginated_pages_can_pass(self): - result = self.run_gate() - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - self.assertIn("provider review records the full current head", result.stdout) - self.assertIn("MAY MERGE", result.stdout) - self.assertIn("--match-head-commit 0123456789abcdef0123456789abcdef01234567", result.stdout) + # -- Baseline ----------------------------------------------------------- - def test_allowlisted_docs_only_skipped_checks_do_not_block(self): - result = self.run_gate("skipped-docs-matrix") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - self.assertIn("allowlisted skipped native CI matrix job", result.stdout) + def test_pass_scenario_may_merge(self): + self.assert_pass("pass", "review evidence names the current head") - def test_skipped_ci_jobs_require_an_explicit_allowlist_and_matching_path_condition(self): - self.assert_blocked("skipped-unallowlisted", "skipped CI job outside the explicit workflow allowlist") - self.assert_blocked("skipped-native-scope", "skipped native CI matrix job despite native-scope changed files") - self.assert_blocked("skipped-bundle-scope", "skipped bundle CI matrix job despite bundle-scope changed files") + # -- Compatibility-surface validation (KEEP #1) -------------------------- - def test_required_skipped_check_blocks(self): - self.assert_blocked("required-skip", "required check 'Required checks' is not passing") + def test_surface_fetch_failure_is_indeterminate(self): + self.assert_indeterminate("surface-fetch-fail", "could not read and validate compatibility surface at " + SHORT) - def test_incomplete_check_run_is_indeterminate(self): - self.assert_indeterminate("check-run-incomplete", "head-bound check-run evidence") + def test_surface_malformed_is_indeterminate(self): + self.assert_indeterminate("surface-malformed", "could not read and validate compatibility surface at " + SHORT) - def test_empty_pending_legacy_status_does_not_block(self): - result = self.run_gate("status-empty-pending") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_base_pinned_path_removed_from_head_is_indeterminate(self): + self.assert_indeterminate("surface-unpins", "base-pinned path(s) are absent from the head surface") - def test_missing_required_context_blocks(self): - self.assert_blocked("missing-required", "required check 'Rust format' was not reported") + def test_touched_pinned_path_without_reseal_blocks(self): + self.assert_blocked("surface-reseal-missing", "changed pinned paths omit the compatibility surface reseal") - def test_unexpected_required_context_blocks(self): - self.assert_blocked("required-context-unexpected", "undocumented required check context") + def test_touched_pinned_path_with_reseal_passes(self): + self.assert_pass("surface-reseal-present", "changed pinned paths include the compatibility surface") - def test_cancelled_check_blocks(self): - self.assert_blocked("cancel-check", "failing, cancelled, or pending") + # -- Privacy / PII scan (KEEP #2) ---------------------------------------- - def test_surface_transport_failure_is_indeterminate(self): - self.assert_indeterminate("surface-fail", "could not read and validate compatibility surface") + def test_email_in_title_blocks(self): + self.assert_blocked("privacy-email-blocker", "customer email shape") - def test_silent_checks_response_is_indeterminate(self): - self.assert_indeterminate("checks-silent", "checks query returned no JSON") + def test_credential_literal_blocks(self): + self.assert_blocked("privacy-credential-blocker", "literal credential, bearer, or API token value") - def test_summary_only_short_sha_is_indeterminate(self): - self.assert_indeterminate("short-review", "independent-review-sha") - - def test_manual_full_sha_attestation_accepts_zero_finding_summary(self): - result = self.run_gate("manual-summary", ("--independent-review-sha", HEAD)) - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - self.assertIn("manual independent review attestation", result.stdout) - - def test_provider_review_requires_completed_summary(self): - self.assert_blocked("summary-failed", "review run for 0123456 is not completed") - - def test_blocked_merge_state_cannot_pass(self): - self.assert_blocked("blocked-state", "merge state BLOCKED") - - def test_unrecognised_final_merge_state_is_indeterminate(self): - self.assert_indeterminate("final-unrecognized", "unrecognised value") - - def test_short_thread_page_is_indeterminate(self): - self.assert_indeterminate("threads-short", "returned 1 of 101 nodes") - - def test_unresolved_second_thread_page_blocks(self): - self.assert_blocked("threads-unresolved-second", "1 of 101 review threads unresolved") - - def test_malformed_thread_pagination_is_indeterminate(self): - self.assert_indeterminate("threads-malformed-pagination", "no advancing cursor") - - def test_first_thread_page_omits_the_empty_cursor(self): - result = self.run_gate("threads-first-empty-cursor-rejected") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_unknown_uuid_is_indeterminate(self): + self.assert_indeterminate("privacy-uuid-indeterminate", "without exact current-head fixture provenance") - def test_thread_total_drift_is_indeterminate(self): - self.assert_indeterminate("threads-total-drift", "totalCount changed") - - def test_head_change_is_blocked(self): - self.assert_blocked("head-moves", "PR head moved during preflight") - - def test_formatted_phone_is_scanned(self): - self.assert_blocked("formatted-phone", "privacy scan found") - - def test_hidden_metadata_identifier_is_still_scanned(self): - self.assert_blocked("hidden-comment-identifier", "privacy scan found") - - def test_unterminated_comment_cannot_supply_validation(self): - self.assert_blocked("unterminated-html-comment", "actual test or reproduction command") - - def test_multiline_comment_cannot_supply_functional_summary(self): - self.assert_blocked("policy-multiline-comment", "non-empty functional summary") - - def test_raw_html_comment_drift_blocks_revalidation(self): - self.assert_blocked("body-comment-drift", "description changed during preflight") - - def test_last_identity_fence_rejects_each_late_mutable_metadata_change(self): - for field in ("title", "title-newline", "body-comment", "body-newline", "draft", "state", "mergeable", "merge-state", "changed-files"): - with self.subTest(field=field): - result = self.run_gate("late-meta-" + field) - self.assertEqual(result.returncode, 1, result.stdout + result.stderr) - self.assertIn("PR metadata changed after final CI evidence", result.stdout) - self.assertNotIn("changed during preflight", result.stdout) - self.assertNotIn("MAY MERGE", result.stdout) - - def test_last_identity_fence_preserves_head_and_base_move_rejection(self): - for field, phrase in (("head", "PR head moved"), ("base-oid", "PR base OID moved"), ("base-name", "PR base moved")): - with self.subTest(field=field): - self.assert_blocked("late-meta-" + field, phrase + " after final CI evidence") - - def test_last_identity_fence_rejects_unreadable_or_malformed_metadata(self): - for field in ("error", "missing-body", "body-wrong-type", "unknown-state"): - with self.subTest(field=field): - self.assert_indeterminate("late-meta-" + field, "could not revalidate PR identity after final CI evidence") - self.assert_indeterminate("late-meta-jq-failure", "could not compare PR metadata after final CI evidence") - - def test_final_required_context_union_rejects_disappearing_contexts(self): - for scenario, count in (("final-contexts-empty", 5), ("final-check-context-disappears", 1), ("final-status-context-disappears", 1)): - with self.subTest(scenario=scenario): - result = self.run_gate(scenario) - self.assertEqual(result.returncode, 1, result.stdout + result.stderr) - self.assertIn("all 5 required check contexts passed", result.stdout) - self.assertIn(f"{count} required check context(s) missing or not successful after final CI evidence", result.stdout) - self.assertNotIn("MAY MERGE", result.stdout) - - def test_final_required_context_union_accepts_paginated_status_only_contexts(self): - result = self.run_gate("final-required-statuses-pass") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - self.assertIn("all required check contexts remain successful in final CI evidence", result.stdout) - - def test_final_required_context_union_rejects_non_success_and_computation_errors(self): - for scenario in ("final-required-check-skipped", "final-required-check-neutral"): - with self.subTest(scenario=scenario): - self.assert_blocked(scenario, "1 required check context(s) missing or not successful after final CI evidence") - self.assert_blocked("final-required-status-fails", "final combined commit-status evidence reports a failure") - self.assert_indeterminate("final-required-check-pending", "could not revalidate final head-bound check-run evidence") - self.assert_indeterminate("final-required-jq-failure", "could not evaluate final required check contexts") - - def test_repeated_digit_phone_is_scanned(self): - self.assert_blocked("repeated-phone", "privacy scan found") - - def test_grouped_formatted_phone_is_scanned(self): - self.assert_blocked("formatted-phone-grouped", "privacy scan found") - - def test_separated_indian_mobile_styles_are_scanned(self): - for scenario in ("phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "unicode-phone", "unicode-phone-tab"): - with self.subTest(scenario=scenario): - self.assert_blocked(scenario, "privacy scan found") - result = self.run_gate("unicode-phone-two-lines") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_grouped_indian_landline_is_scanned_without_global_separator_joining(self): - self.assert_blocked("landline-grouped", "privacy scan found") - - def test_standard_indian_landline_is_scanned_without_global_separator_joining(self): - for scenario in ("landline-standard-hyphen", "landline-standard-space", "landline-standard-four-digit"): - with self.subTest(scenario=scenario): - self.assert_blocked(scenario, "privacy scan found") - result = self.run_gate("landline-standard-underscore") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_pem_certificate_envelope_is_blocked_without_echoing_payload(self): - for scenario, begin, body in ( - ("pem-certificate-envelope", "-" * 5 + "BEGIN CERTIFICATE" + "-" * 5, "MII" + "A" * 48), - ("trusted-pem-certificate-envelope", "-" * 5 + "BEGIN TRUSTED CERTIFICATE" + "-" * 5, "MII" + "B" * 48), - ): - with self.subTest(scenario=scenario): - result = self.run_gate(scenario) - self.assertEqual(result.returncode, 1, result.stdout + result.stderr) - self.assertIn("PEM certificate envelope", result.stdout) - self.assertNotIn(begin, result.stdout) - self.assertNotIn(body, result.stdout) - - def test_header_shaped_added_payload_is_still_scanned(self): - self.assert_blocked("hunk-header-phone", "privacy scan found") - - def test_header_shaped_added_literals_count_as_payload(self): - result = self.run_gate("hunk-header-literals") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_binary_shaped_added_literal_is_not_binary_metadata(self): - result = self.run_gate("hunk-binary-literal") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_unrelated_separator_groups_are_not_fused_into_a_phone(self): - result = self.run_gate("separated-dates") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_new_year_date_ranges_are_not_fused_into_a_phone(self): - result = self.run_gate("separated-dates-new-year") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_year_month_date_ranges_are_not_fused_into_a_phone(self): - result = self.run_gate("separated-dates-year-month") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_adr_identifier_with_underscores_remains_clean(self): - result = self.run_gate("adr-identifier") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_destination_path_is_scanned(self): - self.assert_blocked("path-id", "privacy scan found") - - def test_binary_deletion_is_not_treated_as_added_content(self): - result = self.run_gate("binary-delete") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_binary_addition_without_attestation_blocks(self): + self.assert_blocked("binary-missing-attestation", "require matching --binary-review-sha and --independent-review-sha") - def test_binary_additions_require_two_current_head_manual_attestations(self): - self.assert_blocked("binary-review", "require matching --binary-review-sha and --independent-review-sha") - binary_only = self.run_gate("binary-review", ("--binary-review-sha", HEAD)) - self.assertEqual(binary_only.returncode, 1, binary_only.stdout + binary_only.stderr) - self.assertIn("require matching --binary-review-sha and --independent-review-sha", binary_only.stdout) - malformed = self.run_gate("binary-review", ("--binary-review-sha", "not-a-sha", "--independent-review-sha", HEAD)) - self.assertEqual(malformed.returncode, 1, malformed.stdout + malformed.stderr) - self.assertIn("binary review attestation must be a full 40-hex", malformed.stdout) - stale = self.run_gate("binary-review", ("--binary-review-sha", "f" * 40, "--independent-review-sha", HEAD)) - self.assertEqual(stale.returncode, 1, stale.stdout + stale.stderr) - self.assertIn("binary review attestation names a different commit", stale.stdout) - current = self.run_gate("binary-review", ("--binary-review-sha", HEAD, "--independent-review-sha", HEAD)) - self.assertEqual(current.returncode, 0, current.stdout + current.stderr) - self.assertIn("explicit current-head binary and independent review attestations", current.stdout) - moved = self.run_gate("binary-review-head-moves", ("--binary-review-sha", HEAD, "--independent-review-sha", HEAD)) - self.assertEqual(moved.returncode, 1, moved.stdout + moved.stderr) - self.assertIn("PR head moved during preflight", moved.stdout) - - def test_binary_attestation_does_not_bypass_privacy_metadata_scan(self): - result = self.run_gate("binary-review-private", ("--binary-review-sha", HEAD, "--independent-review-sha", HEAD)) - self.assertEqual(result.returncode, 1, result.stdout + result.stderr) - self.assertIn("privacy scan found", result.stdout) - - def test_malformed_surface_is_indeterminate(self): - self.assert_indeterminate("surface-malformed", "could not read and validate compatibility surface") - - def test_surface_with_no_v1_manifest_schema_is_indeterminate(self): - self.assert_indeterminate("surface-schema-malformed", "could not read and validate compatibility surface") - - def test_removed_base_pin_requires_human_hold(self): - self.assert_indeterminate("surface-unpins", "base-pinned path(s) are absent") - - def test_compare_lineage_mismatch_is_indeterminate(self): - self.assert_indeterminate("lineage-mismatch", "base/head compare did not prove") - - def test_check_run_wrong_head_is_indeterminate(self): - self.assert_indeterminate("check-run-wrong-head", "head-bound check-run evidence") - - def test_check_run_count_mismatch_is_indeterminate(self): - self.assert_indeterminate("check-run-count-mismatch", "head-bound check-run evidence") - - def test_final_check_and_status_snapshots_do_not_inherit_earlier_success(self): - self.assert_blocked("late-check-run-failure", "final check run(s) are failed") - self.assert_blocked("late-check-run-unallowlisted-skip", "final refreshed check-run evidence reports a skipped CI job outside the explicit workflow allowlist") - self.assert_blocked("late-status-failure", "final combined commit-status evidence reports a failure") - - def test_base_oid_mismatch_is_indeterminate(self): - self.assert_indeterminate("base-oid-mismatch", "PR base OID") - - def test_final_base_tip_recheck_rejects_a_late_move_or_read_failure(self): - self.assert_blocked("final-base-tip-moves", "base tip moved after final CI evidence") - self.assert_indeterminate("final-base-tip-failure", "could not revalidate base tip after final CI evidence") - - def test_malformed_commit_status_is_indeterminate(self): - self.assert_indeterminate("status-malformed", "head-bound commit-status evidence") - - def test_malformed_changed_file_is_indeterminate(self): - self.assert_indeterminate("malformed-files", "could not read the complete changed-file set") - - def test_missing_changed_file_status_is_indeterminate(self): - self.assert_indeterminate("missing-file-status", "could not read the complete changed-file set") - - def test_changed_file_count_mismatch_is_indeterminate(self): - self.assert_indeterminate("files-count-mismatch", "changed-file response has") - - def test_empty_file_array_is_not_one_filename(self): - self.assert_indeterminate("files-empty", "contained no filenames") - - def test_fractional_thread_count_is_indeterminate(self): - self.assert_indeterminate("threads-fractional", "could not read review threads") - - def test_empty_page_cannot_claim_more_threads(self): - self.assert_indeterminate("threads-empty-more", "no nodes while claiming another page") - - def test_template_checklist_permalink_on_continuation_passes(self): - result = self.run_gate("checklist-template-continuation") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - self.assertIn("description links a completed review-checklist item", result.stdout) - - def test_checklist_permalink_must_bind_the_full_current_head(self): - self.assert_blocked("checklist-stale-ref", "same-repository line-specific") - self.assert_blocked("checklist-anchor-suffix", "same-repository line-specific") - - def test_foreign_checklist_link_blocks(self): - self.assert_blocked("checklist-foreign", "same-repository line-specific") - - def test_unlinked_checklist_text_blocks(self): - self.assert_blocked("checklist-unlinked", "same-repository line-specific") - - def test_omitted_nonremoved_diff_file_is_indeterminate(self): - self.assert_indeterminate("diff-omits-file", "privacy diff coverage failed") - - def test_truncated_textual_diff_payload_is_indeterminate(self): - self.assert_indeterminate("diff-truncated-payload", "privacy diff coverage failed") - - def test_metadata_only_zero_line_diff_can_pass(self): - result = self.run_gate("metadata-only") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - self.assertIn("metadata-only diff section", result.stdout) - - def test_metadata_only_examples_are_redacted_and_bounded(self): - result = self.run_gate("metadata-private") - self.assertEqual(result.returncode, 1, result.stdout + result.stderr) - self.assertIn("metadata-only diff section", result.stdout) - self.assertNotIn("privacy diff coverage failed", result.stdout) - self.assertNotIn("tester", result.stdout) - examples = next(line for line in result.stdout.splitlines() if "metadata-only diff section" in line) - self.assertLess(len(examples), 260) - self.assertNotIn("x" * 161, examples) - - def test_metadata_only_diff_requires_rest_zero_totals(self): - self.assert_indeterminate("metadata-incomplete", "metadata-only 'docs/example.md' conflicts") - - def test_pr_selector_must_be_numeric(self): - env = os.environ.copy() - env["PATH"] = f"{self.bin}:{env['PATH']}" - result = subprocess.run( - [str(SCRIPT), "321;echo unsafe", "--repo", "lamemustafa/bridge"], - cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, check=False, - ) - self.assertEqual(result.returncode, 2) - self.assertIn("PR selector must be numeric", result.stderr) - - def test_repo_identity_must_be_safe(self): - env = os.environ.copy() - env["PATH"] = f"{self.bin}:{env['PATH']}" - result = subprocess.run( - [str(SCRIPT), "321", "--repo", "lamemustafa/bridge;echo unsafe"], - cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, check=False, - ) - self.assertEqual(result.returncode, 2) - self.assertIn("--repo must be OWNER/NAME", result.stderr) - - def test_other_repository_policy_is_not_assumed(self): - env = os.environ.copy() - env["PATH"] = f"{self.bin}:{env['PATH']}" - result = subprocess.run( - [str(SCRIPT), "321", "--repo", "example/other"], - cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, check=False, + def test_binary_addition_with_attestation_passes(self): + self.assert_pass( + "binary-with-attestation", + "have explicit current-head binary and independent review attestations", + extra_args=("--binary-review-sha", HEAD, "--independent-review-sha", HEAD), ) - self.assertEqual(result.returncode, 2) - self.assertIn("unsupported repository", result.stderr) - - def test_missing_functional_summary_blocks(self): - self.assert_blocked("missing-functional-summary", "functional summary") - - def test_missing_test_summary_blocks(self): - self.assert_blocked("missing-test-summary", "test or reproduction command") - - def test_final_changed_body_revalidates_test_evidence(self): - self.assert_blocked("body-loses-evidence", "description changed and no longer carries test or reproduction evidence") - - def test_final_changed_body_revalidates_functional_summary(self): - self.assert_blocked("body-loses-functional", "description changed and no longer carries a non-empty functional summary") - - def test_draft_merge_state_blocks(self): - self.assert_blocked("draft-state", "merge state DRAFT") - - def test_documented_required_context_cannot_be_omitted(self): - self.assert_blocked("protection-missing-gitguardian", "branch protection omits 1 documented") - - def test_failing_combined_commit_status_blocks(self): - self.assert_blocked("status-failed-combined", "combined commit-status evidence reports a failure") - - def test_failing_individual_commit_status_blocks(self): - self.assert_blocked("status-failed-context", "combined commit-status evidence reports a failure") - - def test_privacy_uuid_policy_is_exact_context_aware_and_value_redacting(self): - privacy = load_privacy_module() - candidate = "12345678-abcd-4abc-8abc-123456789012" - nil = "00000000-0000-0000-0000-000000000000" - provenance = self.bin / "fixture-uuid-provenance.json" - provenance.write_text(json.dumps([{ - "uuid": candidate, - "head": HEAD, - "source": "fixture", - }])) - - for text in ( - "credential_session_id=" + candidate, - "bearerToken=" + candidate, - "credentialSessionId=" + candidate, - ): - with self.subTest(text=text): - result = privacy.scan(text, HEAD, str(provenance)) - self.assertTrue(result["blockers"]) - self.assertNotIn(candidate, json.dumps(result)) - - for text in ( - "urn:uuid:" + candidate, - "synthetic fixture uuid=" + candidate, - ): - with self.subTest(text=text): - result = privacy.scan(text, HEAD) - self.assertTrue(result["indeterminate"]) - self.assertNotIn(candidate, json.dumps(result)) - - nil_result = privacy.scan("{\"id\": \"" + nil + "\"}", HEAD) - self.assertFalse(nil_result["blockers"] + nil_result["indeterminate"]) - - approved = privacy.scan("fixture_value=" + candidate, HEAD, str(provenance)) - self.assertFalse(approved["blockers"] + approved["indeterminate"]) - reused = privacy.scan("token=" + candidate, HEAD, str(provenance)) - self.assertTrue(reused["blockers"]) - - def test_privacy_uuid_provenance_failure_is_indeterminate_without_echoing_input(self): - privacy = load_privacy_module() - candidate = "12345678-abcd-4abc-8abc-123456789012" - result = privacy.scan("urn:uuid:" + candidate, HEAD, str(self.bin / "missing-provenance.json")) - self.assertTrue(result["indeterminate"]) - self.assertNotIn(candidate, json.dumps(result)) - - def test_privacy_module_blocks_trusted_pem_and_four_digit_landline_without_echoing_values(self): - privacy = load_privacy_module() - trusted_pem = "-----BEGIN TRUSTED CERTIFICATE-----\nMII" + "B" * 48 - landline = "0120-2345678" - for value, expected in ((trusted_pem, "PEM certificate envelope"), (landline, "identifier shape")): - with self.subTest(expected=expected): - result = privacy.scan(value, HEAD) - self.assertTrue(result["blockers"]) - self.assertIn(expected, "\n".join(result["blockers"])) - self.assertNotIn(value, json.dumps(result)) - - def test_privacy_module_classifies_literal_credentials_and_customer_emails_without_echoing_values(self): - privacy = load_privacy_module() - value_prefix = "live" - literals = ( - f"Authorization: Bearer {value_prefix}-token-123", - f"api_key = '{value_prefix}-api-key-123'", - f'{{"api_key": "{value_prefix}-json-api-key-123"}}', - f"access_token: {value_prefix}-access-token-123", - f"refresh_token={value_prefix}-refresh-token-123", - f"client_secret: {value_prefix}-client-secret-123", - f"credential = {value_prefix}-credential-123", - f"session-token: {value_prefix}-session-token-123", - ) - for value in literals: - with self.subTest(value=value): - result = privacy.scan(value, HEAD) - self.assertTrue(result["blockers"]) - self.assertNotIn(value, json.dumps(result)) - for value in ("Authorization: Bearer", "api_key =", "client_secret: '"): - with self.subTest(value=value): - result = privacy.scan(value, HEAD) - self.assertTrue(result["indeterminate"]) - self.assertNotIn(value, json.dumps(result)) - for value in ( - "api_key = ${API_KEY}", - '{"api_key": "${API_KEY}"}', - "access_token: ", - "client_secret: str", - "client_secret: Optional[str]", - "client_secret: MyToken", - "tokenizer reference", - ): - with self.subTest(value=value): - result = privacy.scan(value, HEAD) - self.assertFalse(result["blockers"] + result["indeterminate"]) - for value in ("api_key: productionToken", "api_key: supersecret"): - with self.subTest(value=value): - result = privacy.scan(value, HEAD) - self.assertTrue(result["blockers"]) - self.assertNotIn(value, json.dumps(result)) - - def test_privacy_module_scans_password_passphrase_and_private_key_literals_without_echoing_values(self): - privacy = load_privacy_module() - literals = ( - "password = 'live-password-123'", - '"passphrase": "live-passphrase-123"', - "'private-key': 'live-private-key-123'", - '"private_key": "live-private-key-456"', - ) - for value in literals: - with self.subTest(value=value): - result = privacy.scan(value, HEAD) - self.assertTrue(result["blockers"]) - self.assertNotIn(value, json.dumps(result)) - - for value in ( - '"password": "${PASSWORD}"', - '"passphrase": ""', - '"private-key": "str"', - ): - with self.subTest(value=value): - result = privacy.scan(value, HEAD) - self.assertFalse(result["blockers"] + result["indeterminate"]) - - def test_privacy_module_scans_every_credential_assignment_in_minified_json(self): - privacy = load_privacy_module() - value = '{"api_key":"${API_KEY}","password":"live-password-123"}' - result = privacy.scan(value, HEAD) - self.assertTrue(result["blockers"]) - self.assertNotIn(value, json.dumps(result)) - - def test_privacy_email_lane_scans_content_sources_but_not_validated_commit_identity(self): - privacy = load_privacy_module() - for scenario in ("privacy-email-payload", "privacy-email-destination", "privacy-email-title", "privacy-email-body", "privacy-email-commit"): - with self.subTest(scenario=scenario): - result = self.run_gate(scenario) - self.assertEqual(result.returncode, 1, result.stdout + result.stderr) - self.assertIn("customer email shape", result.stdout) - self.assertNotIn("customer@company.test", result.stdout) - result = self.run_gate("privacy-email-author") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - for value in ("customer@company.test", "customer@example.com.attacker.test"): - with self.subTest(value=value): - result = privacy.scan(value, HEAD) - self.assertTrue(result["blockers"]) - self.assertNotIn(value, json.dumps(result)) - for value in ("customer@example.com", "customer@example.invalid"): - with self.subTest(value=value): - result = privacy.scan(value, HEAD) - self.assertFalse(result["blockers"] + result["indeterminate"]) - - def test_duplicate_thread_ids_are_indeterminate(self): - self.assert_indeterminate("threads-duplicate-id", "pagination repeated thread IDs") - - def test_checklist_anchor_must_reference_an_existing_line(self): - self.assert_blocked("checklist-missing-anchor", "review-checklist link to an existing line") - - def test_template_functional_prompt_does_not_count_as_summary(self): - self.assert_blocked("template-functional-prompt", "non-empty functional summary") - - def test_template_validation_prompts_do_not_count_as_evidence(self): - self.assert_blocked("template-validation-prompt", "test or reproduction command") - - def test_pr_title_identifier_is_scanned(self): - self.assert_blocked("metadata-title-id", "privacy scan found") - - def test_pr_commit_metadata_identifier_is_scanned(self): - self.assert_blocked("metadata-commit-id", "privacy scan found") - - def test_standard_commit_identity_fields_are_validated_and_scanned(self): - self.assert_blocked("metadata-author-id", "privacy scan found") - result = self.run_gate("metadata-unlinked-identities") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_capped_commit_metadata_is_indeterminate(self): - self.assert_indeterminate("metadata-capped", "complete head-bound PR commit metadata") - - def test_truncated_commit_metadata_is_indeterminate(self): - self.assert_indeterminate("metadata-truncated", "complete head-bound PR commit metadata") - - def test_duplicate_commit_metadata_is_indeterminate(self): - self.assert_indeterminate("metadata-duplicate", "complete head-bound PR commit metadata") - - def test_commit_metadata_head_mismatch_is_indeterminate(self): - self.assert_indeterminate("metadata-head-mismatch", "complete head-bound PR commit metadata") - - def test_all_a_pan_is_not_exempted_as_a_placeholder(self): - self.assert_blocked("all-a-pan", "privacy scan found") - for scenario in ("grouped-pan-space", "grouped-pan-hyphen"): - with self.subTest(scenario=scenario): - self.assert_blocked(scenario, "privacy scan found") - result = self.run_gate("grouped-masked-pan-space") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_explicit_masked_pan_remains_a_placeholder(self): - result = self.run_gate("masked-pan") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_quoted_git_destination_path_is_covered(self): - result = self.run_gate("quoted-path") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_crlf_and_ambiguous_unquoted_diffs_preserve_coverage(self): - for scenario in ("crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path"): - with self.subTest(scenario=scenario): - result = self.run_gate(scenario) - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_gitlink_requires_explicit_provenance_license_notice_review(self): - for scenario in ("gitlink", "gitlink-existing"): - with self.subTest(scenario=scenario): - self.assert_indeterminate(scenario, "gitlink change(s) require explicit provenance, license, and NOTICE review") - - def test_control_character_in_destination_path_is_indeterminate(self): - self.assert_indeterminate("control-path", "could not read the complete changed-file set") - - def test_populated_sha_without_command_is_not_validation_evidence(self): - self.assert_blocked("template-validation-sha-only", "test or reproduction command") - - def test_validation_heading_without_command_is_not_evidence(self): - self.assert_blocked("template-validation-empty", "actual test or reproduction command") - - def test_protection_requires_strict_status_checks(self): - self.assert_indeterminate("protection-nonstrict", "required status-check response was malformed") - - def test_duplicate_check_run_ids_are_indeterminate(self): - self.assert_indeterminate("check-run-duplicate-id", "head-bound check-run evidence") - - def test_grouped_12_and_16_digit_identifiers_are_scanned(self): - for scenario in ("grouped-identifier-12", "grouped-identifier-16", "grouped-identifier-mixed"): - with self.subTest(scenario=scenario): - self.assert_blocked(scenario, "privacy scan found") - - def test_workflow_change_requires_rollback_and_migration_notes(self): - self.assert_blocked("workflow-notes-missing", "workflow change lacks non-empty rollback notes") - self.assert_blocked("workflow-sibling-migration", "workflow change lacks non-empty rollback notes") - self.assert_blocked("workflow-placeholders", "workflow change lacks non-empty rollback notes") - self.assert_blocked("workflow-punctuated-placeholders", "workflow change lacks non-empty rollback notes") - - def test_workflow_change_with_required_notes_can_pass(self): - result = self.run_gate("workflow-notes-present") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_workflow_delete_and_rename_out_require_notes(self): - for scenario in ("workflow-delete", "workflow-rename-out"): - with self.subTest(scenario=scenario): - self.assert_blocked(scenario, "workflow change lacks non-empty rollback notes") - - def test_workflow_delete_and_rename_out_with_notes_can_pass(self): - for scenario in ("workflow-delete-notes", "workflow-rename-out-notes"): - with self.subTest(scenario=scenario): - result = self.run_gate(scenario) - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_renamed_file_requires_a_string_previous_filename(self): - for scenario in ("renamed-previous-missing", "renamed-previous-null", "renamed-previous-false"): - with self.subTest(scenario=scenario): - self.assert_indeterminate(scenario, "could not read the complete changed-file set") - - def test_sensitive_paths_require_security_impact_notes_including_renames(self): - for scenario in ("security-notes-missing", "security-rename-out", "security-crate", "security-agent-import", "security-dsc"): - with self.subTest(scenario=scenario): - self.assert_blocked(scenario, "security-impact notes") - result = self.run_gate("security-notes-present") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - result = self.run_gate("security-none") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - self.assert_blocked("security-pending", "security-impact notes") - - def test_sensitive_paths_without_security_attestation_are_indeterminate(self): - self.assert_indeterminate("security-review-missing", "security-focused reviewer comment") - - def test_refreshed_failed_or_required_skipped_runs_block(self): - for scenario in ("check-run-failed", "check-run-required-skip", "check-run-optional-skip"): - with self.subTest(scenario=scenario): - phrase = "skipped CI job outside the explicit workflow allowlist" if scenario == "check-run-optional-skip" else "refreshed check run(s)" - self.assert_blocked(scenario, phrase) - result = self.run_gate("check-run-allowlisted-skip") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_nonempty_pending_legacy_status_is_indeterminate(self): - self.assert_indeterminate("status-nonempty-pending", "commit-status evidence") - - def test_truncated_legacy_status_pages_are_indeterminate(self): - self.assert_indeterminate("status-truncated", "complete head-bound commit-status evidence") - - def test_complete_combined_status_pages_bind_head_and_contexts(self): - result = self.run_gate("status-two-page") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - for scenario in ("status-truncated", "status-duplicate", "status-mixed-head"): - with self.subTest(scenario=scenario): - self.assert_indeterminate(scenario, "complete head-bound commit-status evidence") - - def test_validation_commands_must_be_concrete_and_in_validation(self): - for scenario in ("validation-command-outside", "validation-tool-prose", "validation-placeholder-command", "validation-html-comment"): - with self.subTest(scenario=scenario): - self.assert_blocked(scenario, "actual test or reproduction command") - result = self.run_gate("validation-node-command") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - result = self.run_gate("validation-tilde-command") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_empty_fenced_functional_summary_is_not_content(self): - for scenario in ("empty-fenced-summary", "empty-tilde-summary", "empty-typed-tilde-summary", "empty-rule-summary", "inline-fence-summary"): - with self.subTest(scenario=scenario): - self.assert_blocked(scenario, "non-empty functional summary") - for scenario in ("nonempty-fenced-summary", "nonempty-tilde-summary"): - with self.subTest(scenario=scenario): - result = self.run_gate(scenario) - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_checklist_heading_is_not_a_completed_item(self): - self.assert_blocked("checklist-heading", "review-checklist link") - - def test_context_report_jq_failure_is_indeterminate_without_fabricated_count(self): - result = self.run_gate("context-report-jq-failure") - self.assertEqual(result.returncode, 2, result.stdout + result.stderr) - self.assertIn("could not compute bounded required-check diagnostics", result.stdout) - self.assertNotIn("1 of 0 required check contexts", result.stdout) - self.assertNotIn("all 0 required check contexts passed", result.stdout) - - def test_required_context_diagnostics_are_bounded_for_failures(self): - result = self.run_gate("required-context-output-bound") - self.assertEqual(result.returncode, 1, result.stdout + result.stderr) - self.assertIn("50 of 55 required check contexts", result.stdout) - self.assertLess(len(result.stdout.encode()), 6000) - self.assertNotIn("z" * 100, result.stdout) - - def test_security_review_is_focused_independent_and_current(self): - for scenario in ("security-review-stale", "security-review-author", "security-review-unrelated", "security-review-no-scope", "security-review-bare-scope", "security-review-placeholder-rationale", "security-review-hidden", "security-review-hidden-unterminated", "security-review-fenced", "security-review-fenced-unterminated", "security-review-outsider", "security-review-dismissed", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-documents-screen", "security-documents-screen-rename-out", "security-tauri-cargo", "security-tauri-cargo-rename-out", "security-tauri-cargo-lock", "security-tauri-cargo-lock-rename-out", "security-tauri-lib", "security-tauri-lib-rename-out", "security-commands-facade", "security-bank-statement-import", "security-prune-package-compiler-cache", "security-prune-package-compiler-cache-rename-out", "security-ci-workflow", "security-ci-workflow-rename-out", "security-release-preview", "security-release-preview-rename-out", "security-deploy-install-page", "security-deploy-install-page-rename-out", "security-review-path-privacy-module", "security-review-path-privacy-module-rename-out", "security-review-path-privacy-coordinator", "security-review-path-privacy-coordinator-rename-out", "security-review-path-diff-parser", "security-review-path-diff-parser-rename-out"): - with self.subTest(scenario=scenario): - self.assert_indeterminate(scenario, "security-focused reviewer comment") - result = self.run_gate("security-review-valid") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - result = self.run_gate("security-ci-workflow-valid") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - result = self.run_gate("non-sensitive-cargo-lock") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - result = self.run_gate("security-deploy-install-page-valid") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - result = self.run_gate("security-documents-screen-valid") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_sync_rename_out_requires_migration_notes(self): - self.assert_blocked("sync-migration-missing", "migration compatibility") - result = self.run_gate("sync-migration-present") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_implementation_additions_need_all_three_p4_answers(self): - self.assert_blocked("implementation-p4-missing", "all three substantive P4") - self.assert_blocked("implementation-p4-shell", "all three substantive P4") - self.assert_blocked("implementation-p4-powershell", "all three substantive P4") - self.assert_blocked("implementation-p4-sql", "all three substantive P4") - self.assert_blocked("p4-placeholders", "all three substantive P4") - result = self.run_gate("implementation-p4-present") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - result = self.run_gate("implementation-p4-continuation") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_dependency_manifest_additions_need_a_substantive_rationale(self): - self.assert_blocked("dependency-manifest-missing", "dependency manifest addition lacks a substantive dependency justification") - result = self.run_gate("dependency-manifest-present") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_platform_sensitive_paths_need_substantive_both_host_evidence(self): - self.assert_blocked("platform-evidence-missing", "substantive Windows validation") - result = self.run_gate("platform-evidence-present") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - result = self.run_gate("platform-evidence-heading") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - result = self.run_gate("platform-checkbox-evidence") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - self.assert_blocked("platform-checkbox-comment", "substantive Windows validation") - self.assert_blocked("platform-inline-prose", "substantive Windows validation") - self.assert_blocked("platform-negative-outcome", "substantive Windows validation") - self.assert_blocked("platform-unaffected-bare", "substantive Windows validation") - self.assert_blocked("platform-evidence-bare-label", "substantive Windows validation") - self.assert_blocked("platform-evidence-sibling-list", "substantive Windows validation") - self.assert_blocked("platform-evidence-package-manager", "substantive Windows validation") - self.assert_blocked("platform-evidence-empty-fence", "substantive Windows validation") - self.assert_blocked("platform-evidence-punctuated-placeholder", "substantive Windows validation") - result = self.run_gate("platform-evidence-fenced-continuation") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - result = self.run_gate("platform-unaffected-rationale") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_windows_native_setup_action_needs_both_host_evidence(self): - self.assert_blocked("platform-windows-native-action", "substantive Windows validation") - self.assert_blocked("platform-windows-native-action-rename-out", "substantive Windows validation") - - def test_ci_workflow_needs_both_host_evidence(self): - self.assert_blocked("platform-ci-workflow", "substantive Windows validation") - - def test_release_mcpb_preview_needs_both_host_evidence(self): - self.assert_blocked("platform-release-mcpb-preview", "substantive Windows validation") - - def test_ci_workflow_rename_out_needs_both_host_evidence(self): - self.assert_blocked("platform-ci-workflow-rename-out", "substantive Windows validation") - - def test_powershell_paths_need_both_host_evidence(self): - self.assert_blocked("platform-powershell-missing", "substantive Windows validation") - result = self.run_gate("platform-powershell-evidence") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_checklist_fetch_failure_stays_indeterminate(self): - result = self.run_gate("checklist-fetch-fail") - self.assertEqual(result.returncode, 2, result.stdout + result.stderr) - self.assertIn("could not read review-checklist content", result.stdout) - self.assertNotIn("description changed and no longer carries a completed", result.stdout) - - def test_database_migration_paths_need_rollback_notes(self): - self.assert_blocked("migration-rollback-missing", "database migration path lacks non-empty rollback notes") - result = self.run_gate("migration-rollback-present") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - - def test_wrapped_canonical_migration_template_field_is_recognized(self): - result = self.run_gate("migration-template-wrapped") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - self.assert_blocked("migration-template-other-field", "database migration path lacks non-empty rollback notes") - - def test_developer_home_path_shapes_are_scanned_without_echoing_values(self): - unicode_user = chr(0x03BB) + chr(0x00E9) - for scenario in ("home-macos", "home-unix", "home-root", "home-root-home", "home-windows", "home-macos-root", "home-unix-root", "home-windows-forward", "home-windows-escaped", "home-macos-unicode", "home-unix-unicode", "home-windows-unicode"): - with self.subTest(scenario=scenario): - result = self.run_gate(scenario) - self.assertEqual(result.returncode, 1, result.stdout + result.stderr) - self.assertIn("developer-home path shape", result.stdout) - self.assertNotIn("tester", result.stdout) - self.assertNotIn(unicode_user, result.stdout) - result = self.run_gate("home-regex-source") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - def test_diff_parser_uses_python38_compatible_prefix_removal(self): - import importlib.util - parser = ROOT / "scripts" / "merge_gate_diff.py" - spec = importlib.util.spec_from_file_location("merge_gate_diff", parser) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - class NoRemovePrefix(str): - def removeprefix(self, _prefix): - raise AssertionError("Python 3.8 lacks str.removeprefix") - self.assertEqual(module.diff_destination(NoRemovePrefix("diff --git a/x b/y")), "y") - self.assertEqual(module.textual_destination(NoRemovePrefix("+++ b/y")), "y") - - def test_diff_parser_accepts_git_generated_mixed_quote_rename_headers(self): - import importlib.util - parser = ROOT / "scripts" / "merge_gate_diff.py" - spec = importlib.util.spec_from_file_location("merge_gate_diff_mixed", parser) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - unicode_name = '"b/\\351\\233\\252.txt"' - self.assertEqual(module.diff_destination('diff --git "a/\\351\\233\\252.txt" b/plain.txt'), "plain.txt") - self.assertEqual(module.diff_destination('diff --git a/plain.txt ' + unicode_name), "雪.txt") - self.assertEqual(module.diff_destination("diff --git a/foo b/bar b/foo b/bar"), "foo b/bar") - - def test_definite_blocker_wins_over_indeterminate_evidence(self): - self.assert_blocked("draft-surface-fail", "merge state DRAFT") - result = self.run_gate("draft-surface-fail") - self.assertNotIn("INDETERMINATE — do not merge", result.stdout) + def test_gitlink_change_is_indeterminate(self): + self.assert_indeterminate("gitlink-change", "gitlink change(s) require explicit provenance") + + # -- Minimal head-SHA review-evidence check (KEEP #3, new) --------------- + + def test_review_object_naming_head_passes(self): + self.assert_pass("review-names-head-via-review", "review evidence names the current head") + + def test_summary_comment_naming_head_passes(self): + self.assert_pass("review-names-head-via-comment", "review evidence names the current head") + + def test_no_review_evidence_blocks(self): + self.assert_blocked("review-missing", "no review evidence (review or comment) names current head") + self.assertIn("#317", self.run_gate("review-missing").stdout) + + # -- Regression tests for the 7 fixes in the kept code ------------------- + + def test_346_parenthesized_date_range_not_flagged(self): + # Before the fix, GROUPED_NUMBER_RE's captured boundary character + # (here "(" and ")") was left in match.group(0), so DATE_RANGE_RE's + # fullmatch never matched and the date range was misreported as an + # unexplained long digit run. + result = self.assert_pass("date-range-parens") + self.assertNotIn("unexplained long digit run", result.stdout) + + def test_358_scan_input_write_failure_is_indeterminate(self): + # Before the fix, `{ printf ...; cat ...; } >"$scan_input_file"` had + # no `|| status=$?`, so a failed write was never detected and a + # truncated/empty file was silently fed to the privacy classifier. + self.assert_indeterminate("scan-input-write-failure", "could not assemble complete privacy scan input") + + def test_360_lfs_pointer_requires_binary_attestation(self): + # Before the fix, an added Git LFS pointer (ordinary text, no "GIT + # binary patch" marker) was reconciled as a normal textual change and + # never required a binary-review attestation. + self.assert_blocked("lfs-pointer-binary", "require matching --binary-review-sha and --independent-review-sha") + + def test_365_removed_record_line_total_mismatch_is_flagged(self): + # Before the fix, `[ "$status" = "removed" ] && continue` skipped a + # removed REST record before its line totals were ever compared + # against the diff, so a mismatch went unreported. + self.assert_indeterminate("removed-file-mismatch", "line totals for 'docs/retired.md' differ from REST metadata") + + def test_366_coverage_example_redacts_identifier_shape(self): + # Before the fix, only home-directory paths were redacted from a + # coverage-issue filename before it was echoed into gate output; an + # identifier-shaped filename reached the message verbatim. (The same + # filename is also, independently, real scan input -- path_text is + # always scanned raw -- so this PR is separately and correctly + # BLOCKed by the classifier itself; that is not what this test + # checks. It checks that the *coverage diagnostic message* never + # echoes the raw identifier shape.) + result = self.assert_blocked("coverage-example-redaction", "omits or duplicates 'docs/.md'") + self.assertNotIn("ABCDE1234F", result.stdout) + + def test_373_diff_destination_without_rest_record_is_flagged(self): + # Before the fix, reconciliation only walked REST records looking for + # a matching diff section; a diff section with no REST record at all + # (the reverse direction) was never flagged. + self.assert_indeterminate("diff-extra-destination", "diff destination 'docs/smuggled.md' has no corresponding REST record") + + def test_377_localhost_author_email_is_accepted(self): + # Before the fix, the author/committer email regex required a dotted + # two-letter TLD, so a Git-valid address like dev@localhost rejected + # the whole commit-metadata fetch and the PR went INDETERMINATE. + result = self.assert_pass("author-email-localhost") + self.assertNotIn("could not prove complete head-bound PR commit metadata", result.stdout) if __name__ == "__main__": - unittest.main(verbosity=2) + unittest.main() diff --git a/scripts/merge_gate_diff.py b/scripts/merge_gate_diff.py index ea36537a1..0a6a4adb9 100755 --- a/scripts/merge_gate_diff.py +++ b/scripts/merge_gate_diff.py @@ -128,6 +128,15 @@ def textual_destination(line: str) -> str | None: return path[2:] +# A Git LFS pointer is committed as ordinary text -- three or four short +# lines naming the spec version, the real object's oid, and its size -- so it +# never triggers Git's own "GIT binary patch" / "Binary files ... differ" +# markers. Its *content* is the only signal that the tracked file is actually +# binary. Treat the presence of the spec-version line in an added hunk as +# binary so it cannot bypass the binary-review attestation gate. +LFS_POINTER_VERSION_LINE = "version https://git-lfs.github.com/spec/v1" + + def parse(lines: list[str]) -> dict[str, object]: records: list[dict[str, object]] = [] added_payload: list[str] = [] @@ -182,8 +191,11 @@ def emit() -> None: record["in_hunk"] = True continue if record["in_hunk"] and raw_line.startswith("+"): + content = raw_line[1:] record["added"] = int(record["added"]) + 1 - added_payload.append(raw_line[1:]) + added_payload.append(content) + if content.rstrip("\r\n") == LFS_POINTER_VERSION_LINE: + record["binary"] = True elif record["in_hunk"] and raw_line.startswith("-"): record["deleted"] = int(record["deleted"]) + 1 emit() diff --git a/scripts/merge_gate_fake_gh.py b/scripts/merge_gate_fake_gh.py deleted file mode 100755 index c20bd7531..000000000 --- a/scripts/merge_gate_fake_gh.py +++ /dev/null @@ -1,930 +0,0 @@ -#!/usr/bin/env python3 -"""Offline GitHub CLI fixture for ``merge-gate.test.py``.""" - -import base64, json, os, sys -args = sys.argv[1:] -scenario = os.environ.get("GATE_SCENARIO", "pass") -security_case = scenario.startswith("security-review-") or scenario in {"security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-documents-screen", "security-documents-screen-rename-out", "security-documents-screen-valid", "security-tauri-cargo", "security-tauri-cargo-rename-out", "security-tauri-cargo-lock", "security-tauri-cargo-lock-rename-out", "security-tauri-lib", "security-tauri-lib-rename-out", "security-commands-facade", "security-bank-statement-import", "security-prune-package-compiler-cache", "security-prune-package-compiler-cache-rename-out", "security-ci-workflow", "security-ci-workflow-rename-out", "security-ci-workflow-valid", "security-release-preview", "security-release-preview-rename-out", "security-deploy-install-page", "security-deploy-install-page-rename-out", "security-deploy-install-page-valid", "workflow-notes-present", "workflow-delete-notes", "workflow-rename-out-notes"} -security_case = security_case or scenario.startswith("security-review-path-") -security_workflow_case = scenario in {"security-ci-workflow", "security-ci-workflow-rename-out", "security-ci-workflow-valid", "security-release-preview", "security-release-preview-rename-out", "security-deploy-install-page", "security-deploy-install-page-rename-out", "security-deploy-install-page-valid"} -sync_case = scenario.startswith("sync-") -head = "0123456789abcdef0123456789abcdef01234567" -new_head = "fedcba9876543210fedcba9876543210fedcba98" -base = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - -def emit(value): - if value is not None: - print(value if isinstance(value, str) else json.dumps(value)) - -def fail(message="controlled API failure"): - print(message, file=sys.stderr) - raise SystemExit(1) - -def next_counter(name): - path = os.environ.get(name) - if not path: - return 0 - try: - count = int(open(path).read()) - except (FileNotFoundError, ValueError): - count = 0 - with open(path, "w") as counter: - counter.write(str(count + 1)) - return count - -if args[:2] == ["pr", "view"]: - counter_path = os.environ.get("GATE_COUNTER") - view_count = 0 - if counter_path: - try: - view_count = int(open(counter_path).read()) - except (FileNotFoundError, ValueError): - pass - with open(counter_path, "w") as counter: - counter.write(str(view_count + 1)) - selected_head = new_head if scenario in {"head-moves", "binary-review-head-moves"} and view_count > 0 else head - final_state = "UNKNOWN_VALUE" if scenario == "final-unrecognized" and view_count > 0 else ("BLOCKED" if scenario == "blocked-state" else ("DRAFT" if scenario in {"draft-state", "draft-surface-fail"} else "CLEAN")) - title = "Safe merge gate control" - if scenario == "metadata-title-id": - title = "Customer " + "ABCDE" + "1234" + "F" - elif scenario == "privacy-email-title": - title = "Customer customer@company.test" - body = ( - "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" - "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" - "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" - ) - if scenario == "body-loses-evidence" and view_count > 0: - body = ( - "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" - "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" - ) - elif scenario == "body-loses-functional" and view_count > 0: - body = ( - "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" - "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" - ) - if scenario == "checklist-foreign": - body = "- [x] [Errors](https://github.com/other/repo/blob/HEAD/review-checklist.md#L10)" - elif scenario == "checklist-unlinked": - body = "- [x] review-checklist.md line 10" - elif scenario == "checklist-template-continuation": - body = ( - "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" - "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" - "- [x] One completed [`review-checklist.md`](../review-checklist.md) line is\n" - " linked here: https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10" - ) - elif scenario == "missing-functional-summary": - body = ( - "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" - "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" - ) - elif scenario == "template-functional-prompt": - body = ( - "## Outcome and reason\n\nWhat concrete user or maintainer workflow changes, and why now?\n\n" - "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" - "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" - ) - elif scenario in {"template-validation-prompt", "template-validation-sha-only"}: - sha = "deadbeef" if scenario == "template-validation-sha-only" else "" - body = ( - "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" - f"## Validation and evidence\n\n- Exact candidate SHA: {sha}\n" - "- Commands and results (`corepack pnpm ...`, `cargo ...`, or reproduction):\n\n" - "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" - ) - elif scenario == "template-validation-empty": - body = ( - "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" - "## Validation and evidence\n\nCommands and results are recorded elsewhere.\n\n" - "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" - ) - elif scenario in {"workflow-notes-present", "workflow-delete-notes", "workflow-rename-out-notes", "platform-ci-workflow", "platform-ci-workflow-rename-out", "platform-release-mcpb-preview"}: - body = ( - "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" - "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" - "## Rollback notes\n\nRevert the workflow commit.\n\n" - "## Migration compatibility\n\nNo persisted data changes.\n\n" - "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" - ) - if scenario in {"workflow-notes-present", "workflow-delete-notes", "workflow-rename-out-notes"}: - body += ( - "\n- Windows validation evidence: Windows CI passed `python3 scripts/merge-gate.test.py`.\n" - "- macOS validation evidence: macOS CI passed `python3 scripts/merge-gate.test.py`.\n" - ) - elif scenario == "workflow-sibling-migration": - body = ( - "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" - "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" - "## Rollback notes\n\n- Migration compatibility: No persisted data changes.\n\n" - "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" - ) - elif scenario in {"security-notes-present", "security-none", "security-pending", "surface-unpins"}: - security_impact = "None" if scenario == "security-none" else ("pending" if scenario == "security-pending" else "No credential material is added; the Tally path change is reviewed.") - body = ( - "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" - "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" - f"## Security impact\n\n{security_impact}\n\n" - "## Migration compatibility\n\nExisting callers retain their paths and formats.\n\n" - "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" - ) - elif scenario == "checklist-missing-anchor": - body = ( - "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" - "## Validation and evidence\n\n`python3 scripts/merge-gate.test.py`\n\n" - "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L999999)" - ) - elif scenario == "missing-test-summary": - body = ( - "## Outcome and reason\n\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n\n" - "- [x] [Errors](https://github.com/lamemustafa/bridge/blob/HEAD/review-checklist.md#L10)" - ) - if security_case or sync_case: - body += "\n## Security impact\n\nCredential admission remains enforced.\n" - if not scenario.endswith("migration-missing"): - body += "\n## Migration compatibility\n\nNo persisted format change.\n" - if scenario == "validation-command-outside": - body = body.replace("`python3 scripts/merge-gate.test.py`", "Tests not run") + "\n## Rollback\n`cargo test`\n" - if scenario == "validation-node-command": - body = body.replace("`python3 scripts/merge-gate.test.py`", "`node --test scripts/prune-package-compiler-cache.test.mjs`") - if scenario == "validation-tilde-command": - body = body.replace("`python3 scripts/merge-gate.test.py`", "~~~bash\npython3 scripts/merge-gate.test.py\n~~~") - structural_summaries = { - "empty-fenced-summary": "```\n```", - "empty-tilde-summary": "~~~\n~~~", - "empty-typed-tilde-summary": "~~~text example\n~~~", - "empty-rule-summary": "---\n* * *\n___", - "inline-fence-summary": "~~~text", - } - if scenario in structural_summaries: - body = body.replace("A bounded merge preflight keeps incomplete evidence from becoming a merge.", structural_summaries[scenario]) - if scenario == "inline-fence-summary": - body = body.replace("## Functional summary\n\n", "## Functional summary: ") - if scenario in {"nonempty-fenced-summary", "nonempty-tilde-summary"}: - fence = "~~~text" if scenario == "nonempty-tilde-summary" else "```" - body = body.replace("A bounded merge preflight keeps incomplete evidence from becoming a merge.", fence + "\nA bounded merge preflight keeps incomplete evidence from becoming a merge.\n" + fence[:3]) - if scenario == "policy-multiline-comment": - body = body.replace("A bounded merge preflight keeps incomplete evidence from becoming a merge.", "") - if scenario == "validation-html-comment": - body = body.replace("`python3 scripts/merge-gate.test.py`", "Tests not run ") - if scenario == "unterminated-html-comment": - body = body.replace("`python3 scripts/merge-gate.test.py`", "" - if scenario == "body-comment-drift" and view_count > 0: - body += "\n" - if scenario == "validation-tool-prose": - body = body.replace("`python3 scripts/merge-gate.test.py`", "Tests not run; cargo is available") - if scenario == "validation-placeholder-command": - body = body.replace("`python3 scripts/merge-gate.test.py`", "`cargo ...`") - if scenario == "dependency-manifest-present": - body += "\n## Dependency justification\n\nThe parser library is required for the sealed response format.\n" - if scenario == "checklist-heading": - body = body.replace("#L10", "#L1") - if scenario == "checklist-anchor-suffix": - body = body.replace("#L10", "#L10junk") - if scenario in {"implementation-p4-present", "implementation-p4-continuation", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-negative-outcome", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "migration-rollback-present", "migration-template-wrapped", "security-notes-present", "security-none", "security-pending", "security-review-valid"}: - body += ( - "\n## Scope, reuse, and impact\n\n" - "- Existing component reused: the existing gate parser and file inventory.\n" - "- What is deleted (or why no deletion is justified): no duplicate path remains.\n" - "- What breaks if this is not built: unsafe evidence could reach a merge.\n" - ) - if scenario == "implementation-p4-continuation": - body = body.replace( - "- Existing component reused: the existing gate parser and file inventory.\n" - "- What is deleted (or why no deletion is justified): no duplicate path remains.\n" - "- What breaks if this is not built: unsafe evidence could reach a merge.\n", - "- Existing component reused:\n the existing gate parser and file inventory.\n" - "- What is deleted (or why no deletion is justified):\n no duplicate path remains.\n" - "- What breaks if this is not built:\n unsafe evidence could reach a merge.\n", - ) - if security_case or scenario in {"surface-unpins", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"}: - body += ( - "\n## Scope, reuse, and impact\n\n" - "- Existing component reused: the existing gate parser and file inventory.\n" - "- What is deleted (or why no deletion is justified): no duplicate path remains.\n" - "- What breaks if this is not built: unsafe evidence could reach a merge.\n" - "\n## Security impact\n\nNo credential material is added.\n" - "\n## Migration compatibility\n\nExisting callers retain their paths and formats.\n" - "\n- Windows validation evidence: Windows CI passed `python3 scripts/merge-gate.test.py`.\n" - "- macOS validation evidence: macOS CI passed `python3 scripts/merge-gate.test.py`.\n" - ) - if security_workflow_case: - body += "\n## Rollback notes\n\nRevert the workflow change before the next release.\n" - if scenario in {"security-tauri-cargo", "security-tauri-cargo-rename-out"}: - body += "\n## Dependency rationale\n\nThe manifest boundary remains under independent credential-focused review.\n" - if scenario == "security-encrypted-keystore": - body += "\n## Rollback notes\n\nRevert the encrypted-store change before deployment.\n" - if scenario in {"platform-evidence-present", "platform-powershell-evidence", "migration-template-wrapped", "security-notes-present", "security-none", "security-pending", "security-review-valid", "sync-migration-present"}: - body += ( - "\n- Windows validation evidence: Windows CI passed `python3 scripts/merge-gate.test.py`.\n" - "- macOS validation evidence: macOS CI passed `python3 scripts/merge-gate.test.py`.\n" - ) - if scenario == "platform-evidence-heading": - body += ( - "\n### Windows validation evidence\n\n" - "`python3 scripts/merge-gate.test.py` passed on Windows CI.\n" - "\n### macOS validation evidence\n\n" - "`python3 scripts/merge-gate.test.py` passed on macOS CI.\n" - ) - if scenario == "platform-checkbox-evidence": - body += ( - "\n- [x] Native Windows validation completed: `python3 scripts/merge-gate.test.py` passed on Windows CI.\n" - "- [x] Native macOS validation completed: `python3 scripts/merge-gate.test.py` passed on macOS CI.\n" - ) - if scenario == "platform-checkbox-comment": - body += ( - "\n- [x] Native Windows validation completed: \n" - "- [x] Native macOS validation completed: \n" - ) - if scenario == "platform-inline-prose": - body += ( - "\n- Windows validation evidence: reviewed by the release team.\n" - "- macOS validation evidence: reviewed by the release team.\n" - ) - if scenario == "platform-negative-outcome": - body += "\n- Windows validation evidence: Windows CI did not pass.\n- macOS validation evidence: macOS CI did not pass.\n" - if scenario == "platform-unaffected-bare": - body += "\n- Windows validation evidence: unaffected\n- macOS validation evidence: unaffected\n" - if scenario == "platform-unaffected-rationale": - body += "\n- Windows validation evidence: unaffected because this changes shared documentation only.\n- macOS validation evidence: unaffected because this changes shared documentation only.\n" - if scenario in {"platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager"}: - body += ( - "\n## Security impact\n\nNo credential material is added.\n" - "\n## Migration compatibility\n\nExisting callers retain their paths and formats.\n" - ) - if scenario == "migration-rollback-present": - body += "\n## Rollback notes\n\nRevert the migration commit before deployment.\n" - if scenario == "migration-template-wrapped": - body += ( - "\n- Migration/sync compatibility and rollback procedure (required when an\n" - " existing workflow changes): Existing readers retain the old format; revert this commit before deployment.\n" - ) - if scenario == "migration-template-other-field": - body += ( - "\n- Migration/sync compatibility and rollback procedure (required when an\n" - " existing workflow changes):\n" - "- Destructive database migration: No\n" - ) - if scenario == "p4-placeholders": - body += ( - "\n## Scope, reuse, and impact\n\n" - "- Existing component reused: N/A\n" - "- What is deleted (or why no deletion is justified): TODO\n" - "- What breaks if this is not built: pending\n" - ) - if scenario == "workflow-placeholders": - body += "\n## Rollback notes\n\nN/A\n\n## Migration compatibility\n\nTBD\n" - if scenario == "workflow-punctuated-placeholders": - body += "\n## Rollback notes\n\nN/A.\n\n## Migration compatibility\n\nTBD.\n" - if scenario == "platform-evidence-bare-label": - body += "\n- Windows validation evidence\n- macOS validation evidence\n" - if scenario == "platform-evidence-sibling-list": - body += "\n- Windows validation evidence\n- Notes: tracked separately\n- macOS validation evidence\n- Notes: tracked separately\n" - if scenario == "platform-evidence-package-manager": - body += "\n- Windows validation evidence\n- Package manager: pnpm\n- macOS validation evidence\n- Package manager: pnpm\n" - if scenario == "platform-evidence-empty-fence": - body += "\n### Windows validation evidence\n~~~bash\n~~~\n### macOS validation evidence\n~~~bash\n~~~\n" - if scenario == "platform-evidence-punctuated-placeholder": - body += "\n- Windows validation evidence: N/A.\n- macOS validation evidence: TBD.\n" - if scenario == "platform-evidence-fenced-continuation": - body += "\n### Windows validation evidence\n~~~text\nWindows CI passed `python3 scripts/merge-gate.test.py`.\n~~~\n### macOS validation evidence\n~~~text\nmacOS CI passed `python3 scripts/merge-gate.test.py`.\n~~~\n" - if scenario == "privacy-email-body": - body += "\nCustomer contact: customer@company.test\n" - body = body.replace("blob/HEAD", f"blob/{head}") - if scenario == "checklist-stale-ref": - body = body.replace(f"blob/{head}", "blob/" + "f" * 40) - one_file = scenario in {"metadata-private", "files-empty", "formatted-phone", "formatted-phone-grouped", "unicode-phone", "unicode-phone-tab", "unicode-phone-two-lines", "repeated-phone", "grouped-identifier-12", "grouped-identifier-16", "grouped-identifier-mixed", "phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized", "landline-grouped", "landline-standard-hyphen", "landline-standard-space", "landline-standard-underscore", "pem-certificate-envelope", "path-id", "binary-delete", "binary-review", "binary-review-private", "binary-review-head-moves", "metadata-only", "metadata-incomplete", "hunk-header-phone", "hunk-header-literals", "hunk-binary-literal", "separated-dates", "separated-dates-new-year", "separated-dates-year-month", "adr-identifier", "all-a-pan", "masked-pan", "grouped-pan-space", "grouped-pan-hyphen", "grouped-masked-pan-space", "quoted-path", "control-path", "workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration", "workflow-delete", "workflow-delete-notes", "workflow-rename-out", "workflow-rename-out-notes", "workflow-placeholders", "workflow-punctuated-placeholders", "renamed-previous-missing", "renamed-previous-null", "renamed-previous-false", "security-notes-missing", "security-notes-present", "security-none", "security-pending", "security-rename-out", "security-crate", "security-agent-import", "security-dsc", "dependency-manifest-missing", "dependency-manifest-present", "home-macos", "home-unix", "home-windows", "crlf-diff", "ambiguous-unquoted-path", "ambiguous-rename-path", "gitlink", "gitlink-existing", "implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders", "platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-powershell-missing", "platform-powershell-evidence", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-negative-outcome", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager", "platform-windows-native-action", "platform-windows-native-action-rename-out", "platform-ci-workflow", "platform-ci-workflow-rename-out", "platform-release-mcpb-preview", "migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"} or scenario.startswith("home-") or security_case or sync_case - one_file = one_file or scenario in {"skipped-native-scope", "skipped-bundle-scope", "non-sensitive-cargo-lock", "privacy-email-payload", "privacy-email-destination", "privacy-email-title", "privacy-email-body", "privacy-email-commit", "privacy-email-author"} - selected_base = new_head if scenario == "base-oid-mismatch" else base - metadata = {"headRefOid": selected_head, "baseRefOid": selected_base, "baseRefName": "master", - "mergeable": "MERGEABLE", "mergeStateStatus": final_state, - "isDraft": False, "state": "OPEN", "title": title, - "body": body, "changedFiles": 1 if one_file else 2} - if view_count >= 2: - late_changes = { - "late-meta-head": ("headRefOid", new_head), - "late-meta-base-oid": ("baseRefOid", new_head), - "late-meta-base-name": ("baseRefName", "other"), - "late-meta-title": ("title", "Changed title"), - "late-meta-title-newline": ("title", title + "\n"), - "late-meta-body-comment": ("body", body + "\n"), - "late-meta-body-newline": ("body", body + "\n"), - "late-meta-draft": ("isDraft", True), - "late-meta-state": ("state", "CLOSED"), - "late-meta-mergeable": ("mergeable", "CONFLICTING"), - "late-meta-merge-state": ("mergeStateStatus", "HAS_HOOKS"), - "late-meta-unknown-state": ("mergeStateStatus", "UNKNOWN_VALUE"), - "late-meta-changed-files": ("changedFiles", 3), - "late-meta-body-wrong-type": ("body", False), - } - if scenario == "late-meta-error": - fail("controlled final PR metadata read failure") - if scenario == "late-meta-missing-body": - del metadata["body"] - if scenario in late_changes: - field, value = late_changes[scenario] - metadata[field] = value - requested_fields = args[args.index("--json") + 1].split(",") - emit({field: metadata[field] for field in requested_fields if field in metadata}) -elif args[:2] == ["pr", "checks"]: - if scenario == "checks-silent": - raise SystemExit(0) - if scenario == "cancel-check": - emit([{"bucket": "cancel", "name": "Required checks"}]) - elif scenario == "required-skip": - emit([{"bucket": "skipping", "name": "Required checks"}, {"bucket": "pass", "name": "Rust format"}]) - elif scenario == "missing-required": - emit([{"bucket": "pass", "name": "Required checks"}]) - else: - checks = [{"bucket": "pass", "name": "Frontend build"}, - {"bucket": "pass", "name": "Rust format"}, - {"bucket": "pass", "name": "GitGuardian Security Checks"}, - {"bucket": "pass", "name": "Dependency security"}, - {"bucket": "pass", "name": "Required checks"}, - {"bucket": "skipping", "name": "Retain two compiler-cache snapshots per OS"}] - if scenario in {"skipped-docs-matrix", "skipped-native-scope", "skipped-bundle-scope"}: - checks.extend([ - {"bucket": "skipping", "name": "Native checks (windows-latest)"}, - {"bucket": "skipping", "name": "Native checks (macos-latest)"}, - {"bucket": "skipping", "name": "Bundle smoke (windows-latest)"}, - {"bucket": "skipping", "name": "Bundle smoke (macos-latest)"}, - ]) - if scenario == "skipped-unallowlisted": - checks.append({"bucket": "skipping", "name": "Unreviewed conditional job"}) - emit(checks) -elif args[:2] == ["pr", "diff"]: - if scenario == "security-documents-consumer-rename-out": - emit("diff --git a/src-tauri/src/documents.rs b/docs/retired-documents.rs\nsimilarity index 100%\nrename from src-tauri/src/documents.rs\nrename to docs/retired-documents.rs\n") - elif scenario == "security-prune-package-compiler-cache-rename-out": - emit("diff --git a/scripts/prune-package-compiler-cache.mjs b/docs/retired-cache-pruner.mjs\nsimilarity index 100%\nrename from scripts/prune-package-compiler-cache.mjs\nrename to docs/retired-cache-pruner.mjs\n") - elif scenario == "security-ci-workflow-rename-out": - emit("diff --git a/.github/workflows/ci.yml b/docs/retired-ci.yml\nsimilarity index 100%\nrename from .github/workflows/ci.yml\nrename to docs/retired-ci.yml\n") - elif scenario == "security-release-preview-rename-out": - emit("diff --git a/.github/workflows/release-mcpb-preview.yml b/docs/retired-preview.yml\nsimilarity index 100%\nrename from .github/workflows/release-mcpb-preview.yml\nrename to docs/retired-preview.yml\n") - elif scenario == "security-deploy-install-page-rename-out": - emit("diff --git a/.github/workflows/deploy-install-page.yml b/docs/retired-install-page.yml\nsimilarity index 100%\nrename from .github/workflows/deploy-install-page.yml\nrename to docs/retired-install-page.yml\n") - elif scenario == "security-documents-screen-rename-out": - emit("diff --git a/src/DocumentsScreen.tsx b/docs/retired-documents-screen.tsx\nsimilarity index 100%\nrename from src/DocumentsScreen.tsx\nrename to docs/retired-documents-screen.tsx\n") - elif scenario == "security-tauri-cargo-rename-out": - emit("diff --git a/src-tauri/Cargo.toml b/docs/retired-tauri-cargo.toml\nsimilarity index 100%\nrename from src-tauri/Cargo.toml\nrename to docs/retired-tauri-cargo.toml\n") - elif scenario == "security-tauri-cargo-lock-rename-out": - emit("diff --git a/src-tauri/Cargo.lock b/docs/retired-tauri-cargo.lock\nsimilarity index 100%\nrename from src-tauri/Cargo.lock\nrename to docs/retired-tauri-cargo.lock\n") - elif scenario.startswith("security-review-path-") and scenario.endswith("-rename-out"): - paths = { - "security-review-path-privacy-module-rename-out": ("scripts/merge_gate_privacy.py", "docs/retired-privacy.py"), - "security-review-path-privacy-coordinator-rename-out": ("scripts/merge-gate.sh", "docs/retired-merge-gate.sh"), - "security-review-path-diff-parser-rename-out": ("scripts/merge_gate_diff.py", "docs/retired-diff.py"), - } - old_path, new_path = paths[scenario] - emit(f"diff --git a/{old_path} b/{new_path}\nsimilarity index 100%\nrename from {old_path}\nrename to {new_path}\n") - elif scenario == "security-tauri-lib-rename-out": - emit("diff --git a/src-tauri/src/lib.rs b/docs/retired-tauri-lib.rs\nsimilarity index 100%\nrename from src-tauri/src/lib.rs\nrename to docs/retired-tauri-lib.rs\n") - elif security_case: - paths = {"security-axal-frontend": "src/AxalScreen.tsx", "security-axal-native": "src-tauri/src/axal.rs", "security-encrypted-keystore": "src-tauri/src/db/encrypted.rs", "security-documents-consumer": "src-tauri/src/documents.rs", "security-documents-screen": "src/DocumentsScreen.tsx", "security-documents-screen-valid": "src/DocumentsScreen.tsx", "security-tauri-cargo": "src-tauri/Cargo.toml", "security-tauri-cargo-lock": "src-tauri/Cargo.lock", "security-tauri-lib": "src-tauri/src/lib.rs", "security-commands-facade": "src-tauri/src/commands.rs", "security-bank-statement-import": "scripts/bank_statement_import.py", "security-prune-package-compiler-cache": "scripts/prune-package-compiler-cache.mjs", "security-ci-workflow": ".github/workflows/ci.yml", "security-ci-workflow-valid": ".github/workflows/ci.yml", "security-release-preview": ".github/workflows/release-mcpb-preview.yml", "security-deploy-install-page": ".github/workflows/deploy-install-page.yml", "security-deploy-install-page-valid": ".github/workflows/deploy-install-page.yml", "security-review-path-privacy-module": "scripts/merge_gate_privacy.py", "security-review-path-privacy-coordinator": "scripts/merge-gate.sh", "security-review-path-diff-parser": "scripts/merge_gate_diff.py"} - path = paths.get(scenario, "src-tauri/src/dsc.rs") - emit(f"diff --git a/{path} b/{path}\n--- a/{path}\n+++ b/{path}\n@@ -0,0 +1 @@\n+safe check\n") - elif sync_case: - emit("diff --git a/src-tauri/src/sync.rs b/docs/retired.rs\nsimilarity index 100%\nrename from src-tauri/src/sync.rs\nrename to docs/retired.rs\n") - elif scenario == "formatted-phone": - phone = "+91 " + "98765" + "-43210" - emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+Call {phone}\n") - elif scenario == "path-id": - path_id = "ABCDE" + "1234" + "F" - emit(f"diff --git a/docs/safe.md b/docs/{path_id}.md\n--- a/docs/safe.md\n+++ b/docs/{path_id}.md\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario == "skipped-native-scope": - emit("diff --git a/tools/ci-check.md b/tools/ci-check.md\n--- /dev/null\n+++ b/tools/ci-check.md\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario == "skipped-bundle-scope": - emit("diff --git a/index.html b/index.html\n--- a/index.html\n+++ b/index.html\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario == "non-sensitive-cargo-lock": - emit("diff --git a/tools/Cargo.lock b/tools/Cargo.lock\n--- a/tools/Cargo.lock\n+++ b/tools/Cargo.lock\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario == "privacy-email-payload": - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+customer@company.test\n") - elif scenario == "privacy-email-destination": - emit("diff --git a/docs/customer@company.test.md b/docs/customer@company.test.md\n--- /dev/null\n+++ b/docs/customer@company.test.md\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario in {"privacy-email-title", "privacy-email-body", "privacy-email-commit", "privacy-email-author"}: - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario in {"security-notes-missing", "security-notes-present", "security-none", "security-pending"}: - emit("diff --git a/src-tauri/src/tally/runtime.rs b/src-tauri/src/tally/runtime.rs\n--- a/src-tauri/src/tally/runtime.rs\n+++ b/src-tauri/src/tally/runtime.rs\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario in {"security-crate", "security-agent-import", "security-dsc"}: - paths = {"security-crate": "src-tauri/crates/bridge-tally-core/src/master_binding.rs", "security-agent-import": "src-tauri/src/agent_import.rs", "security-dsc": "src-tauri/src/dsc.rs"} - emit(f"diff --git a/{paths[scenario]} b/{paths[scenario]}\n--- a/{paths[scenario]}\n+++ /dev/null\n@@ -1 +0,0 @@\n-safe text\n") - elif scenario == "security-rename-out": - emit("diff --git a/src-tauri/src/tally/runtime.rs b/src/runtime.rs\nsimilarity index 100%\nrename from src-tauri/src/tally/runtime.rs\nrename to src/runtime.rs\n") - elif scenario.startswith("home-"): - root_home = bytes((47, 114, 111, 111, 116)).decode("ascii") - unicode_user = chr(0x03BB) + chr(0x00E9) - homes = {"home-macos": "/" + "Users" + "/" + "tester" + "/work", "home-unix": "/" + "home" + "/" + "tester" + "/work", "home-root": root_home + "/work/customer.pem", "home-root-home": root_home, "home-windows": "C:" + "\\" + "Users" + "\\" + "tester" + "\\work", "home-macos-root": "/" + "Users" + "/" + "tester", "home-unix-root": "/" + "home" + "/" + "tester", "home-windows-forward": "C:" + "/" + "Users" + "/" + "tester" + "/work", "home-windows-escaped": "C:" + "\\\\" + "Users" + "\\\\" + "tester" + "\\\\work", "home-macos-unicode": "/" + "Users" + "/" + unicode_user + "/work", "home-unix-unicode": "/" + "home" + "/" + unicode_user + "/work", "home-windows-unicode": "C:" + "\\" + "Users" + "\\" + unicode_user + "\\work", "home-regex-source": "mac_home=" + "'/'" + '"Users"' + "'/[A-Za-z0-9._-]+'"} - emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{homes[scenario]}\n") - elif scenario == "binary-delete": - emit("diff --git a/docs/old.png b/docs/old.png\nBinary files a/docs/old.png and /dev/null differ\n") - elif scenario in {"binary-review", "binary-review-private", "binary-review-head-moves"}: - emit("diff --git a/docs/new.png b/docs/new.png\nBinary files /dev/null and b/docs/new.png differ\n") - elif scenario == "diff-omits-file": - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario == "diff-truncated-payload": - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +2 @@\n+first line\n") - elif scenario == "formatted-phone-grouped": - phone = "6" + "98 765-4321" - emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {phone}\n") - elif scenario == "repeated-phone": - phone = "6" + "6" * 9 - emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {phone}\n") - elif scenario in {"grouped-identifier-12", "grouped-identifier-16", "grouped-identifier-mixed"}: - groups = ("8421", "7654", "9012", "3456") - identifier = (" ".join(groups[:3]) if scenario.endswith("12") - else (" ".join(groups[:2]) + "-" + " ".join(groups[2:]) - if scenario == "grouped-identifier-mixed" else "-".join(groups))) - emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {identifier}\n") - elif scenario == "platform-ci-workflow-rename-out": - emit("diff --git a/.github/workflows/ci.yml b/docs/retired-ci.yml\nsimilarity index 100%\nrename from .github/workflows/ci.yml\nrename to docs/retired-ci.yml\n") - elif scenario == "platform-release-mcpb-preview": - emit("diff --git a/.github/workflows/release-mcpb-preview.yml b/.github/workflows/release-mcpb-preview.yml\n--- a/.github/workflows/release-mcpb-preview.yml\n+++ b/.github/workflows/release-mcpb-preview.yml\n@@ -0,0 +1 @@\n+safe workflow text\n") - elif scenario in {"workflow-notes-missing", "workflow-notes-present", "workflow-sibling-migration", "platform-ci-workflow"}: - emit("diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml\n--- a/.github/workflows/ci.yml\n+++ b/.github/workflows/ci.yml\n@@ -0,0 +1 @@\n+safe workflow text\n") - elif scenario in {"dependency-manifest-missing", "dependency-manifest-present"}: - emit("diff --git a/package.json b/package.json\n--- a/package.json\n+++ b/package.json\n@@ -0,0 +1 @@\n+safe dependency metadata\n") - elif scenario in {"workflow-delete", "workflow-delete-notes"}: - emit("diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml\n--- a/.github/workflows/ci.yml\n+++ /dev/null\n@@ -1 +0,0 @@\n-safe workflow text\n") - elif scenario in {"workflow-rename-out", "workflow-rename-out-notes"}: - emit("diff --git a/.github/workflows/ci.yml b/docs/retired-ci.yml\nsimilarity index 100%\nrename from .github/workflows/ci.yml\nrename to docs/retired-ci.yml\n") - elif scenario in {"phone-space", "phone-dot", "phone-plus", "phone-underscore", "phone-parenthesized"}: - phones = { - "phone-space": "6" + "9876 54321", - "phone-dot": "6" + "987.654.321", - "phone-plus": "6" + "9876+54321", - "phone-underscore": "6" + "987_654_321", - "phone-parenthesized": "(" + "69876" + ") 54321", - } - emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {phones[scenario]}\n") - elif scenario == "landline-grouped": - landline = "0" + "11" + "-" + "2345" + "-" + "6789" - emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {landline}\n") - elif scenario in {"landline-standard-hyphen", "landline-standard-space", "landline-standard-underscore"}: - separator = {"landline-standard-hyphen": "-", "landline-standard-space": " ", "landline-standard-underscore": "_"}[scenario] - landline = "0" + "11" + separator + "23456789" - emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {landline}\n") - elif scenario == "landline-standard-four-digit": - landline = "0" + "120" + "-" + "2345678" - emit(f"diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic {landline}\n") - elif scenario == "pem-certificate-envelope": - begin = "-" * 5 + "BEGIN CERTIFICATE" + "-" * 5 - end = "-" * 5 + "END CERTIFICATE" + "-" * 5 - body = "MII" + "A" * 48 - emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +3 @@\n+{begin}\n+{body}\n+{end}\n") - elif scenario == "trusted-pem-certificate-envelope": - begin = "-" * 5 + "BEGIN TRUSTED CERTIFICATE" + "-" * 5 - end = "-" * 5 + "END TRUSTED CERTIFICATE" + "-" * 5 - body = "MII" + "B" * 48 - emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +3 @@\n+{begin}\n+{body}\n+{end}\n") - elif scenario == "unicode-phone": - emit("diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic 69876\u00a054321\n") - elif scenario == "unicode-phone-tab": - emit("diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +1 @@\n+synthetic 69876\t54321\n") - elif scenario == "unicode-phone-two-lines": - emit("diff --git a/docs/contact.md b/docs/contact.md\n--- a/docs/contact.md\n+++ b/docs/contact.md\n@@ -0,0 +2 @@\n+69876\n+54321\n") - elif scenario == "metadata-only": - emit("diff --git a/docs/example.md b/docs/example.md\nsimilarity index 100%\nrename from docs/example.md\nrename to docs/example.md\n") - elif scenario == "metadata-private": - private_path = "docs/" + "/" + "Users" + "/tester/" + "x" * 300 - emit(f"diff --git a/{private_path} b/{private_path}\nsimilarity index 100%\nrename from {private_path}\nrename to {private_path}\n") - elif scenario == "metadata-incomplete": - emit("diff --git a/docs/example.md b/docs/example.md\nsimilarity index 100%\nrename from docs/example.md\nrename to docs/example.md\n") - elif scenario == "hunk-header-phone": - phone = "6" + "9876" + "54321" - emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+++ b/synthetic {phone}\n") - elif scenario == "hunk-header-literals": - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +2 @@\n+++ b/safe\n+++ /dev/null\n") - elif scenario == "hunk-binary-literal": - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+GIT binary patch\n") - elif scenario == "all-a-pan": - identifier = "AAAAA" + "1234" + "A" - emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{identifier}\n") - elif scenario == "masked-pan": - identifier = "XXXXX" + "1234" + "X" - emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{identifier}\n") - elif scenario in {"grouped-pan-space", "grouped-pan-hyphen", "grouped-masked-pan-space"}: - separator = "-" if scenario == "grouped-pan-hyphen" else " " - prefix = "XXXXX" if scenario == "grouped-masked-pan-space" else "ABCDE" - suffix = "X" if scenario == "grouped-masked-pan-space" else "A" - emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{prefix}{separator}1234{separator}{suffix}\n") - elif scenario == "quoted-path": - emit('diff --git "a/docs/caf\\303\\251.md" "b/docs/caf\\303\\251.md"\n--- "a/docs/caf\\303\\251.md"\n+++ "b/docs/caf\\303\\251.md"\n@@ -0,0 +1 @@\n+safe text\n') - elif scenario == "crlf-diff": - emit("diff --git a/docs/example.md b/docs/example.md\r\n--- a/docs/example.md\r\n+++ b/docs/example.md\r\n@@ -0,0 +1 @@\r\n+safe text\r\n") - elif scenario == "ambiguous-unquoted-path": - emit("diff --git a/docs/a b/example.md b/docs/a b/example.md\n--- a/docs/a b/example.md\n+++ b/docs/a b/example.md\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario == "ambiguous-rename-path": - emit("diff --git a/docs/a b/example.md b/docs/a b/example.md\nsimilarity index 100%\nrename from docs/a b/example.md\nrename to docs/a b/example.md\n") - elif scenario == "gitlink": - emit("diff --git a/vendor/module b/vendor/module\nnew file mode 160000\nindex 0000000..2222222\n--- /dev/null\n+++ b/vendor/module\n@@ -0,0 +1 @@\n+Subproject commit 2222222\n") - elif scenario == "gitlink-existing": - emit("diff --git a/vendor/module b/vendor/module\nindex 1111111..2222222 160000\n--- a/vendor/module\n+++ b/vendor/module\n@@ -1 +1 @@\n-Subproject commit 1111111\n+Subproject commit 2222222\n") - elif scenario in {"implementation-p4-missing", "implementation-p4-present", "implementation-p4-continuation", "implementation-p4-shell", "implementation-p4-powershell", "implementation-p4-sql", "p4-placeholders"}: - suffix = {"implementation-p4-shell": "sh", "implementation-p4-powershell": "ps1", "implementation-p4-sql": "sql"}.get(scenario, "py") - emit(f"diff --git a/scripts/example.{suffix} b/scripts/example.{suffix}\n--- a/scripts/example.{suffix}\n+++ b/scripts/example.{suffix}\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario in {"platform-evidence-missing", "platform-evidence-present", "platform-evidence-heading", "platform-checkbox-evidence", "platform-checkbox-comment", "platform-inline-prose", "platform-negative-outcome", "platform-unaffected-bare", "platform-unaffected-rationale", "platform-evidence-bare-label", "platform-evidence-sibling-list", "platform-evidence-empty-fence", "platform-evidence-punctuated-placeholder", "platform-evidence-fenced-continuation", "platform-evidence-package-manager"}: - emit("diff --git a/src-tauri/src/local_files/paths.rs b/src-tauri/src/local_files/paths.rs\n--- a/src-tauri/src/local_files/paths.rs\n+++ b/src-tauri/src/local_files/paths.rs\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario == "platform-windows-native-action": - emit("diff --git a/.github/actions/setup-windows-native/action.yml b/.github/actions/setup-windows-native/action.yml\n--- a/.github/actions/setup-windows-native/action.yml\n+++ b/.github/actions/setup-windows-native/action.yml\n@@ -0,0 +1 @@\n+safe action text\n") - elif scenario == "platform-windows-native-action-rename-out": - emit("diff --git a/.github/actions/setup-windows-native/action.yml b/docs/retired-windows-native-action.yml\nsimilarity index 100%\nrename from .github/actions/setup-windows-native/action.yml\nrename to docs/retired-windows-native-action.yml\n") - elif scenario in {"platform-powershell-missing", "platform-powershell-evidence"}: - emit("diff --git a/scripts/signing.ps1 b/scripts/signing.ps1\n--- a/scripts/signing.ps1\n+++ b/scripts/signing.ps1\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario in {"migration-rollback-missing", "migration-rollback-present", "migration-template-wrapped", "migration-template-other-field"}: - emit("diff --git a/src-tauri/migrations/001.sql b/src-tauri/migrations/001.sql\n--- a/src-tauri/migrations/001.sql\n+++ b/src-tauri/migrations/001.sql\n@@ -0,0 +1 @@\n+safe text\n") - elif scenario == "separated-dates": - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+2026-09-12 2026-09-13\n") - elif scenario == "separated-dates-new-year": - first_date = "-".join(("0101", "2026")) - second_date = "-".join(("0201", "2026")) - emit(f"diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+{first_date} {second_date}\n") - elif scenario == "separated-dates-year-month": - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+2025-09-11 2025-09-12\n") - elif scenario == "adr-identifier": - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+CE_ADR_0016_E\n") - elif scenario == "surface-unpins": - emit("diff --git a/src/example.rs b/src/example.rs\n--- a/src/example.rs\n+++ b/src/example.rs\n@@ -0,0 +1 @@\n+safe text\n" - "diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json\n" - "--- a/docs/tally/compatibility/compatibility-surface.json\n+++ b/docs/tally/compatibility/compatibility-surface.json\n@@ -0,0 +1 @@\n+safe manifest\n") - else: - emit("diff --git a/docs/example.md b/docs/example.md\n--- a/docs/example.md\n+++ b/docs/example.md\n@@ -0,0 +1 @@\n+safe text\ndiff --git a/docs/second.md b/docs/second.md\n--- a/docs/second.md\n+++ b/docs/second.md\n@@ -0,0 +1 @@\n+other text\n") -elif args and args[0] == "api": - joined = " ".join(args) - if "graphql" in args: - if scenario == "threads-first-empty-cursor-rejected" and "cursor=" in args: - fail("the first review-thread request must omit cursor") - def thread_nodes(start, count, unresolved=False): - return [{"id": f"thread-{index}", "isResolved": not (unresolved and index == start)} - for index in range(start, start + count)] - has_cursor = "C1" in joined - if scenario == "threads-empty-more": - nodes, page_info = [], {"hasNextPage": True, "endCursor": "C1"} - elif scenario == "threads-short": - nodes, page_info = thread_nodes(1, 1), {"hasNextPage": False, "endCursor": None} - elif scenario == "threads-malformed-pagination": - nodes, page_info = thread_nodes(1, 100), {"hasNextPage": True, "endCursor": None} - elif has_cursor: - if scenario == "threads-duplicate-id": - nodes = [{"id": "thread-1", "isResolved": True}] - else: - nodes = thread_nodes(101, 1, scenario == "threads-unresolved-second") - page_info = {"hasNextPage": False, "endCursor": None} - else: - nodes, page_info = thread_nodes(1, 100), {"hasNextPage": True, "endCursor": "C1"} - emit({"data": {"repository": {"pullRequest": {"reviewThreads": { - "totalCount": 101.5 if scenario == "threads-fractional" else (102 if scenario == "threads-total-drift" and has_cursor else 101), - "pageInfo": page_info, "nodes": nodes - }}}}}) - elif "/compare/" in joined: - if scenario == "lineage-mismatch": - emit({"status": "diverged", "behind_by": 1, - "merge_base_commit": {"sha": new_head}}) - else: - emit({"status": "ahead", "behind_by": 0, - "merge_base_commit": {"sha": base}}) - elif "/commits/" in joined and "/check-runs" in joined: - check_call = next_counter("GATE_CHECK_COUNTER") - if scenario == "check-run-wrong-head": - run_head = new_head - else: - run_head = head - second_id = 1 if scenario == "check-run-duplicate-id" else 2 - run_status = "queued" if scenario == "check-run-incomplete" else "completed" - conclusion = "failure" if scenario in {"check-run-failed", "late-check-run-failure"} and (scenario != "late-check-run-failure" or check_call > 0) else ("skipped" if scenario in {"check-run-required-skip", "check-run-optional-skip", "check-run-allowlisted-skip"} or (scenario == "late-check-run-unallowlisted-skip" and check_call > 0) else "success") - run_name = "Unreviewed final conditional job" if scenario == "late-check-run-unallowlisted-skip" and check_call > 0 else ("Native checks (windows-latest)" if scenario == "check-run-allowlisted-skip" else ("Optional changed after rollup" if scenario in {"check-run-failed", "check-run-optional-skip"} else "Required checks")) - rows = [ - {"id": 1, "name": run_name, "head_sha": run_head, "status": run_status, "conclusion": conclusion}, - {"id": second_id, "name": "Rust format", "head_sha": run_head, "status": "completed", "conclusion": "success"}] - for name in ("Required checks", "Frontend build", "GitGuardian Security Checks", "Dependency security"): - if not any(row["name"] == name for row in rows): - rows.append({"id": len(rows) + 1, "name": name, "head_sha": run_head, - "status": "completed", "conclusion": "success"}) - if scenario in {"final-required-statuses-pass", "final-status-context-disappears", "final-required-status-fails"}: - rows = [row for row in rows if row["name"] not in {"GitGuardian Security Checks", "Dependency security"}] - if check_call > 0: - if scenario == "final-contexts-empty": - rows = [] - elif scenario == "final-check-context-disappears": - rows = [row for row in rows if row["name"] != "Rust format"] - elif scenario in {"final-required-check-skipped", "final-required-check-neutral", "final-required-check-pending"}: - rows[0]["conclusion"] = scenario.rsplit("-", 1)[1] - if scenario == "final-required-check-pending": - rows[0].update(status="queued", conclusion=None) - total_count = len(rows) + (1 if scenario == "check-run-count-mismatch" else 0) - emit([{"total_count": total_count, "check_runs": rows}]) - elif "/commits/" in joined and "/status?" in joined: - status_call = next_counter("GATE_STATUS_COUNTER") - def status_page(rows, total_count, state="success", page_head=head): - return {"sha": page_head, "state": state, "total_count": total_count, "statuses": rows} - if scenario in {"final-required-statuses-pass", "final-status-context-disappears", "final-required-status-fails"}: - rows = [{"id": 1, "context": "GitGuardian Security Checks", "state": "success"}, - {"id": 2, "context": "Dependency security", "state": "success"}] - state = "success" - if status_call > 0 and scenario == "final-status-context-disappears": - rows = rows[1:] - elif status_call > 0 and scenario == "final-required-status-fails": - rows[0]["state"] = state = "failure" - emit([status_page([row], len(rows), state) for row in rows]) - elif scenario == "status-two-page": - emit([status_page([{"id": 1, "context": "legacy one", "state": "success"}], 2), - status_page([{"id": 2, "context": "legacy two", "state": "success"}], 2)]) - elif scenario == "status-truncated": - emit([{"sha": head, "state": "success", "total_count": 1, "statuses": []}]) - elif scenario == "status-duplicate": - row = {"id": 1, "context": "legacy one", "state": "success"} - emit([{"sha": head, "state": "success", "total_count": 2, "statuses": [row]}, - {"sha": head, "state": "success", "total_count": 2, "statuses": [row]}]) - elif scenario == "status-mixed-head": - emit([{"sha": head, "state": "success", "total_count": 2, "statuses": [{"id": 1, "context": "legacy one", "state": "success"}]}, - {"sha": new_head, "state": "success", "total_count": 2, "statuses": [{"id": 2, "context": "legacy two", "state": "success"}]}]) - elif scenario == "status-failed-context" or (scenario == "late-status-failure" and status_call > 0): - emit([{"sha": head, "state": "failure", "total_count": 1, "statuses": [{"id": 1, "context": "legacy optional", "state": "failure"}]}]) - elif scenario == "status-nonempty-pending": - emit([{"sha": head, "state": "pending", "total_count": 1, "statuses": [{"id": 1, "context": "queued", "state": "pending"}]}]) - elif scenario == "status-empty-pending": - emit([{"sha": head, "state": "pending", "total_count": 0, "statuses": []}]) - elif scenario == "status-malformed": - emit([{"sha": head, "state": "success", "total_count": "0", "statuses": []}]) - elif scenario == "status-failed-combined": - emit([{"sha": head, "state": "failure", "total_count": 1, "statuses": [{"id": 1, "context": "legacy failed", "state": "failure"}]}]) - else: - emit([{"sha": head, "state": "success", "total_count": 0, "statuses": []}]) - elif "/pulls/321/commits" in joined: - message = "safe commit metadata" - if scenario == "metadata-commit-id": - message = "Customer " + "ABCDE" + "1234" + "F" - elif scenario == "privacy-email-commit": - message = "Customer customer@company.test" - identity = {"name": "Maintainer", "email": "maintainer@example.invalid"} - if scenario == "metadata-author-id": - identity = {"name": "ABCDE" + "1234" + "F", "email": "maintainer@example.invalid"} - elif scenario == "privacy-email-author": - identity = {"name": "Maintainer", "email": "maintainer@company.test"} - linked_author = None if scenario == "metadata-unlinked-identities" else {"login": "author"} - linked_committer = None if scenario == "metadata-unlinked-identities" else {"login": "committer"} - commits = [{"sha": head, "commit": {"message": message, "author": identity, "committer": identity}, - "author": linked_author, "committer": linked_committer}] - if scenario == "metadata-duplicate": - commits.append({"sha": head, "commit": {"message": message}}) - emit([commits]) - elif any(arg.endswith("/pulls/321") for arg in args): - commits = 1 - metadata_head = head - if scenario == "metadata-capped": - commits = 251 - elif scenario in {"metadata-truncated", "metadata-duplicate"}: - commits = 2 - elif scenario == "metadata-head-mismatch": - metadata_head = new_head - emit({"commits": commits, "head": {"sha": metadata_head}, "user": {"login": "author"}}) - elif "branches/master/protection/required_status_checks" in joined: - contexts = [ - "Frontend build", "Rust format", "GitGuardian Security Checks", - "Dependency security", "Required checks"] - if scenario == "protection-missing-gitguardian": - contexts = [context for context in contexts if context != "GitGuardian Security Checks"] - if scenario == "required-context-output-bound": - contexts += ["control-" + str(i) + "-" + "z" * 10000 for i in range(50)] - if scenario == "required-context-unexpected": - contexts += ["Unexpected required context"] - emit({"strict": scenario != "protection-nonstrict", "contexts": contexts, "checks": []}) - elif "branches/master" in joined: - base_tip_call = next_counter("GATE_BASE_TIP_COUNTER") - if scenario == "final-base-tip-failure" and base_tip_call >= 2: - fail("controlled final base-tip read failure") - emit(new_head if scenario == "final-base-tip-moves" and base_tip_call >= 2 else base) - elif "/pulls/321/reviews" in joined: - if scenario in {"short-review", "summary-only", "manual-summary"}: - emit([[]]) - else: - records = [{"user": {"login": "chatgpt-codex-connector[bot]", "type": "Bot"}, "state": "COMMENTED", "commit_id": head}] - if security_case and not scenario.startswith("security-review-path-") and scenario not in {"security-review-missing", "security-camel-dsc", "security-camel-credential", "security-axal-frontend", "security-axal-native", "security-encrypted-keystore", "security-documents-consumer", "security-documents-consumer-rename-out", "security-documents-screen", "security-documents-screen-rename-out", "security-tauri-cargo", "security-tauri-cargo-rename-out", "security-tauri-cargo-lock", "security-tauri-cargo-lock-rename-out", "security-tauri-lib", "security-tauri-lib-rename-out", "security-commands-facade", "security-bank-statement-import", "security-prune-package-compiler-cache", "security-prune-package-compiler-cache-rename-out", "security-ci-workflow", "security-ci-workflow-rename-out", "security-release-preview", "security-release-preview-rename-out", "security-deploy-install-page", "security-deploy-install-page-rename-out"}: - record = {"user": {"login": "reviewer", "type": "User"}, "author_association": "COLLABORATOR", "state": "COMMENTED", "commit_id": head, "body": f"Security review: {head}\nResult: accepted\nReviewed credential handling: token diagnostics remain redacted.\nSecurity rationale: the current access boundary prevents a cache token from reaching logs."} - if scenario == "security-review-stale": record["commit_id"] = new_head - if scenario == "security-review-author": record["user"]["login"] = "author" - if scenario == "security-review-unrelated": record["body"] = "Looks good" - if scenario == "security-review-no-scope": record["body"] = f"Security review: {head}\nResult: accepted" - if scenario == "security-review-bare-scope": record["body"] = f"Security review: {head}\nResult: accepted\nReviewed credential:\nSecurity rationale: the current access boundary prevents a cache token from reaching logs." - if scenario == "security-review-placeholder-rationale": record["body"] = f"Security review: {head}\nResult: accepted\nReviewed credential handling: token diagnostics remain redacted.\nSecurity rationale: TBD" - if scenario == "security-review-hidden": record["body"] = f"" - if scenario == "security-review-hidden-unterminated": record["body"] = f"