Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
99623f7
Rectify incomplete merge preflight evidence
Sep 13, 2026
76b02e9
Rectify merge gate evidence controls
Sep 13, 2026
925465c
fix: reject structural-only policy summaries and reseal workflow
Sep 13, 2026
ae12d1b
fix: fail closed on merge-gate review evidence
Sep 13, 2026
914ea74
Harden merge-gate review evidence
Sep 13, 2026
0f50605
Tighten platform evidence continuations
Sep 13, 2026
d3d5bd8
Bound platform evidence list continuations
Sep 13, 2026
2023ab4
Avoid literal home paths in gate fixtures
Sep 13, 2026
c5222cd
fix(gate): close review evidence gaps
Sep 13, 2026
91404d7
fix(gate): harden platform and credential checks
Sep 13, 2026
4fda1e2
test(gate): cover renamed sensitive paths
Sep 13, 2026
2c73c64
fix(gate): require affirmative platform evidence
Sep 13, 2026
6f958f2
test: avoid privacy scan fixture false positives
Sep 13, 2026
3cadc52
fix: require security review for bank import credentials
Sep 13, 2026
441a7ba
fix: require security review for cache token handling
Sep 13, 2026
204c4b5
fix: strengthen merge-gate security evidence
Sep 13, 2026
4dfe339
test: qualify workflow security fixtures
Sep 13, 2026
d16416d
fix: extend merge gate credential and privacy coverage
Sep 13, 2026
7a29762
fix: extend merge gate credential boundaries
Sep 13, 2026
5f5fd22
rectify: modularize merge gate privacy policy
Sep 14, 2026
78a3931
rectify: validate skipped CI jobs at final fence
Sep 14, 2026
c033dd4
rectify: review credential dependency lockfiles
Sep 14, 2026
57799cb
rectify: harden privacy and security review gates
Sep 14, 2026
f41d634
rectify: classify quoted credential assignments
Sep 14, 2026
92d151c
rectify: harden merge gate final fences
Sep 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ jobs:
workflow-consistency:
name: Workflow consistency
runs-on: ubuntu-latest
timeout-minutes: 10
# Allow the full suite and setup to finish on slower runners while retaining
# a bounded job timeout.
timeout-minutes: 20
permissions:
contents: read
steps:
Expand All @@ -138,6 +140,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
Expand Down
2 changes: 1 addition & 1 deletion docs/tally/compatibility/compatibility-matrix.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"schema_version": 1,
"bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e",
"compatibility_surface_sha256": "74ac4427cf03860e7d4a7005682cccaa3d2544366e87f50fbe53ee79584a076b",
"compatibility_surface_sha256": "8ac9e496dac7f3f620ff10e11a49c709e27c5cb740fa1d5e0bf532430e45050b",
"claims": [
{
"claim_id": "erp9-6-6-3-windows-education-xml-one-company",
Expand Down
4 changes: 2 additions & 2 deletions docs/tally/compatibility/compatibility-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"files": [
{
"path": ".github/workflows/ci.yml",
"sha256": "2d2220d0dc1942eab04034dbfd07f5ecf693cc7d5f47b1e31615fe17e4b2f140"
"sha256": "aeed5fab97de591ed8ad1cf8cf094eaaac5356d40adbd10158905ec19316fd2f"
},
{
"path": ".github/workflows/dependency-security.yml",
Expand Down Expand Up @@ -850,5 +850,5 @@
"sha256": "a8ac2714fecf51947f2822c8c46d7ce2e8602c732780ff60566a7771f0836f9a"
}
],
"manifest_sha256": "74ac4427cf03860e7d4a7005682cccaa3d2544366e87f50fbe53ee79584a076b"
"manifest_sha256": "8ac9e496dac7f3f620ff10e11a49c709e27c5cb740fa1d5e0bf532430e45050b"
}
1,587 changes: 1,587 additions & 0 deletions scripts/merge-gate.sh

Large diffs are not rendered by default.

931 changes: 931 additions & 0 deletions scripts/merge-gate.test.py

Large diffs are not rendered by default.

208 changes: 208 additions & 0 deletions scripts/merge_gate_diff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
#!/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
Comment thread
lamemustafa marked this conversation as resolved.
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
Comment thread
lamemustafa marked this conversation as resolved.
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
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)
Loading