Repository navigation
Conversation
b8cc564 to
6d95a6b
Compare
Code review against #143 phase 1Reviewed at head Summary. The PR delivers items 1, 2, 3 and 5 and most of it is ready. It does not yet complete phase 1: item 4 is absent,
BlockingB1. Item 4 (
|
| Query | Hits |
|---|---|
hahn-banach |
1 |
Hahn–Banach, |
0 |
"non-ambiguous" |
0 |
Suggested fix: always use the stripped word, and fall back to the original only when stripping leaves nothing. Add a test with a trailing comma on a hyphenated word.
Titles and ancestor titles are matched as Markdown source. search.py:380 and :384 normalize node.title directly, while the statement goes through visible_prose. For a title Non-*ambiguous* map at café [linked](http://urlword.example):
| Query | Result |
|---|---|
non-ambiguous |
not found |
café |
not found |
eacute, urlword |
hit on title |
The issue asks for prose fields to be matched on rendered text, and title is the top-ranked field. Suggested fix: pass titles through visible_prose.
B4. The audit can lose missing-depends-section when the graph drops the edge
audit.py:260-263 now derives the section list from article_parts, while graph._parse_node (graph.py:384) keeps its own comment and fence rules. They disagree on malformed input. Inserting this before ## Depends on in the example's non-ambiguity.md:
```html
<!-- an example of markup
```
makes the graph drop the dependency edge on both main and this PR. main's audit reports missing-depends-section for the file; this PR's audit reports nothing. It needs an unclosed comment inside a fence, so it is rare, but it removes the only finding for a silently lost edge.
Suggested fix: have graph._parse_node take its sections from article_parts, which also makes "one definition" true for the loader. Otherwise keep the audit's depends check on the graph's reading and note the limit.
Should fix
- S1. Query stemming changes the issue's ordering and is asymmetric. With a title "Weak topology" and a statement "Several topologies are compared here",
topologieslists the statement match above the title match, andtopologydoes not find the second article.indicesis searched asindic(matches "indicator", misses "Index");mappingasmapp(misses "map"). The issue did not ask for stemming. I would remove_stem,_WORD_ENDINGSand the exact-first sort key, and keep the README's "retry with fewer words" advice. If it stays, the description should say ordering departs from the issue in two ways, not one. - S2. The skill prose test guards wording, not the rule (
tests/test_skill_examples.py:861). Deleting Roadmap's "link to it ... instead of adding a node" instruction leaves it passing, while rewording "Matching is literal, so" fails it. Assert the semantic anchors (## Depends onnear the search rule,missing prerequisite, the contract link) and drop the incidental phrases. - S3. The read-once test counts search's own wrapper (
tests/test_search.py:449). Counting real reads gives three per article per search (load_graph,build_runtime_graph, and search's reread). Count real reads and assert the actual number, or rename the test. The reverse-edge counter is still absent, as the description says. - S4. Description corrections.
- The hyphen change (
markdown.py:101) also acceptsTBD-later,TODO-choose a milestoneandpending-reviewas coverage evidence, not onlyUnknown-variance case. - The
RecursionErrorfix coversrendered_visible_textonly;_collect_tablesand_visible_rows(markdown.py:352,:386) still recurse, sopublished_tables("<div>" * 1500 + "x")still raises.
- The hyphen change (
- S5. No skill mentions
--skeletonor tells an agent how to read its flag, so the feature has no caller in the product. This resolves itself if commit 5 moves out.
Minor
ℝandℕfold torandn, which match nearly every article. Worth a README warning or a refusal of one-letter terms.--declaration bogusreturns "No matching articles" at exit 0. Echoing the active filters and searched terms in the human footer would make a typo visible.- Lean target resolution in search repeats the runtime's logic without its "escapes the Lean root" guard. I found no triggering input; reusing the helper keeps one enforcement point.
- Commit 3 emits a different
autoform-search/v1than commit 5. Squash on merge, or fold the two keys into commit 3. markdown.HTML_COMMENTandmarkdown.FENCEremain in__all__with no importer.- The README's
filterssub-keys are not named, andmathlib_declarationsis missing from the description's list of extra keys.
What holds up
article_partsandvisible_proseare a clean seam, and audit and render lose their hand-rolled parsers. The example site renders byte-identically tomainand gains no audit finding.- The converter-cache fix is a real bug with a regression test.
- Search is deterministic, read-only, starts no process, refuses symlinked entries and changed bytes, and escapes article text in human output. I found no query input that produces a traceback.
- Matching
leanandnode_idon the last component, withqualified_namesranked last, is a real improvement on the issue's field list. - The skills link to the contract and restate no flag, and the description's claims about Bind statement: formalized to a recorded statement_hash #141 and Keep implementation notes separate from mathematical articles #172 compatibility check out.
Suggested path
- Fix B3 and B4, and remove the stemmer (S1).
- Retitle and add the "Not in this PR" section (B1); correct the description (S4).
- Tighten the two tests (S2, S3).
- Move commit 5 to a follow-up after Bind statement: formalized to a recorded statement_hash #141, or apply option 2 under B2 with the maintainer's agreement.
With 1 to 3 done, commits 1 to 4 are ready to merge as items 1, 2, 3 (without --skeleton) and 5.
faee1c2 to
a159b8a
Compare
Second review against #143 phase 1Reviewed at head Summary. The rewrite resolves all four blocking findings of the first review, and the PR's scope statement is now accurate: it delivers items 1, 2, 3 (without Status of the first review
Phase 1 acceptance criteria12 met, 1 partly met and declared (the read-once and reverse-edge counter test), 2 deferred and declared ( BlockingN1. The graph drops edges that
|
Dependencies of non-ambiguity |
Audit finding for the file | Link on the rendered page | |
|---|---|---|---|
main |
eligibility |
none | shown |
| this PR | none | none | shown |
The description says a fence "closes only on a bare fence line now, as on the page". The site renderer treats a fence that never closes as no fence, so the page still shows the link and the graph has lost the edge with no diagnostic. The "Known limits" entry covers only the case where the audit reports missing-depends-section, which needs the fence to sit above the heading on a formalizable article.
Suggested fix, either of:
- Treat a fence still open at the end of the article as not a fence, as the renderer does.
- Keep the rule and have the audit report an unclosed fence or comment on every article.
Add a graph test for the fence-close rule in both cases; tests/test_graph.py covers only the comment-inside-fence case.
N2. The "as typed" tier outranks field rank and pushes the canonical article past the limit
search.py:221-226 sorts articles holding every word as typed above all others, before the field rank. With a chapter titled "Separating hyperplanes" holding 25 unrelated articles, and an article titled "Hyperplane separation theorem" elsewhere:
| Query | Total matches | Position of "Hyperplane separation theorem" |
|---|---|---|
hyperplanes |
28 | 28th; absent from the default 20 |
The 25 unrelated articles match only through ancestors and all precede a title match. An agent reading the first page concludes the result is new, which is the outcome search exists to prevent.
Suggested fix: apply the as-typed tie-break inside a field rank, not above it. Removing the stemmer (S1 of the first review) also removes the tier.
N3. Search reports no match for results that exist
All cases exit 0 with an empty result. The statement used is "The family of inequalities on boundaries uses `Nat.succ` and f(x)." under the title "Weak topologies".
| Query | Terms searched | Hits |
|---|---|---|
`Nat.succ` |
`nat.succ` |
0 |
Nat.succ |
nat.succ |
1 |
**weak** |
**weak** |
0 |
weak topology |
weak, topology |
0 |
topologies |
topolog |
1 |
inequality, boundary |
as typed | 0 |
(see f(x)) |
see, f(x)) |
0 |
(f(x)) |
(f(x)) |
0 |
f(x), |
f(x) |
1 |
Three causes, each a few lines in _words or _stem:
- Markdown quoting is not shed. Agents paste backticked Lean names, and the published text carries no backticks. Add
`and*to the stripped set. - Shortening is one-directional and the README does not say so. The singular in
-ynever finds the plural in-ies. Either cut a finalyafter a consonant as well, or remove the stemmer; in both cases state the limit in the search contract, not only in the PR description. - The unbalanced-bracket fallback is all-or-nothing (
search.py:293-296). Strip brackets one at a time while they are unbalanced.
Should fix
- Merging with Keep implementation notes separate from mathematical articles #172 breaks a test. The merge is textually clean, but
tests/test_audit.py::test_container_note_audit_handles_deep_containment_iterativelyfails on the merged tree withAttributeErroratmarkdown.py:176: Keep implementation notes separate from mathematical articles #172's test builds_ArticleShape(True, True)and commit 1 makes the first fieldstatement: str. The description says "compatible". State it, so whichever lands second updates the stub. - The overlap section is understated. Trial merges also conflict with Allow proof imports with measured impact #183 in
autoform_cli/README.mdand with Publish blueprint sites transactionally #165 inrender.pyandtests/test_render.py. - The skills never say "exit 2". The description says they do;
skills/roadmap/SKILL.md:64andskills/formalize/SKILL.md:84-86say only "refusal". Suggest "A refusal (exit 2, anerror:line) is not an empty result". - The skill test still pins phrases (
tests/test_skill_examples.py:861-892). Deleting a rule is now caught, which is the main gain. An inverted rule that keeps the keywords passes, "rather than adding a node" fails, and a second paragraph namingautoform searchfails withValueError: too many values to unpackin place of an assertion. Either describe it as a presence check, or assert that "instead of adding" sits in the same sentence as## Depends on. - Undocumented statement-audit false positives.
Pending: a transaction $t$ is pending if ...andUnknown–known duality holds ...are reported as placeholders, and a symbol-only statement such as⊥ ≠ ⊤.as empty. The README documents onlyUnknown:.
Minor
!=and:=become the term=, which matches every equation.foo'is searched asfoo; it still finds the primed name but cannot rank it first.graph.py:19imports the privatemarkdown._mask_fences_and_comments; a public wrapper would drop the throwawayset()argument.SearchHit,SearchLeanTargetandSearchResultare insearch.__all__with no importer.- The audit renders each statement: 0.76 s on
mainagainst 2.39 s here at 1,500 synthetic articles. The description states this cost. - A blueprint the graph rejects makes search exit 2, which is common mid-authoring. Worth a line under "Known limits".
What holds up
- Audit, render and graph now read fences and comments through one helper. The example's graph is field-for-field identical to
main's and its rendered site is byte-identical. - The comment-opener-inside-a-fence fix is real:
mainloses the edge, this PR keeps it, and a test covers it. - Search produced no traceback for any input tried, its JSON bytes are identical across hash seeds and working directories, it carries no absolute path, and the emitted keys match the README contract exactly.
- The stemmer has a four-letter minimum and only ever yields a prefix, so the claim that shortening never removes a match is true for the match set. The problem is ordering (N2), not membership.
- Lint is clean at each of the four commits, no commit imports something a later one adds, and the skill paragraphs restate no flag and contradict no neighbouring rule.
Suggested path
- Fix N1 and add the fence-close graph test.
- Fix N2 and N3 in
_words,_stemand the sort key, with one test per row of the N3 table, and state the shortening limit in the README. - Correct the Keep implementation notes separate from mathematical articles #172, Allow proof imports with measured impact #183 and Publish blueprint sites transactionally #165 overlap notes and the "exit 2" sentence.
With 1 and 2 done, this PR is ready to merge as items 1, 2, 3 (without --skeleton) and 5.
a159b8a to
695650e
Compare
Third review against #143 phase 1Reviewed at head Summary. All three blocking findings of the second review are resolved. The PR is close to ready for the scope it states: items 1, 2, 3 (without Status of the second review
Fix before mergeBoth cases exit 0 with an empty result. The test article is titled "Convex cone", with the statement "A face of the cone is an edge or a line in R1. A plural in
|
| Query | Terms searched | Hits |
|---|---|---|
convex cone |
convex, cone |
1 |
convex cones |
convex, cones |
0 |
faces, edges, lines |
as typed | 0 |
The loop in _stem (search.py:339-349) matches es first, gets a three-letter stem, and returns the word without trying s. nodes, trees, types, cases and bases behave the same. This also contradicts the search contract, which says a word sheds a final s "when four letters remain": dropping the s from cones leaves four.
Suggested fix: continue to the next ending when the stem is too short, and add cones to the parametrized test.
R2. Wrappers are peeled in a fixed order, so combined ones survive
| Query | Terms searched | Hits |
|---|---|---|
$L^2$ |
l^2 |
1 |
L^2, |
l^2 |
1 |
$L^2$, |
l^2$ |
0 |
**weak** |
weak |
1 |
(**weak**) |
**weak** |
0 |
$ is stripped before the sentence punctuation (search.py:288), and emphasis is peeled before brackets, once (:303-311). A formula pasted with its trailing comma is a likely query.
Suggested fix: treat $ as one of the peeling steps in _bare and repeat the steps until the term stops changing.
Document or follow up
R3. Residual fence case
With the sloppy closer from N1 and a later fenced block whose fence is longer or indented, the graph treats that later line as the closer and masks everything between. I inserted the ```lean … ``` -- end block above ## Depends on in the example's non-ambiguity.md and appended a ## Notes section holding a four-backtick block:
Dependencies of non-ambiguity |
Audit finding for the file | Raw render shows the heading | |
|---|---|---|---|
main |
eligibility |
none | yes |
| this PR | none | missing-depends-section |
yes |
Here the audit reports it. With the sloppy fence under the heading instead, a fuzz run found the same edge loss with no finding (not re-run by hand). The cause is that markdown.py:695-699 closes on any bare marker at least as long as the opener at up to three spaces, while the site's fence extension needs the same length and indent.
A differential fuzz against the renderer puts this at 3 to 9 per 40,000 generated articles, down from 70 at the previous head, and over the differing articles this head agrees with the rendered page far more often than main does. I would accept it as a known limit with one test and one sentence under "Known limits" and in the README, which currently says only "closes only on a bare fence line".
Other items
- Test gaps in the fence loop. No test pins "a closed fence after an unclosed one is still hidden" (the existing test has the closed fence first) or that comment state is restored on rewind; both mutants pass the suite. The 20,000-line test has no time bound.
- Worst-case cost. The rewind is super-linear on adversarial input: 2,000 openers of strictly decreasing length (2 MB) take 0.67 s here and more on variants with long marker lines;
mainis linear. Ordinary articles are unaffected. One backward pass that records the longest later closer per fence character would remove the rewind. - Stemmer limits the README does not list.
level setsdoes not find "Level set" (three-letter singular); from a wider sweep,bodyandbodiesmiss each other, anddefinedoes not find "defining". The contract names only irregular forms. - Brackets means parentheses.
[weak]and a pasted[[wikilink]]keep their brackets. The contract sentence on shedding does not mention parentheses or this limit. - "As typed" is judged over all fields (
search.py:219), while the README says "Within one field". Harmless in practice; align one with the other. sentence()in the skill test (tests/test_skill_examples.py:883) raisesValueError: too many values to unpackwhen its phrase occurs in two sentences. Give it the same assertion message asrule().foo'has no test, andskills/roadmap/SKILL.md:64is 89 columns.
What holds up
- The backtracking loop agrees with an independent reference implementation on 450,000 random inputs, in output and hidden set, and it terminates: each rewind strictly lowers the recorded opener length.
- The bundled example's graph, audit findings,
site-srcand built site are identical tomain's. - The ordering is as documented, and a test asserts that a title match reached by a shortened word precedes a statement match as typed.
- No stem is a non-prefix of its word across 1,083 words, so shortening only adds matches. Over 24 queries on the bundled example no exact hit was lost.
- Search JSON is byte-identical across hash seeds, its keys match the README, and every refusal is a one-line error with exit 2.
- Every deletion of a skill rule is caught by the test, and both skills say "refusal (exit 2)".
Suggested path
- Fix R1 and R2, with one test row each.
- Add the R3 test and known-limit sentence, and the closed-after-unclosed fence test.
- List the remaining stemmer and bracket limits in the search contract.
With step 1 done and R3 documented, I consider this PR ready to merge as items 1, 2, 3 (without --skeleton) and 5.
The audit and the renderer each decided where a statement ends, and they disagreed: the audit stopped at the first H2, the renderer at any heading. article_parts in the Markdown primitives splits an article at its visible H2 headings, using the masking the coverage contract already relies on, and both now read it. The theorem box therefore runs to the first H2, with subheadings inside it demoted like the rest of the node's headings, and a heading inside an HTML comment no longer cuts the statement short. In the audit, prose above the title counts as statement text, a statement that is only an indented code block does not, and the missing-statement-text reason names the span read. The graph loader now reads fences and comments through the same helper. It used to remove comments before it recognised fences, so a `<!--` shown inside a code block hid every section after it and the article's dependency edges were dropped without a finding. A fence also closes only on a bare fence line now, and one that never closes hides nothing for the graph and the statement, because the page draws it as plain text with the headings and links after it in view. The coverage and link checks keep reading such a fence as open: a table in its paragraph is not published. Part of facebookresearch#143. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
695650e to
516a2f0
Compare
Fourth review: no blocking findingsReviewed at head Summary. Every finding of the third review is resolved, and this round found nothing blocking. The PR is ready for maintainer review for the scope it states: phase 1 items 1, 2, 3 (without Status of the third review
What this round checked
For the maintainer to decideThe description flags two behaviour changes as the ones to decide at review:
Follow-up material, none blocking
|
Review of
|
| Item | Status |
|---|---|
| 1. One statement span | Done, apart from point 2 and the render.py follow-up |
| 2. Statement text contract | Done |
3. autoform search |
Done except --skeleton, ## Execution notes (point 3) and the ranking in point 1 |
4. duplicate-lean-target |
Not started; two articles with the same primary lean: name still pass the audit |
| 5. Skills search first | Done; the skill test checks that the wording is present, as the description says |
Two acceptance criteria are unmet (--skeleton refuses a foreign report; duplicate primary ownership) and one is partly met: the counter test counts only what search adds, while each article is read three times per call and nothing asserts that reverse edges are built once. Deferring item 4 and --skeleton is reasonable for the reasons given in the description; tracking them as separate issues would keep "phase 1" from reading as complete.
A formalizable article could pass the audit with "TODO" as its statement, or with markup that publishes nothing. Search and reuse depend on the statement saying what the result is, so the audit now reports empty-statement-text and placeholder-statement-text. At most one statement finding fires per article, judged on the prose the site publishes without headings, code blocks and diagrams. A blueprint with such statements gains findings; the bundled example has none. Each formalizable statement is now rendered once, about 1 ms. The placeholder rule is the one coverage evidence follows, so it moves to markdown.py for both to share. One change to it applies to coverage too: a single hyphen or en dash marks a placeholder only when a space follows, so "Unknown-variance case" is no longer rejected. Two renderer failures that coverage could already reach would otherwise reach every statement, so they are fixed here. The rendered-tree walk is iterative, so deeply nested markup does not end the audit with a RecursionError. A conversion that fails midway used to leave the shared converter unable to render later text; it is now discarded. Part of facebookresearch#143. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing told an agent whether the blueprint already held a result, so a Roadmap pass could add a second article for it and Formalize could prove a helper twice. `autoform search TARGET QUERY` answers in one local, read-only call, and each hit carries what a reuse decision needs: the statement as authored, the derived state, the Lean targets with file and line, the sources, and the articles that already depend on it. An article is a hit when every term occurs in one of its fields, after folding case, width, accents, typographic punctuation and invisible characters. Filler words such as "of" are dropped from the query and a word of letters loses a common ending, so "recovered" finds "recovers". A term sheds the sentence punctuation, quotation marks, backticks and wrapping emphasis around it. Only the query is shortened, so shortening adds hits and never removes one. Fields rank title, Lean name, the article's own path segment, statement, then containing titles. Full path IDs and qualified Lean names rank last, so a chapter directory or a namespace does not bury a statement match. Titles and the statement are matched as the site publishes them. Ordering adds two tie-breaks to the issue's. Between hits with the same best field, one holding the words as typed is before one reached only through a shortened word, and then the one whose weakest term sits in a better field is first. `## Execution notes` is not searched, because facebookresearch#172 moves those notes out of the article. Search keeps no index: each call reads every article and renders its statement. It rereads each article and refuses bytes other than those the graph was built from, so a hit belongs to the reported source revision. `--json` writes `autoform-search/v1`. Part of facebookresearch#143. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`autoform search` prevents a duplicate only when an agent runs it first, and no skill mentioned it. Roadmap now searches before adding a formalizable article and links to an article that already states the result. Formalize searches before adding a helper and may use a hit only when the claimed article's dependencies reach it. The roadmap-quality rubric names the search as evidence for its duplicate check. Matching is literal, so Roadmap and Formalize treat a result as new only after retries with fewer words and other names find no article stating it, and say a refusal is not an empty result. The rubric says finding none is not proof. One new test pins those rules. Another checks that every inline skill link to the CLI reference points at that file and, when it names a section, at a heading that exists, so renaming a section cannot strand a skill. Refs facebookresearch#143. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
516a2f0 to
66cc8a1
Compare
A query term sheds the quote or sentence mark at its end, so `Project.foo'` was searched as `Project.foo`. The index kept the name's own ending, the whole-name lookup missed, and the article that owns the declaration fell to `qualified_names`, below every article that cites it in a statement. The same held for names ending in `?` and `!`. The indexed name now loses the same ending as the term. `Project.foo` is therefore also a `lean` match for `Project.foo'`; the search contract says so. Two more tests cover branches of the full-name match that nothing exercised: a `mathlib_declaration` name, and a `lean:` name written with `«»` and searched without them. Refs facebookresearch#143. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`autoform search --declaration definition` on a blueprint whose definitions are written `def` printed "No matching articles." and exited 0. That is the answer a new result gets, so a misspelled kind told an agent to add an article that already exists. Search now exits 2 for a kind that no article in the blueprint declares and names the kinds in use. A kind in use that the query does not reach is still an ordinary empty result. The README also states a second way the page and the graph read an unclosed fence differently: the page can pair the fence line with a later backtick run and show a link between them as inline code, while the graph still reads that link as a dependency. Refs facebookresearch#143. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Final review at
|
| Check | Result |
|---|---|
make lint |
clean |
make test |
2021 passed, 1 failed, 55 skipped, 1 xfailed |
make check-example |
exit 0 |
| CI | all jobs pass, including real Lean |
The one failure is tests/test_claims.py::test_cas_acquire_race_has_exactly_one_winner. It fails the same way on main at cc7e3a8, in files this PR does not touch. Seven hand-made mutants of the new behaviour were each killed by a named test.
What held up
- One statement definition. A differential fuzz of 3,833 generated articles,
mainagainst this head, found only the statement changes this description discloses, plus two benign whitespace changes now added to it. - Bundled example. Its rendered site differs from
main's only in the embedded commit SHA, and its audit output is identical. - Search safety. Search exits 2 for a symlink at every level, non-UTF-8 bytes and an escaping source target. It starts no process and writes nothing.
- Search output. Human output escapes control and bidirectional characters. JSON is byte-identical across invocation styles and carries no absolute path.
Findings
Important: an unclosed fence now passes every tool while the chapter page is still corrupted (#199). The loader reads a fence that never closes as plain text, on the premise that the page draws it that way. That holds for a page with one article. On a chapter page the opener pairs with a later article's closing fence, and one code block swallows the next article's theorem box. The page was equally corrupted on main, and the graph is more correct here. What is lost is the audit's missing-depends-section finding, which was the only signal the author got. This is not a merge blocker because the trigger is an authoring typo, the corruption is not new, and the fix is additive.
Minor, search (#200):
--declarationwith two kinds exits 2 when only one of them is unused.- A primed name does not rank its owner first when the unprimed name also exists.
- A space inside
«»turns a namespace into aleanmatch. - The README's search contract freezes the filler words and stemming rules.
Minor, parsing (#201): graph.py and render.py keep private copies of the fence and heading rules, and four readers in render.py still use the old one.
Description corrections
Edited in place to match what the code does:
- The "Indented code only" row: any statement whose first line is indented code changes from a paragraph to a code block.
- The notes below the statement are rebuilt with blank lines between sections.
- The unclosed-fence premise is qualified for chapter pages, with a link to Audit should report a code fence that never closes #199.
- The reread cost: about 14 seconds for 2,000 openers with an info string, not about a second.
Not checked
- Merge cleanliness against Keep implementation notes separate from mathematical articles #172, Allow proof imports with measured impact #183, Scale roadmap inventories and the full DAG explorer #90, Publish blueprint sites transactionally #165 and Bind statement: formalized to a recorded statement_hash #141.
- The 7,130 evidence-cell figure and the 2,031-article ranking anecdote in this description.
## Execution notesis not indexed, although item 3 of Add blueprint search and reuse: a description contract,autoform search, and search-before-add rules #143 asks for it. The description defers this to Keep implementation notes separate from mathematical articles #172; that is a maintainer decision.
Conflicts: - autoform_cli/markdown.py: export both sides' names, and rebuild statement_and_notes on main's article_parts (#171), so the statement box and the readback check that reads published_markdown end a statement where the audit and search do, at the first visible H2. In the box, ### subheadings and a second H1 now stay in the statement, demoted like the notes' headings; a title written below a ## section keeps its statement instead of losing it to the notes; and a fence that never closes no longer carries the sections after it into the statement. The old split's helpers, _body_without_dependencies and _trimmed, go. - autoform_cli/render.py: keep this branch's imports; drop main's _split_body and _demote_headings, which statement_and_notes replaces. - autoform_cli/audit.py: _read_article keeps this branch's snapshot text and reads it with article_parts; the imports are the union without frontmatter_end. - autoform_cli/graph.py: main's mask_fences_and_comments loop beside this branch's snapshot import. - autoform_cli/coverage.py: the union of the imports. - skills/setup/SKILL.md: the three workflows, then main's CODEOWNERS.autoform.example paragraph, then the approval rule. - tests/test_audit.py: both sides' new tests.
The merge of main 89dff27 took #171's graph loop, which masks fences and comments and reads a fence that never closes as text, and with it lost this branch's masking of indented code blocks. A link written in an indented block under Depends on or Sources became a dependency edge or a cited source again, though the page shows it as code and the audit's link check does not read it. mask_code_and_comments masks indented code on top of #171's reading, and the graph reads its lines through it.
Follow-up to our reviewWe reviewed this PR at
The statement-audit fence regression we found at Posted by PR swarm: PR Swarm Lead |
Part of #143: phase 1 items 1, 2, 3 (without
--skeleton) and 5. Six commits, reviewable one at a time; each builds on the one before. What phase 1 still needs after this PR is under "Not in this PR".The branch was rewritten after each of the two reviews: the fixes are folded into the commits they belong to, and the former fifth commit (
--skeleton) is removed. The fix from the third review is commit 5 and the fix from the final review is commit 6, both added on top without rewriting the branch.Commit 1: one definition of an article's statement
audit._read_articleandrender._split_bodyeach decided where a statement ends, and disagreed: the audit stopped at the first H2, the site at the first heading of any level. An article with an H3 before its first H2 had statement text for the audit and a truncated theorem box on the site.markdown.article_parts(text)now returns the statement and the H2 sections, and both callers use it. The statement is the body from the end of the frontmatter to the first##heading, without the H1 line. A##inside a fenced block or an HTML comment does not end it.##inside an HTML commentThe notes below the statement are rebuilt with a blank line between sections, so the page HTML changes where a
##line had no blank line above it.The graph loader reads fences and comments through the same helper. It used to remove comments before it recognised fences, so a
<!--shown inside a code block hid every section after it: the article's dependency edges were dropped, and with the audit moved toarticle_partsnothing reported it. Two rules follow the page:mainalso closed it on a fence line with trailing text.```lean ... ``` -- endblock above## Depends onloses no edge.autoform checkcan newly fail (a#line in what is now plain text is a second H1:multiple H1 titles) and the graph can gain or lose an edge. For example, under## Depends on, the lines``` -- end,```lean,- [a](a.md)and a bare```gave the edge toaonmainand give none here: the page draws theleanblock as code with the link inside it. Where the two differ, this head usually agrees with the rendered page, but not always:```is indented by one to three spaces, the page shows a real link toa,mainkept the edge, and this head drops it. This is the sloppy-closer limit below.## Depends on, a```leanline,-- see [a](a.md)and``` -- endin one paragraph are drawn by the page as inline code with no link, because the unclosed fence line pairs with the later backtick run.maingave no edge toa; this head adds one. The README says so.The coverage and link checks keep reading an unclosed fence as open, as on
main: a table in the same paragraph as such a fence is not published. The bundled example's graph is identical before and after.The render change in the first row of the table and this loader change are the ones to decide at review. The bundled example has no article in any row: its rendered site is byte-identical to
main's.Commit 2: audit statements that are empty or only a placeholder
A formalizable article passed the audit with
TODOas its statement, or with markup that publishes nothing. The statement is judged as the site publishes it, so one whose published prose has a letter or digit is never missing or empty. At most one statement finding now fires per article, in this order:missing-statement-text(existing): no prose outside code blocks, HTML comments and headings.empty-statement-text: the published prose has no letter or digit.placeholder-statement-text: every word is one ofpending,placeholder,todo,tbd,unknown, or the statement opens with one of them followed by a colon or a dash.The prose is judged as the site publishes it, through a new
markdown.visible_prose. The placeholder rule is the one coverage evidence already follows;is_placeholderandhas_substancemove fromcoverage.pytomarkdown.pyso both share it.What an existing user will notice:
autoform auditandautoform doctorthen exit 1. No generated workflow runs either command. The bundled example gains none.Unknown-variance case, ...is no longer rejected. The same rule now also acceptsTBD-later,TODO-choose a milestoneandpending-review. Across 7,130 generated evidence cells, these hyphen cases were the only verdicts that changed.Unknown–known duality ...passes for the same reason.Two renderer faults that coverage could already reach would otherwise reach every statement, so they are fixed here. Say if you would rather have them as a separate PR.
rendered_visible_textraised an uncaughtRecursionErroron markup nested about 1,000 elements deep. Its tree walk is now iterative.published_tablesstill recurses and still raises on such input, as it does onmain; statements do not go through it.render_htmlnow discards the converter when a conversion raises.Commit 3:
autoform searchautoform search TARGET QUERY [--lean-root PATH] [--state KEY]... [--declaration KIND]... [--limit N] [--json]reports the articles that contain every query term. It is read-only, keeps no index and starts no process. Each hit carries the statement as authored, the derived state, Lean targets with file and line, sources, and up to ten dependents with the full count.--jsonwritesautoform-search/v1; the contract is inautoform_cli/README.mdunder "Search contract".Where it departs from the issue, and why:
leanandnode_idmatch only the last component of a name, and a new lowest-ranked field,qualified_names, holds the path ID and Lean names in full. With the issue's fields, every article in a chapter matched the chapter's directory name: on a 2,031-article test blueprint, the one article whose statement was about a probability measure ranked 102 of 102 for "measure". It now ranks 2.## Execution notesis not searched, although Add blueprint search and reuse: a description contract,autoform search, and search-before-add rules #143 item 3 lists it, because Keep implementation notes separate from mathematical articles #172 (open) moves those notes out of the article. Indexing it now would add amatched_fieldsname to the contract that Keep implementation notes separate from mathematical articles #172 then empties.lean:ormathlib_declarationname, or its last components (Convex.separationofProject.Convex.separation), counts as aleanmatch, with or without_root_.and«». A namespace alone still matches onlyqualified_names. Otherwise an article that cites a declaration in its statement outranked the article that owns it. Commit 5 extends this to names ending in a prime,?or!.hahn-banachfinds "Hahn–Banach".≠does not match=. A term sheds the sentence punctuation, quotation marks, backticks,$and wrapping emphasis around it, and parentheses that are unbalanced or wrap the whole term, in any combination, soHahn–Banach,,"non-ambiguous",`Nat.succ`,(**weak**),$L^2$,and(f(x))find what the bare terms find.C*,foo_and!=are kept as typed.Non-*ambiguous* mapis found bynon-ambiguous, and an entity name or a link URL in a title is not searched.of,the,if, ...) are dropped, and a word of letters loses a common ending, sorecoveredfindsrecovers,topologiesfindstopologyandconesfindscone. Only the query is shortened, so this only adds hits, and an article holding the words as typed is listed before one reached only through a shortened word.topologiesandtopologyare both searched astopolog, so each finds the other, but a form that does not begin with the shortened word does not:indicesdoes not find "index",sets"set",bodies"body", nordefine"defining". Shortening is kept because it only ever adds hits, and a missed hit is what leads to a duplicate. The search contract states the limit.article_revision,mathlib_declarations,mathlib_file,shared_titleper hit andopen_statementsat the top, since sibling schemas carry them and a key added later means a version bump.Search rereads each article and exits 2 on bytes other than those the graph was built from, and refuses what
autoform workrefuses (a symlinked roadmap entry, an escaping target).Commit 4: the skills search before adding a result
Search prevents a duplicate only if an agent runs it first, so three skill files gain one paragraph each. None restates a flag; each names
autoform searchand links to the search contract.skills/roadmap/SKILL.md): before adding a formalizable article, search by a few distinctive words and by a Lean name when one is known, read each hit's statement, and retry with fewer words and other usual names before treating the result as new. When another article already states the result, link to it under## Depends onor## Proof depends onand from the coverage row. A hit that is more general, a special case, or only similar does not replace the result.skills/formalize/SKILL.md): before adding a helper, search the blueprint as well. A hit's declaration is used only when the claimed article's dependencies reach that hit's article and the open-statement policy allows it; a hit they do not reach is a missing prerequisite, never something to restate.references/roadmap-quality.md): a new evidence item runs the search for each main result in scope and lists the queries. Finding no duplicate is not proof, because matching is literal.Both skills say that a refusal (exit 2) is not an empty result. Two tests back this. One checks that every skill link into
autoform_cli/README.mdlands on an existing section. The other reads the paragraph that namesautoform searchin each file and checks that it keeps the rule: the contract link, the retry with fewer words, what to do with a hit (link under## Depends oninstead of adding a node; use a hit only when dependencies reach it, otherwise a missing prerequisite), and that a refusal is not an empty result. It is a presence check: each instruction must be there, in one sentence, but an inverted rule that kept the words would pass.The paragraphs are insert-only and sit away from the lines #172 rewrites; the merge with #172 is clean in both orders and the merged tree's skill tests pass.
Commit 5: a Lean name ending in a prime,
?or!A query term sheds the quote or sentence mark at its end, so
Project.foo'was searched asProject.foo, while the index kept the name's own ending. The whole-name lookup of commit 3 therefore missed, and the owner ofProject.foo',Project.get?orProject.get!was listed below every article citing it. The indexed name now loses the same ending as the term.Project.foois also aleanmatch forProject.foo'. The search contract says so.mathlib_declarationname, and alean:name written with«»and searched without them.Commit 6: a declaration kind no article uses
autoform search --declaration definitionon a blueprint whose definitions are writtendefprinted "No matching articles." and exited 0, which is the answer a new result gets. Search now exits 2 for a kind that no article in the blueprint declares and names the kinds in use:A kind in use that the query does not reach is still an ordinary empty result. The README states the rule, and also the second fence case from commit 1 (a link the page shows as inline code but the graph reads as a dependency).
Not in this PR
duplicate-lean-target(criterion: "autoform auditreports duplicate canonical primary ownership ..."). Per the maintainer's comment on Add blueprint search and reuse: a description contract,autoform search, and search-before-add rules #143 it is to be implemented after Scale roadmap inventories and the full DAG explorer #90 and the refreshed Bind formalized Lean targets to built artifacts in CI #119 artifact layer, and the Reject duplicate formalized Lean targets #127 gate is not to be reused.autoform search --skeleton REPORT(the rest of item 3; criterion: "--skeletonrefuses a report made for a different blueprint"). A report identifies the whole blueprint, so refusing on any difference makes the flag unusable while articles are being added, and accepting it lets an outdated signature through. The criterion needs a finer identity, to be agreed on Add blueprint search and reuse: a description contract,autoform search, and search-before-add rules #143; the implementation is kept on a separate branch for a follow-up PR.Known limits
mapmatchesroadmap,unambiguousdoes not matchnon-ambiguous, andℝfolds tor, which matches nearly every article. The README tells agents to retry with fewer words before treating a result as new.--statekeys are exact:proveddoes not includefully_proved.source_revisioncomes from the runtime's read, which upstream does not compare withload_graph's. Search binds its own read to the graph's hashes; closing the remaining gap is a two-line change inruntime.pythat I left out of this PR.missing-depends-sectiononly when the comment sits above that heading on a formalizable article. This ismain's behaviour.Unknown: ...orPending: ...is reported as a placeholder and needs rewording, and one written in symbols alone (⊥ ≠ ⊤.) is reported as empty;To be writtenandTODO state itpass and are left to review. The README documents these.autoform_cli/render.pykeeps its own fence readers with the old rule, so a###after an unclosed fence inside a statement is not demoted on the page.--declarationis checked against the blueprint, not against a fixed list: a kind no article declares exits 2 and names the kinds in use (commit 6), so the filter cannot be used on a blueprint that has no article of that kind yet.main; search and the statement audit now read it.[weak]and a pasted[[wikilink]]are searched with them.foo'is searched asfoo, so a search cannot tellfoofromfoo': both owners areleanmatches for either spelling, bare or qualified. Keeping the quote would make a possessive miss.Overlap with open PRs
tests/test_audit.py::test_container_note_audit_handles_deep_containment_iterativelybuilds_ArticleShape(True, True), and commit 1 makes the first fieldstatement: str. Whichever lands second changes that stub to_ArticleShape("x", True). Search reads no notes.autoform_cli/__main__.py(same spot as the search parser) and inautoform_cli/README.md; whichever lands second needs a small rebase.audit._read_articleand nearby lines; a small manual rebase for whichever lands second.coverage.py(import block),render.pyandtests/test_render.py, and touchesmarkdown.__all__; coverage's call sites are unchanged here to keep that small.article_statement.article_parts(...).statementis meant to be the one definition it reuses.Testing
make lintclean;make check-examplepasses.make testin a worktree of22d1c02: 2,021 passed, 1 failed. The failure istests/test_claims.py::test_cas_acquire_race_has_exactly_one_winner, in files this PR does not touch; it fails there on untouchedmainas well.🤖 Generated with Claude Code