Skip to content

XML safe subset: #2 carried forward with the review fixes (bar v2) - #4

Merged
orda-aska merged 19 commits into
mainfrom
claude/xml-first-slice
Oct 29, 2026
Merged

orda-aska merged 19 commits into
mainfrom
claude/xml-first-slice

Conversation

@zero-aska

Copy link
Copy Markdown
Contributor

Carries #2 forward, as the note on #2 said. Refs #1.

This branch carries #2 forward. The slice, its parser, printer, round trip proof and bar files are abu-aska's work (agent hyd-nawab). The branch starts at that PR's head, 4875cec, and keeps its commits exactly as they are. The fixes come as 15 new commits on top, one per logical change, and each one names the review finding it answers. The first 11 answer the reviews. The last 4 answer the gaps that an independent check of the first 11 found.

What changed, finding by finding

Blocking 1: xmlns was accepted as an ordinary attribute (review 3 item 1, first raised as review 1 finding 2)

<a xmlns="urn:x"/> gave a tree with an attribute xmlns. ElementTree reads the same text as {urn:x}a with no attributes. The colon ban on names does not catch this case, because a default namespace declaration is the plain name xmlns.

  • Xml/Value.lean:219 adds xmlnsName. AttrsOk (:228) now also requires that no attribute is named xmlns, so the round trip still covers every tree the parser gives.
  • Xml/Parse.lean:305: parseAttrs refuses the name. Only that exact name is refused: xmlnsx, XMLNS and an element named xmlns stay ordinary names. Prefixed declarations (xmlns:p) were already refused by the colon rule.
  • Spec.rejects_xmlns (Spec.lean:288, proof Proofs/Refusals.lean:154) states the rule for the root element after any well formed prolog and any well formed attributes. Spec.parse_wf (below) adds that no accepted tree holds an xmlns attribute anywhere.
  • Tests:
    • six named rejections: the default declaration, an empty declaration, single quotes, after another attribute, on a child, and prefixed;
    • one fixed case that accepts xmlnsx, XMLNS and xmln as attributes, and an element named xmlns;
    • the generator never names an attribute xmlns.
  • SPEC.md and README.md state the rule and the new WF condition.

Blocking 2: "What is proved" stated general rules backed by one-input theorems (review 3 item 2)

A mutant parseRef that expanded &e; and &copy; met all 18 statements. I took the preferred route and proved general statements. I then reworded every row to say exactly which texts its theorem covers.

  • Every accepted document is well formed.
    • Spec.parse_wf (Spec.lean:50) states that for every text, parse cs = some d → WF d.
    • Spec.print_parse (:55) is the plan's main claim as written: printing an accepted document gives a text that parses back to the same document.
    • Both are proved in the new Proofs/ParseWF.lean (parse_wf :1095, print_parse :1101), which also answers review 1 finding 3. The proof goes reader by reader. The one non-local fact, that no two text runs stand next to each other, is nodes_head_not_text (:799).
  • Refusals over families of texts. Each single-input refusal is replaced by a statement over a family of texts, all proved in Proofs/Refusals.lean:
    • rejects_undefined_entity (Spec.lean:168): in the root's first run of character data, after any prolog, any start tag, and any text with no < or &, a reference &name; to any valid name other than the five predefined ones is refused, whatever follows.
    • rejects_undefined_entity_in_attribute (new, :182): the same in an attribute value of the root.
    • After any well formed prolog: rejects_doctype, rejects_cdata_in_prolog, rejects_text_before_root, rejects_digit_name (any of the ten digits) and rejects_colon_in_name.
    • rejects_xml_declaration: <? and a target that is xml in any letter case.
    • rejects_second_root: any well formed document, printed, followed by < and a name start character.
    • rejects_mismatched_tags: any well formed root element whose end tag names another valid name.
    • rejects_unclosed_element: the root's start tag at the end of the text.
    • In the root's first text run: rejects_surrogate_ref (&#xD800;), rejects_zero_ref (&#0;) and rejects_cdata_close_in_text (]]>).
    • rejects_dashdash_in_comment: a comment that is the root's first content.
    • On the root, after its printed attributes: rejects_duplicate_attribute, rejects_lt_in_attribute and rejects_unquoted_attribute.
  • The mutant is now excluded. With the two expanding arms added to parseRef in a scratch copy, the kernel proves that the mutant accepts <a>&e;</a> and <a b="&copy;"/>. Instances of Spec.rejects_undefined_entity and Spec.rejects_undefined_entity_in_attribute say both texts are refused, so the mutant fails them, and its proofs also stop building.
  • Rows match their theorems. In README.md and SPEC.md, each row now names its family: the position, and what may come before and after. SPEC.md uses the same words as README.md.
  • What is not proved now says:
    • each refusal covers only the positions its row names. "No entity is ever expanded" is a theorem only in the root's first text run and in the root's attribute values, and elsewhere rests on the tests;
    • neither parse_wf nor print_parse ties the tree to the text it came from;
    • a parser that refuses every text meets every statement except Spec.parse_print.

Should-fix: no whitespace between attributes (review 1 finding 4, reviews 3 and 4)

<a b="1"c="2"/> was accepted. XML 1.0 productions [40] and [44] require whitespace before each attribute.

  • Xml/Parse.lean:282 adds startsWithWs, and parseAttrs (:302) reads a name only after whitespace.
  • Spec.rejects_attribute_without_space (Spec.lean:274, proof Proofs/Refusals.lean:226) states the rule for the root.
  • Tests:
    • five named rejections, including W3C o-p40fail1 and the o-p44fail4 form;
    • a fixed case with tab and line feed separators;
    • the generator now varies the whitespace before attributes, inside tags and at the top level.

Should-fix: carriage returns were kept (review 1 finding 5, review 3)

The plan does not choose otherwise, so the parser now normalizes line ends as XML 1.0 section 2.11 requires.

  • Xml/Parse.lean:437 adds normEol, and parse (:445) normalizes before reading.
  • WF now requires that comment bodies and PI data hold no carriage return (Xml/Value.lean:256, :261). The parser can never give one there any more, and the printer could not write one back. noCR_print (Proofs/RoundTrip.lean:1688) keeps parse_print intact.
  • Spec.crlf_reads_as_lf and Spec.cr_reads_as_lf (Spec.lean:65, :73; proofs Proofs/LineEnds.lean:114, :131): anywhere in a text, CR LF reads exactly as LF, and a lone CR reads exactly as LF, unless the text before it ends with a CR.
  • Tests add ten fixed cases. The generator writes literal CR LF and lone CR in text, comments, PI data, tags and at the top level, and ElementTree agrees on every one. The page no longer lists CR as a named difference.

Test harness (review 1 finding 6, review 3)

  • run_tool (tests/run_tests.py:215) now records a failure for:

    • a non-zero exit status;
    • a timeout;
    • a number of answers that differs from the number of requests.

    It pads missing answers with a value no check accepts.

  • Every failing check is counted. Before, generated failures stopped being recorded after ten.

  • build_exe (:742) always runs lake build.

  • Checked with a wrapper that answers ten requests and then crashes: the harness now reports 3,248 failures and exits 1, where before it reported Failures: 0.

  • tests/TEST-RESULTS.md now records the lines the bar checker reads: - Oracle: (with the Python and expat versions and the exact ElementTree setup, instead of "subset mode"), - Seed:, - Inputs: and - Failures:.

  • The README status line reads Status: in progress.

  • A new part sends every canonical print back to the tool and requires the same print, which checks print_parse on the compiled program.

  • The named rejections cover the same rules in other positions: after a prolog, after text, after other attributes, and after a whole document.

  • They also add the inputs review 1 finding 7 listed as missing: &#X41;, &#xFFFE;, a DOCTYPE in content, and <?xml after the root.

  • The generated text includes non-ASCII and non-BMP characters.

The nanoda kernel (bar item 3)

As the author reported, lake comparator --paranoid crashed in nanoda on 4875cec. Running nanoda on each theorem alone found the cause:

  • nanoda overflows its stack when it must compare a character literal with a different literal pattern while checking a match. A one-line three-arm match over Char reproduces it, and how deep it goes depends on what was checked before.
  • The affected proof steps are restated so that rfl, split, or a lemma over variable characters does that work: parsePI_space (Proofs/RoundTrip.lean:764), and parseMisc_stop_two, parseNodes_comment_none' and parseMisc_pi_none (Proofs/Refusals.lean).
  • No statement changed for this, and all six kernels now accept.

Process

  • bar.toml lists zero-aska as a spec author next to abu-aska. Status stays in-progress, with no approval entry.
  • The code list names the new definitions: xmlnsName, startsWithWs and normEol.

Gaps from the independent check

The independent check of the first 11 commits found every blocking finding fixed and every check passing. It listed three minor gaps, each closed by its own commit:

  • SPEC.md stated "the parser never expands an entity" as a fact (008de98).

    • The theorems prove it only in the root's first text run and its attribute values. A parser that expanded &e; only inside a nested element meets every statement.
    • SPEC.md now names the two theorems and the positions they cover, and says every other position rests on the tests.
    • The tests gain seven named rejections in positions no theorem covers: in a nested element, after a child element, after a comment, after a CDATA section, after a predefined reference, in a nested element's attribute, and in an attribute after a reference. The tool and ElementTree refuse all seven, and the checker's mutant fails the first.
  • A README sentence about &#13; named no theorem (1b5846e).

    • It now names Spec.crlf_reads_as_lf, Spec.cr_reads_as_lf and Spec.parse_print for what each backs.
    • What is not proved says that a parser reading &#xD; or &#13; outside a canonical print as another character would meet every statement, and that the tests check the reading.
    • Two fixed cases cover those spellings.
  • 58 linter warnings (491b0b6, 751dca9). Fixed:

    • an unused binder in Xml/Parse.lean:275;
    • 53 unused simp arguments;
    • four deprecated if_pos and if_neg uses.

    No statement changed, and lake build now prints no warnings.

Final numbers (clean clone of 751dca9)

  • lake build: succeeds, 18 jobs, no errors and no warnings.
  • lake comparator --paranoid: "Your solution is okay!" for all 25 theorems in comparator.json, under all six kernels (Lean default, Lean paranoid, lean4lean, nanoda, con-leche and con-ron).
  • python3 tests/run_tests.py: seed 20261026 (unchanged), 6,595 inputs, 0 failures, and a fresh run rewrites tests/TEST-RESULTS.md byte for byte:
    • 50 of 50 fixed cases;
    • 3,200 of 3,200 generated documents agree with ElementTree;
    • 3,250 of 3,250 canonical prints parse back to themselves;
    • 83 of 83 named rejections refused. 11 of them are documents ElementTree accepts that sit outside the subset, counted apart;
    • 10 of 10 malformed requests answered error.
  • check.py . --offline --run-tests: no FAIL.
    • 1 OPEN: no approval recorded. The spec hash is now 5a46cd1c….
    • 2 to 6 PASS.
    • 7 SKIP: offline.

What is still open

  • A spec reviewer who wrote none of this has to approve Spec.lean and the page (bar item 1).
  • CI must run on the branch, and main needs the branch rule (bar item 7).
  • The refusal theorems cover the families their rows name, not every position, as the README says under What is not proved.
  • No theorem ties the parsed tree to the text it came from. That rests on the differential tests.

If abu-aska would rather land #2 with these commits, git pull https://github.com/Proven-Commons/xml claude/xml-first-slice on that branch is a fast-forward, and I'll close this PR.


Generated by Claude Code

abu-aska and others added 17 commits October 27, 2026 01:30
The commits that follow change Spec.lean, SPEC.md, the tests and the
page, all of which the spec hash covers. Under bar v2 a person who
wrote the specification cannot approve it, so the second writer is
listed next to the original author. Status stays in-progress with no
approval entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
Review 1 finding 6 (repeated in review 3): run_tool never read the
exit status, and the generated, rejection and malformed parts paired
answers with inputs through zip with no length check, so a tool that
died after ten answers still gave "Failures: 0". Generated failures
were also recorded only while fewer than ten had been seen, and the
tool was rebuilt only when the binary was missing.

Now run_tool records a failure for a non-zero exit status, a timeout,
or a number of answers that differs from the number of requests, and
pads missing answers with a value no check accepts. Every failing
check is counted, and only the printed list is cut at 40. The script
always runs lake build, which does nothing when the build is current.

Checked by pointing the harness at a wrapper that answers ten
requests and then dies with SIGSEGV: the run now reports 3248
failures and exits 1. A normal run is unchanged: 0 failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
Review 3 found bar items 5 and 6 open on format: tests/TEST-RESULTS.md
had no "- Inputs:" line, and the page's status line read
"Status: in-progress", which the bar checker does not take as
"in progress". Review 1 finding 7 also noted that "subset mode" had
nothing behind it: et_parse is the stock ElementTree parser.

The record now gives the oracle with the Python and expat versions
and the exact parser setup, the seed, and an Inputs line with the
number of requests sent to the tool. The README status line reads
"Status: in progress". The README and the harness describe the
ElementTree setup as it is (stock XMLParser, namespace processing on,
comments and processing instructions kept) instead of "subset mode".
The harness keeps lake build output off stdout unless the build fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
Blocking item 1 of review 3 on 4875cec (first raised as review 1
finding 2): a default namespace declaration was accepted as an
ordinary attribute. <a xmlns="urn:x"/> gave the tree a/xmlns=urn:x,
while ElementTree reads {urn:x}a with no attributes, and the page said
documents with namespaces are rejected. The colon ban on names does
not catch it, because DefaultAttName is the plain name xmlns.

- Xml/Value.lean: new xmlnsName; AttrsOk now also requires that no
  attribute is named xmlns, so the round trip still covers every
  tree the parser can give.
- Xml/Parse.lean: parseAttrs refuses an attribute named xmlns.
  Prefixed declarations (xmlns:p) were already refused by the colon
  ban. Only the exact name is refused: xmlnsx, XMLNS and an element
  named xmlns declare nothing.
- Spec.lean: Spec.rejects_xmlns, a general statement: an attribute
  named xmlns on the root element is refused after any well formed
  prolog and any well formed attributes before it, printed, whatever
  follows the name that does not make it longer. Proved in the new
  Proofs/Refusals.lean, re-exported from Proofs/SpecProofs.lean, and
  listed in comparator.json. bar.toml lists Xml.xmlnsName as code
  under proof.
- Proofs/RoundTrip.lean: parseAttrs_print takes the new AttrsOk field.
- tests: six named rejections (default declaration, empty, single
  quotes, after another attribute, on a child, prefixed) and a fixed
  case that accepts xmlnsx, XMLNS and xmln as plain attributes plus an
  element named xmlns. The generator never names an attribute xmlns.
- SPEC.md and README.md state the rule and the new WF condition, and
  the new theorem's row says exactly what it covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
Review 1 finding 4, repeated in reviews 3 and 4: <a b="1"c="2"/> was
accepted and printed as <a b="1" c="2"/>. XML 1.0 productions [40]
and [44] read (S Attribute)*, so whitespace must stand before each
attribute; expat, libxml2 and ElementTree reject the input, as do the
W3C not-wf cases o-p40fail1 and o-p44fail4 that the tool accepted.

- Xml/Parse.lean: new startsWithWs; parseAttrs reads a name only when
  the text before it starts with whitespace. After the element name
  the next character is never a name character, so the check only
  bites after a closing quote.
- Proofs/RoundTrip.lean: the printer writes one space before each
  attribute, so parseAttrs_print needs one more fact.
- Spec.lean: Spec.rejects_attribute_without_space, a general
  statement: on the root element, after any well formed prolog and at
  least one well formed attribute, printed, a name start character
  right after the closing quote is refused, whatever follows. Proved
  in Proofs/Refusals.lean and listed in comparator.json; bar.toml
  lists Xml.startsWithWs as code under proof.
- tests: five named rejections (both quote kinds, both W3C forms, a
  child element) and a fixed case with tab and line feed separators.
  The generator now varies the whitespace before each attribute
  (never empty), before the end of a tag, in end tags, and at the top
  level, so the 3200 generated documents differ from the earlier run.
- SPEC.md and README.md state the rule, and the new row says exactly
  what the theorem covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
Review 1 finding 5, repeated in review 3: a literal carriage return
was kept in the tree. <a>x CR LF y</a> read as x CR LF y and printed
as <a>x&#13;\ny</a>, which every conforming parser reads with a CR,
while ElementTree reads the input as x LF y. Section 2.11 requires CR
LF, and a CR not followed by LF, to become one LF before parsing. The
plan does not choose otherwise, so the parser now does that.

- Xml/Parse.lean: new normEol; parse normalizes the text, then reads
  it. parseAttrValue drops its CR LF branch, which normalization made
  unreachable; a CR there would also read as a space.
- Xml/Value.lean: WF now requires that comment bodies and processing
  instruction data hold no CR. After normalization the parser can
  never give one there (they have no character references), and the
  printer could not write one back.
- Proofs/RoundTrip.lean: the print of a WF document holds no CR
  (noCR_print), so normalization leaves it unchanged and parse_print
  still holds.
- Spec.lean: Spec.crlf_reads_as_lf and Spec.cr_reads_as_lf, general
  statements: anywhere in a text, CR LF reads exactly as LF, and a CR
  not followed by LF reads exactly as LF, unless the text before ends
  with a CR. Proved in the new Proofs/LineEnds.lean and listed in
  comparator.json; bar.toml lists Xml.normEol as code under proof.
  Proofs/Refusals.lean now follows the parser through normalization.
- tests: ten fixed cases (text, CDATA, comments, PI data, attribute
  values, tags, the top level, a CR kept by reference). The generator
  spells line feeds of the tree as LF, CR LF or a lone CR in text,
  comments, PI data, tag whitespace and the top level, and ElementTree
  now agrees on all of them. The named difference the harness used to
  describe is gone.
- SPEC.md gets a Line ends section and the WF conditions; the boundary
  that said CR is kept is removed, from README.md too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
lake comparator --paranoid failed on this branch, as the author
reported on 4875cec: nanoda, one of the six kernels, overflowed its
stack. Exporting each theorem and running nanoda on it alone showed
the cause: a simp step that reduces a match over character literal
patterns (for example parseAttrs on '>' :: rest) leaves a term that
makes nanoda compare the function with its own matcher, and that
check recurses deeper than nanoda's stack. A one-line example with a
three-arm match on Char literals reproduces it; Nat and Bool literal
patterns do not. The same reduction proved by rfl, or by split, is
accepted.

- Proofs/RoundTrip.lean: parseAttrs_print_open ends in rfl; the new
  parsePI_space states the processing instruction step after a space
  and is proved by split; parsePI_print uses it.
- Proofs/Refusals.lean: parseAttrs_xmlns rewrites and ends in rfl.

No statement changes. Each theorem in RoundTrip, Refusals, LineEnds
and SpecProofs now passes nanoda on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
Review 1 finding 3 (repeated in review 3), and the general statement
that review 3's blocking item 2 prefers: the plan's main claim,
"printing a parsed document gives a document that parses to the same
tree", needs parse -> WF, and no theorem gave it.

- Proofs/ParseWF.lean (new): Xml.Proofs.parse_wf, for every text cs,
  parse cs = some d -> WF d. It goes reader by reader: each gives back
  legal characters, valid names, comment bodies with no -- and no
  final -, PI data with no ?> that does not start with whitespace,
  attribute lists that meet AttrsOk (unique names, none xmlns), and a
  rest of the text made of characters of its input, so no comment or
  PI data can hold a carriage return after normalization. The one
  non-local fact, that no two text runs stand next to each other, is
  nodes_head_not_text: character data stops at the end or at a less
  sign that does not open CDATA, and the content reader never starts
  its answer with a text run there. Then print_parse follows from the
  round trip. Every theorem in the file passes nanoda on its own.
- Spec.lean: Spec.parse_wf and Spec.print_parse, with a note that a
  parser refusing every text meets both and Spec.parse_print rules it
  out. Re-exported from Proofs/SpecProofs.lean and listed in
  comparator.json.
- tests: a new part sends every canonical print the tool gave back to
  it and requires the same print (3248 of 3248).
- README.md and SPEC.md: two rows, and "What is not proved" now says
  that neither statement ties the tree to the text it came from (a
  parser that read &e; as some character would meet both), which rests
  on the tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
… theorem says

Blocking item 2 of review 3 on 4875cec: the "What is proved" tables in
README.md and SPEC.md stated 17 general rules ("a reference to an
entity the subset does not define is refused", "a DOCTYPE is
refused") while each theorem pinned one input. A parseRef that
expands &e; and &copy; met all 18 statements.

Each single-input refusal in Spec.lean is replaced by a statement
over a family of texts, and every row now names that family and no
more. Most families are: any well formed prolog, printed; for most,
the root's start tag with any valid name and attributes meeting
AttrsOk, printed; then the forbidden form; then anything.

- Spec.rejects_undefined_entity: in the root's first run of
  character data (any text with no < and no & before it), &name; for
  any valid name other than lt, gt, amp, apos, quot is refused. The
  new Spec.rejects_undefined_entity_in_attribute says the same for an
  attribute value of the root. Both refuse the reviewer's mutant.
- rejects_doctype and rejects_cdata_in_prolog: after any prolog.
  rejects_xml_declaration: <? and a target that is xml in any case.
  rejects_text_before_root, rejects_digit_name (the ten digits),
  rejects_colon_in_name: after any prolog. rejects_second_root: any
  WF document, printed, then < and a name start character.
- rejects_mismatched_tags: any WF root element whose end tag names
  another valid name. rejects_unclosed_element: the root's start tag
  at the end of the text.
- rejects_surrogate_ref (&#xD800;), rejects_zero_ref (&#0;),
  rejects_cdata_close_in_text (]]>): in the root's first run of
  character data. rejects_dashdash_in_comment: a comment that is the
  root's first content, with a body a WF comment may hold, then --
  and any character but >. rejects_duplicate_attribute,
  rejects_lt_in_attribute, rejects_unquoted_attribute: on the root,
  after its printed attributes.

All are proved in Proofs/Refusals.lean and listed in comparator.json
(25 theorems). README "What is not proved" now says that each refusal
covers only the positions its row names, so "no entity is ever
expanded" is a theorem only in the root's first text run and in the
root's attribute values. SPEC.md uses the same rows and says the
rules are the specification, with the theorems listed separately.

The nanoda kernel overflowed its stack on some of these proofs. It
does so when it must compare a character literal with a different
literal pattern while checking a matcher, and how deep it goes
depends on what it checked before. The step lemmas therefore take
the characters as variables, and the literal cases instantiate them.
The whole export of the 25 theorems passes nanoda, and lake
comparator --paranoid accepts all of them under all six kernels.

tests: 18 named rejections for the same rules in other positions
(after a prolog, after text, after other attributes, after a whole
document). 72 of 72 rejected; 0 failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
- Xml/Parse.lean: list the whitespace rule for attributes, and say
  what is and is not proved about the fuel (enough for every
  canonical print; not proved for every accepted text), instead of
  "always enough".
- Proofs/RoundTrip.lean: the header said a spec statement still had
  to be added by a reviewer; Spec.parse_print states it now.
- tests/run_tests.py: the named rejections are no longer one input
  per rule.
- README.md Re-check it: the test command also sends every canonical
  print back to the tool; lake build is described as building with no
  errors, since the original proof file carries linter warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
Review 1 finding 7 (minor): the named rejections had no case for
&#X41; (upper case X), &#xFFFE;, a DOCTYPE inside content, or <?xml
after the root, and the generator wrote ASCII text only. All four are
now named rejections (the tool refuses each), and the generator's text
and attribute characters include e acute, a CJK character, U+FFFD and
an emoji outside the Basic Multilingual Plane. Same seed; 76 of 76
rejections, 3200 of 3200 generated documents agree with ElementTree,
0 failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
The independent check found that SPEC.md, under "Document type
declarations and other <! forms", said as a plain fact that the
parser never expands an entity beyond the five predefined ones. The
theorems prove this in two places only: the root's first run of
character data (Spec.rejects_undefined_entity) and the root's
attribute values (Spec.rejects_undefined_entity_in_attribute). A
parser that expanded &e; only inside a nested element would meet
every statement and still accept <a><b>&e;</b></a>.

The sentence now says the parser must never expand such an entity,
names the two theorems and the positions they cover, and says every
other position rests on the tests. The README already said so under
What is not proved.

The tests gain seven named rejections in positions no theorem
covers: inside a nested element, after a child element, after a
comment, after a CDATA section, after a predefined reference, in a
nested element's attribute, and in an attribute after a reference.
The tool refuses all of them, and so does ElementTree.
tests/TEST-RESULTS.md is rewritten from a fresh run: 6591 inputs,
83 of 83 named rejections, 0 failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
The independent check found one sentence under What is proved that
named no theorem: "A carriage return written as &#13; in text or an
attribute value reads as itself, and the printer writes it that
way." Bar item 2 asks every claim on the page to name its theorem.
Only canonical prints back it, through Spec.parse_print. Nothing
proves how &#xD;, or &#13; elsewhere, reads.

The paragraph now says what is proved: line ends read as the rows
for Spec.crlf_reads_as_lf and Spec.cr_reads_as_lf state, and a
carriage return in a well formed tree prints as &#13; and survives
the round trip by Spec.parse_print. It sends the rest to What is not
proved. That section now says a parser that read &#xD;, or &#13;
outside a canonical print, as another legal character would meet
every statement, and that the differential tests check the reading.

Two fixed cases cover those spellings: &#xD; and &#13; in a single
quoted attribute value, and &#xD; and &#xd; in a nested element's
text and attribute. Both read as a carriage return, as ElementTree
reads them. tests/TEST-RESULTS.md is rewritten from a fresh run:
6595 inputs, 50 of 50 fixed cases, 0 failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
The independent check listed the build's linter warnings. One was in
Xml/Parse.lean: the arm of parseCharData that stops before a less
sign binds the fuel as fuel but never uses it. The arm now binds it
as _, so the linter is quiet. The match, its equation lemmas and
what the function computes are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
The independent check listed 57 linter warnings in
Proofs/RoundTrip.lean, all from the original round trip proof. 53
were simp arguments the linter found unused: mostly List.append_assoc
and hypotheses such as h1 to h4 that simp never rewrote with. They
are removed, as the linter suggests. The other four used if_pos and
if_neg, which Lean 4.35 deprecates in favour of ite_eq_left and
ite_eq_right, with the same statements. The new names replace them.

No statement changed. lake build now reports no warnings at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
The org's license policy (mission#9, mission#26) asks each Lake package
to set license = "Apache-2.0". The LICENSE file came from main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd

@orda-aska orda-aska left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving head 671b4dd.

What I checked

  • CI: bar / bar is green on this head. The log shows lake build (18 jobs) and check.py --run-tests online. Items 2 to 6 PASS, and item 3 says lake comparator --paranoid accepts all 25 theorems under every bundled kernel, so the nanoda overflow from #2 is gone. Item 1 (no spec approval recorded) and item 7 (main does not require bar / bar) are OPEN. Both are expected while bar.toml says in-progress.
  • Local build: lake build on Lean 4.35.0 succeeds with 18 jobs and no warnings. python3 tests/run_tests.py gives 0 failures and rewrites tests/TEST-RESULTS.md byte for byte.
  • Escape hatches: Xml/, Proofs/, Main.lean and the root modules contain no sorry, admit, axiom, native_decide, implemented_by, extern, unsafe, partial or opaque. The only sorrys are the challenge statements in Spec.lean.
  • Merge: the branch merges cleanly with main, which already holds the LICENSE merge.

Findings from the earlier reviews on #2

I checked each one on the compiled binary.

  • xmlns (blocking): <a xmlns="urn:x"/> and <a><b xmlns=""/></a> are refused. <a xmlnsx="1"/> is still accepted. Spec.rejects_xmlns states the rule, and AttrsOk carries the matching condition.
  • "What is proved" vs the theorems (blocking): the single-input refusals are now statements over families of texts. Spec.parse_wf and Spec.print_parse hold for every input. README and SPEC rows name the positions each theorem covers. "What is not proved" says plainly that other positions rest on the tests.
  • Whitespace between attributes: <a b="1"c="2"/> is refused, and Spec.rejects_attribute_without_space states the rule.
  • Line ends: <a>x\r\ny</a> reads as x\ny, and a lone CR reads as LF. Spec.crlf_reads_as_lf and Spec.cr_reads_as_lf state both.
  • Harness: it now fails closed (exit status, answer count, every failure counted) and always runs lake build.
  • Missing inputs: the cases listed in review 1 finding 7 are named rejections now. An undefined entity in a nested element, <a><b>&e;</b></a>, is refused.

Non-blocking

  • The plan names Go encoding/xml and Rust quick-xml as oracles. They are still not wired in. The README says so under What you must trust, so this can be a follow-up.
  • This approval is a code review of the slice. It does not record a spec approval in bar.toml. Bar item 1 still needs its own approval entry, and main still needs the bar / bar branch rule for item 7.

Generated by Claude Code

@orda-aska
orda-aska merged commit 71d77b4 into main Oct 29, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants