Skip to content

diffgate: cut a diff where git does and abstain where the two readers still part (#205); a declared count is ASCII digits in both ports (#206) - #220

Open
fathomlab wants to merge 10 commits into
mainfrom
fix/diffgate-205-206
Open

fathomlab wants to merge 10 commits into
mainfrom
fix/diffgate-205-206

Conversation

@fathomlab

@fathomlab fathomlab commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

The Python diff gate cut a diff into lines with str.splitlines(). That call also breaks at a form feed and at seven more characters where git, CPython's tokenizer and the JavaScript port do not. A line +<U+000C>def test_a(): was cut in two and its definition dropped, so a true claim of one new test, and of the function test_a, read CONTRADICTED (#205). A declared count in a DECLARE-1 block was read through Python's Unicode \d, so a count in full-width or Arabic-Indic digits was checked by the Python and MALFORMED in the port, and which digits counted moved with the interpreter's Unicode version (#206).

Closes #205
Closes #206

What changes

#205, the cut (Python). _git_lines cuts every piece of git output where the port's _splitlines cuts it (CRLF, a lone CR, LF). Four sites use it: parse_unified_diff, parse_unified_diff_sides, and gate_diff's --name-status listing and its added lines, so both doors read one cut. On a text holding none of the eight characters git does not cut at, the result is str.splitlines() exactly. A lone CR stays a cut, as in the port; that one is #176's.

#205, the abstention (both ports). Once a line is whole, U+000B, U+001C to U+001F, U+0085, U+2028 and U+2029 still part the readers. CPython refuses each in source outside a string or a comment, and the two readers' \s, ^ and . read all but U+000B apart (on U+000B both read an indentation CPython refuses). Over an added line of any file holding one, tests_added and symbol_added are UNCHECKABLE in both ports, with one reason, the same on every runtime: "an added line holds U+XXXX, which CPython refuses in source outside a string or a comment; this template counts def lines and cannot tell where it sits (#205)". The compatibility reading, which was UNCHECKABLE already, reads no definition and says so in the same terms. Each reason says only what holds for every character of the set wherever it sits, and a test asks ast.parse on the running CPython for that; neither says the readers read the character apart, which is false for U+000B. U+001F is in the set because a form feed used to hide it. U+FEFF is not: the cut does not change how either reader reads it, and it heads line one of every file saved with a byte-order mark.

#205, the header patterns (port). _DIFF_GIT and _BINARY_LINE read . as [^\n], as the Python's . reads, so a header holding U+2028 registers its file in both ports.

#205, PATH-2a. No rule is added and DECIDE and APPLY are not edited. The overlay's model of the Python's line cut follows the new cut: line view 0 is the diff cut at git's line ends, so the two views are one object; the starts after U+2028 and U+2029 are read for the port's count only; the count-once shortcut is not taken on a text holding them. _p2a_divergent is kept as it was, as caution on raw-door text git never writes.

#206 (both ports). A declared files_changed or tests_added count is [0-9]{1,9}. A value holding a character from U+0080 up is MALFORMED: a count is written in the ASCII digits 0-9, and the value holds U+XXXX, naming the leftmost such code point with no repr() and no Unicode table, in both ports. An ASCII value keeps its old reason byte for byte.

Verdicts that move

  • The Python, a form feed in the indentation. Over an added line where a form feed leads a definition, leads its indentation or sits inside it (+<U+000C>def test_a():, the issue's class TestA: / +<U+000C> def test_a(self):, and + <U+000C>def test_a(self):): the one-test claim and the test_a claim go CONTRADICTED to VERIFIED, and the zero-test claim VERIFIED to CONTRADICTED, at both doors, in prose and declared, in both strict modes.
  • The Python, a form feed after def. Over +def<U+000C>foo():, the symbol claim naming foo goes CONTRADICTED to VERIFIED. The test count over +def<U+000C>test_a(): does not move; it is a residual, below.
  • The port, the issue's class shape. The port already read the lead, the in-indentation and the after-def shapes as the Python now does. It moves on the class shape, where the form feed comes before the indentation. There main's PATH-2a overlay withheld the port's own verdicts (diffgate: tests_added and symbol_added read only the added lines — a changed def verifies as "added" (a false VERIFIED) #101, again): its model of the Python's old cut read the piece after the form feed as an unchanged line defining the same name. The two mains now cut there alike, so in the port the one-test and test_a claims go UNCHECKABLE to VERIFIED (a --strict FAIL to PASS), and the zero-test claim UNCHECKABLE to CONTRADICTED (PASS to FAIL without --strict; FAIL before and after with it). No committed input has this shape, so the committed-input figures below do not show it; the new test module pins it in both ports.
  • A form-feed re-indent of an existing test or function is counted, as a space re-indent is, and withheld by PATH-2a (diffgate: tests_added and symbol_added read only the added lines — a changed def verifies as "added" (a false VERIFIED) #101) in both ports.
  • The abstention. tests_added and symbol_added over an added line of any file holding a character of the set: any verdict to UNCHECKABLE in both ports. That includes the port's VERIFIED after U+000B or U+2028 (diffgate: a definition CPython refuses still counts as added #177's shape, in added lines) and the old U+001F divergence. It also includes true claims beside such a character where CPython accepts it, since the templates cannot tell where it sits: a new def test_new(): beside one in a comment, in a string or in a line of docs/notes.md, for each of the eight characters, in prose and declared, is 48 inputs whose two claims were VERIFIED in both ports and are UNCHECKABLE in both now, a --strict PASS to FAIL. Those 48 are pinned as the abstention's cost. --strict fails on each new UNCHECKABLE.
  • Forged lines, whole headers, the git door's listing (the Python). A context line no longer forges a header, an added line or a removed one, a header path holding one of these characters is read whole, and the git door reads --name-status at git's line ends. These move records, and each verdict that moves goes to what the diff's added lines define:
  • diffgate: a DECLARE-1 count written in non-ASCII digits is read by the Python and MALFORMED in the port, and which digits read depends on the runtime's Unicode version #206, Python. A count in non-ASCII digits goes VERIFIED or CONTRADICTED to UNCHECKABLE. A false such count no longer FAILs by default; a true one, a right VERIFIED lost, FAILs under --strict. The preregistration's rule covers it: a value that does not parse is MALFORMED, reported, never an accusation. Port: the reason's text only. No committed text on main declares such a count: of the 51 files_changed: and tests_added: occurrences in its tracked files, read raw and through JSON and Python escapes, none has a value outside ASCII.

Measured

Against 62f8ff44, both ports, final records (overlay applied), raw door, on the 6,672 committed inputs the PATH-2a tests read:

  • Inputs whose two final claim lists are equal in (kind, verdict, reason): 6,038 to 6,113, none newly apart.
  • Inputs whose two gate verdicts differ: 23 to 9 without --strict, 13 to 10 with it.
  • Inputs where a port raises: 5 to 3. Before, the Python alone on 2, the port alone on 1 and both on 2; now both on the same 3. So on one input, p2a-fuzz:20260930:692, a record became a raise in the Python, where the port already raised: read whole, its <U+000B>--- .docs/x.md line is no --- line, so the +++ /dev/null after it has no old path, a shape git never writes.

These were measured on CPython 3.12.10 and Node 24.13.0, and again after the two #205 reasons were reworded, with the same result; no test asserts them.

On a 377-cell grid (every str.splitlines() break, every str.isspace() character and U+FEFF, in eight positions on a definition line, judged by ast.parse), pinned by the new test module:

  • Claims ast reads as true that a port CONTRADICTS: 10 to 6 in the Python, 6 to 6 in the port, none newly CONTRADICTED. The 6 left are the same in both ports: a lone CR (diffgate: a lone CR or a backslash continuation hides an added definition #176) in four, and a tab or a form feed between def and a test's name in two.
  • Claims ast reads as false that a port VERIFIES: 7 to 5 in the Python, 5 to 5 in the port, of the same two causes.
  • Cells whose two final claim lists differ: 52 to 8, all U+FEFF.

The port differential over main's corpora (the 176 real pairs rebuilt from their pinned shas, the 3,000 regenerated fuzz pairs, the pinned pairs) reads 3,230 pairs and 0 disagreements, and 190 pinned pairs with 0. styxx.charon verify over the committed log reads 257 lines MOVED_VERIFIER and none SAME_LINE, SKEW, DRIFT, UNRESOLVED, HEAD_MISMATCH or TAMPER, before and after main with #219 (identity phase 3) was merged into this branch; with #219's verifier all 257 read verifier=legacy. #219 does not touch the diff gate's reader, port or declaration module, so every figure above against 62f8ff44 holds against the merged main; nor does #218 (the zoo screen), merged into this branch next with the CHANGELOG kept as a union.

Pins, records and tests

  • The pin set moved as diffgate: CONTRADICTED is printed as CONTRADICTED with its reason, not as LIE #201 and diffgate: every record names the build and runtime that wrote it (identity phase 2) #217 moved it, and reading paths moved in both languages this time. py_side.py PINNED, the README and the port block's comment name the whole file, b0a65576…. MAIN_PY_SHA and the Python block's comment name the reader, f457d2e4…. MAIN_JS_SHA moves too, which no change since PATH-2a merged had done: d74cc729…. The bookmarklet is rebuilt with terser 5.46.0, which reproduces the previous build byte for byte: 4f46cfdf…, 54,335 characters.

  • path2a_pairs.json is not edited. A new issue205_moves.json, in path2a_moves.json's form and read after it, records the two pinned claims diffgate: a form feed before def on an added line hides the definition from the Python, not from the port #205 moved in both ports.

  • Re-pinned with the reason beside each: the abstention counts (the Python's decided claims 12,718 to 12,458, as main abstains on 260 itself), the port's decided and withheld, bar C's committed rows on CPython 3.12 and 3.14 in both path flavours, the inputs whose gates part only under the overlay (six to y2 alone), PORT_161, three cross-port cases, and the seeded line break set, whose premise is gone. The truth world does not move.

  • The truth module's plants of the old two-view model are replaced by their inverses. The counted tests' U+2028 starts can move no record now, so their plants are listed as equivalent and told by the lockstep.

  • Identity phase 2's content check against 295529c6 is narrowed by what diffgate: a form feed before def on an added line hides the definition from the Python, not from the port #205 moved and no more. It still compares every committed input. The 139 whose content moved, each holding a character diffgate: a form feed before def on an added line hides the definition from the Python, not from the port #205 reads anew, are pinned by their count and by a sha256 of their ids, the same set on CPython 3.12.10 and 3.14.2. The other 453 inputs holding such a character are still compared byte for byte.

  • The new module tests/test_diffgate_issue205_206.py holds:

    • the issues' reproductions at the raw door, at the git door (a real two-commit repository) and in the port, in both strict modes;
    • the grid;
    • the cut against str.splitlines() and against the port's, over every break, seeded, and the sites that cut;
    • the abstention set, its reasons and its reach in both ports, and a check that the reasons hold for every character of the set on the running CPython;
    • every code point from U+0080 to U+10FFFF in a declared count, so no runtime's digit table can reach the reader;
    • a plant of each change, each told by a named check.

    The port halves skip where node is absent.

Run here, CPython 3.12.10 and Node 24.13.0: the diff gate's test modules, the differential, port, declare, capsule, charon and stranger tests. The grid, the moved set of the identity check and the reasons' check were also run on CPython 3.14.2, with the modules staged under an empty package.

Not repaired, and named

So this does not say that the two ports read every declared count alike.

For the operator

  1. The five-file cap is exceeded, as identity phase 2's was; the excess is the pin cascade and its tests.
  2. The abstention set reaches past the form feed the issue names, and over every file of the diff. It is the smallest set that keeps the new cut from opening new disagreements; its cost is the 48 pinned inputs whose true claims are now withheld. Reading only Python files, or only code outside strings and comments, would narrow it; that is a design change, not made here.
  3. The two diffgate: a form feed before def on an added line hides the definition from the Python, not from the port #205 reasons are pinned by the moves file, the tests, both reconstructed shas and the bookmarklet, so rewording them after merge repeats this cascade.
  4. _p2a_divergent is kept, so right verdicts on malformed raw-door text stay withheld; dropping it is an operator option.
  5. diffgate: a DECLARE-1 count written in non-ASCII digits is read by the Python and MALFORMED in the port, and which digits read depends on the runtime's Unicode version #206 loses right VERIFIEDs on full-width counts, which CJK input methods produce.
  6. The rebuilt bookmarklet is one line, so it is an added line, and it carries main's existing DECLARE-1 reason for a duplicate key, whose wording uses an ordinal the lab does not print. Rewording it moves a pinned reason in both ports; it is left for a follow-up, not changed here.

Operator rulings (2026-10-13)

The operator accepted the defaults: the five-file cap is waived for this change (the excess is the pin cascade and its tests); the abstention set stays as built; _p2a_divergent is kept.

🤖 Generated with Claude Code

darkflobi and others added 10 commits October 12, 2026 19:32
…rt (#205); a declared count is ASCII digits (#206)

#205. The Python cut git output with str.splitlines(), which also breaks at
U+000B, U+000C, U+001C-U+001E, U+0085, U+2028 and U+2029, so a line led by a
form feed lost its definition and true test and symbol claims read
CONTRADICTED. It now cuts at CRLF, a lone CR and LF (_git_lines), as the port
does, at all four sites and both doors. In both ports the def counts and the
compatibility reading abstain over a line holding U+000B, U+001C-U+001F,
U+0085, U+2028 or U+2029, naming the character. The port reads the two header
patterns' dot as the Python does. The PATH-2a overlay's model of the line cut
follows the new cut; no rule is added.

#206. A declared files_changed or tests_added count is [0-9]{1,9} in both
ports; a value holding a character from U+0080 up is MALFORMED and the reason
names the leftmost such code point, so verdict and reason no longer depend on
the runtime's Unicode tables.

The pins move as #201 and #217 moved them, MAIN_JS_SHA and the bookmarklet
too (rebuilt with terser 5.46.0). path2a_pairs.json is not edited: its two
moved claims are in the new issue205_moves.json. Re-pinned with reasons: the
abstention counts, bar C, PORT_161, three cross-port cases, the line-break
seeded set. tests/test_diffgate_issue205_206.py holds the reproductions, a
319-cell grid judged by ast, the cut against str.splitlines() and the port,
every code point in a declared count, and a plant of each change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…siduals

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… after #205

Two of the 84 cases (C1-xl-unit-separator, C1-xl-count) are read alike by
main's two ports now: lists equal 49 -> 51, diff side 6 -> 4, gates apart
under the overlay alone without --strict 14 -> 12.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the set; the abstention's reach and a separator position pinned

Review of fix/diffgate-205-206, findings 2 and 3.

The two #205 reasons said each character is one "which CPython refuses in
source and the Python and JavaScript readers read apart". That is false for
U+000B, which both readers read alike, and false where the character sits in
a comment, a string or a file that is not Python, where CPython accepts it and
the abstention still fires. Both ports now write:

  an added line holds U+XXXX, which CPython refuses in source outside a
  string or a comment; this template counts `def` lines and cannot tell
  where it sits (#205)

and the compatibility reading the same in its own terms. No verdict moves: on
the 6,672 committed inputs, both ports, both strict modes, 264 records move
their reason's text and nothing else.

The pins move with the strings: the reader f457d2e4 (MAIN_PY_SHA), the port
d74cc729 (MAIN_JS_SHA), the whole file b0a65576 (py_side.py PINNED, the
README, the port block's comment), the `to` of issue205_moves.json, and the
bookmarklet, rebuilt with terser 5.46.0 on LF copies: 4f46cfdf, 54,335
characters; href e6913f66, 54,346.

Tests in tests/test_diffgate_issue205_206.py:
- the reasons hold for every member on the running CPython: ast.parse
  refuses it in eight code positions and accepts it in a comment, a string
  and a docstring;
- the abstention's reach, in both ports: 48 inputs whose two true claims sit
  beside a member in a comment, a string or another file, VERIFIED on main
  and UNCHECKABLE now (a --strict PASS to FAIL), pinned as its cost;
- the grid gains a separator position, def<c>test_n: 377 cells. A tab or a
  form feed there is a residual the tests template has always had (it reads
  `def test_` with one space), pinned with the lone CR: 6 true claims
  CONTRADICTED and 5 false claims VERIFIED, the same in both ports and on
  CPython 3.12.10 and 3.14.2. On main: 10 and 7 in the Python, 6 and 5 in
  the port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t again; the 139 inputs #205 moved are pinned

Review of fix/diffgate-205-206, note 4.

The check left out all 592 raw-door inputs whose diff holds a #205
character. Only 139 of them have any record moved against 295529c's
reader (a record, or the new raise on p2a-fuzz:20260930:692). The check
now runs every input: an input whose content moved must hold a #205
character, and the moved set is pinned by its count and by a sha256 of its
ids, the same set on CPython 3.12.10 and 3.14.2. The other 453 are compared
byte for byte again. A raise of this reader where the parent raised none is
read as content moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the abstention's reach and the residuals named

Review of fix/diffgate-205-206, findings 1, 2 and 3, notes 4, 5 and 7.

- The port did not "already read so" on the issue's class shape,
  `class TestA:` / `+<U+000C>    def test_a(self):`. main's overlay withheld
  the port's own verdicts there (#101, `again`), and they now stand:
  UNCHECKABLE to VERIFIED, and "Added 0 tests." UNCHECKABLE to CONTRADICTED,
  a default PASS to FAIL. The Python's move on `def<U+000C>foo()` is listed.
  No committed input has the class shape.
- The abstention's reasons are quoted as reworded; its any-file reach and
  its cost (48 inputs whose true claims are now withheld) are stated.
- The grid figures are for the eight-position grid. The tab and form-feed
  separator and the ASCII-control reason of a declared count join the
  residuals named.
- The raises on the committed inputs are broken down (5 to 3). The
  identity check's narrowing is restated. The declaration count names its
  method: 51 occurrences on 62f8ff4, none outside ASCII.
- The new pins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… became a raise, a named reason residual

The forged-line, whole-header and name-status bullet now lists what it moves in the Python:
the context-line reproduction and two fuzz inputs going PASS to FAIL by default, the two
binary headers' measured field, and the git door with core.quotePath=false. The raise on
p2a-fuzz:20260930:692 is named where the raise counts are. The reason gap for an ASCII count
holding a single quote and a backslash, on main too, joins the residuals with #182.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #219 (identity phase 3: charon's verifier names its runtime). The one conflict is
CHANGELOG.md, where both add an [Unreleased] section at the top; both are kept, this branch's
above phase 3's. charon verify on the merged tree reads 257 lines MOVED_VERIFIER, all of them
verifier=legacy, and none SAME_LINE, SKEW, DRIFT, UNRESOLVED, HEAD_MISMATCH or TAMPER.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nding with a separator, the two validators #206 names

The last review's two minors and one note, text only:
- a path holding U+0085, U+2028 or U+2029 under core.quotePath=false is read whole at both doors, and a false
  count there goes UNCHECKABLE to CONTRADICTED, a default PASS to FAIL, as the port already read it;
- the strip()/trim() residual covers a header path starting with the character as well as ending with it;
- _PATHY_RE and _IDENT_RE, which #206 names, are listed as not repaired, so they stay tracked once #206 closes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts resolved as unions: CHANGELOG.md  (both sides kept, this branch's above main's).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant