Skip to content

Report unclosed Markdown fences during audit - #202

Merged
Deicyde merged 2 commits into
facebookresearch:mainfrom
1cbyc:1cbyc/199-unclosed-code-fence
Oct 12, 2026
Merged

Deicyde merged 2 commits into
facebookresearch:mainfrom
1cbyc:1cbyc/199-unclosed-code-fence

Conversation

@1cbyc

@1cbyc 1cbyc commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #199

autoform audit now reports unclosed-code-fence with the opening source line whenever an article contains a fence that reaches EOF. The detection reuses the existing Markdown masking pass, so audit diagnostics and graph parsing keep the same fence and comment semantics.

The README now distinguishes single-article parsing from chapter-page corruption, and regression coverage verifies both the finding and the bundled example.

Validation:

  • uv run pytest -q tests/test_audit.py (65 passed)
  • uv run ruff check autoform_cli servers tests
  • uv run pytest -q (2020 passed, 58 skipped, 1 xfailed)
  • make check-example
  • git diff --check

Co-authored-by: insisong emmanuelisaacnsisong@gmail.com

Co-authored-by: insisong <emmanuelisaacnsisong@gmail.com>
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 9, 2026
@Deicyde

Deicyde commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Review at edf44d0

I reproduced #199 on main and on this head using the issue's chapter: c/a.md with an unclosed ```lean, c/b.md with a closed block, and c/z.md. On main the audit is clean while the rendered chapter page puts Beta's statement inside a code block. This head reports roadmap/c/a.md: unclosed-code-fence: code fence opened on line 9 never closes and exits 1. The check reuses the #171 masker with unclosed_fence_is_text=True, so it follows main's rules: a fence closes only on a bare line, and an unclosed fence hides nothing from the graph or the statement. Audit output on the hatcher blueprint (43 articles with fences), on hartshorne and on the bundled example is identical to main. tests/test_audit.py, test_markdown.py, test_graph.py and test_doctor.py pass, and ruff is clean. Verdict: small fixes. Two pieces of documentation still claim more than the check does, one inline-code pattern becomes a false error, and the check is simpler and safer inside _read_article.

Should fix

1. The new README sentence overstates the check (autoform_cli/README.md:67-68)

"The audit reports unclosed-code-fence before that can corrupt the chapter" is false when the graph treats a fence as closed but the page does not. The next sentence already says the page wants the opener's exact length and indent. In the #199 chapter I tried three such closers: four backticks, three spaces before the backticks, and an opener indented two spaces with a flush closer. Each leaves the audit clean while Beta's statement still lands in a code block (main behaves the same). Say only what the check does, for example "The audit reports a fence with no closing line as unclosed-code-fence." Flagging a closer whose length or indent differs from its opener would make the stronger sentence true, but that can be a follow-up.

2. The mask_fences_and_comments docstring still says the page agrees (autoform_cli/markdown.py:659-662)

#199 asked for this docstring to be corrected along with the README. It still says the graph, the audit and the page agree, and that the site draws an unclosed fence as plain text. Something like:

    """Return ``lines`` with fenced blocks and HTML comments blanked.

    This is how the graph and the audit agree on which headings and links an
    article has. A fence without a closing line is read as plain text, so it
    hides nothing here. A one-article page draws it the same way, but a chapter
    page can pair it with a later article's fence, so the audit reports it as
    ``unclosed-code-fence``.
    """

3. A line that starts with inline code in triple backticks becomes a failing error (autoform_cli/markdown.py:69)

FENCE accepts a backtick opener whose info string contains a backtick. Neither CommonMark nor superfences treats that line as a fence. On main the masker misread it too, but nothing failed. This PR turns it into an error that fails autoform audit and consumer CI. Repro: in the #199 chapter, replace a.md's fence with the line ```x``` is inline code here.. The page renders <p><code>x</code> is inline code here.</p>, but this head reports unclosed-code-fence on line 9 and exits 1. Add a closed block under a later ## Sources and the finding moves to line 19, the correct closing fence of that block. (In that layout main and this head both also lose the c/z edge, which predates this PR.) The fix is one line:

FENCE = re.compile(r"^ {0,3}(`{3,}(?=[^`]*$)|~{3,})")

With it, both layouts audit clean, the edge comes back, the four test files above pass, and audit output on the three real blueprints is unchanged. render.py:30 has the same pattern if you want to keep the two in step.

4. Read the article once, in _read_article (autoform_cli/audit.py:173)

The new line reads the file a second time without the OSError/UnicodeError guard that the other two reads in audit.py have. audit_graph audits an already loaded graph (audit_blueprint and autoform doctor call it), so an article renamed or removed after the load now raises instead of producing findings. Repro: load the #199 chapter, delete c/b.md, then call audit_graph. Main returns two findings for c/b.md; this head raises FileNotFoundError. The window is narrow, but it is a regression. The new line also scans the frontmatter, which the graph and article_parts skip: a # <!-- draft YAML comment line hides a real unclosed fence below it from the check. Computing the result in _read_article fixes both and shrinks the diff:

@dataclass(frozen=True, slots=True)
class _ArticleShape:
    statement: str
    has_depends_section: bool
    #: The one-based source line of the first fence that never closes.
    unclosed_fence: int | None = None


def _read_article(path: Path) -> _ArticleShape:
    ...
    parts = _article_parts(text)
    lines = text.splitlines()
    start = _frontmatter_end(lines)
    unclosed = _first_unclosed_fence(lines[start:])
    return _ArticleShape(
        statement=parts.statement,
        has_depends_section=any(section.title.casefold() == "depends on" for section in parts.sections),
        unclosed_fence=None if unclosed is None else start + unclosed + 1,
    )
        if article.unclosed_fence is not None:
            findings.append(
                AuditFinding(
                    article_path,
                    "unclosed-code-fence",
                    f"code fence opened on line {article.unclosed_fence} never closes",
                )
            )

In markdown.py, rename the keyword so it no longer shadows the module-level function:

def first_unclosed_fence(lines: list[str]) -> int | None:
    """Return the index of the first fence line that never closes, if any."""

    openers: set[int] = set()
    _mask_fences_and_comments(lines, set(), unclosed_fence_is_text=True, unclosed_openers=openers)
    return min(openers, default=None)

This version reports only the first opener. With several, the current message reads code fence opened on line 9, 11 never closes, and line 11 can be the too-short closer the author meant for line 9 (a four-backtick opener closed with three backticks). Fixing the first fence usually settles the rest. If you would rather report them all, write "lines" and add a test with two unclosed fences. Reported numbers stay source lines, so your test's line 13 still holds. With items 3 to 5 applied, the deleted-file repro returns main's findings, the four test files pass (247 tests), and the real blueprints audit the same.

Test gaps

5. No test puts the fence in a chapter README (tests/test_audit.py:874)

Both new tests use a formalizable leaf, so changing the check to if unclosed_fences and node.formalizable: still passes all 65 tests in test_audit.py. That variant would miss a non-formalizable article that does corrupt a chapter. A chapter README.md heads its chapter page, and in the repro an unclosed ```text there swallows Beta's statement. This test passes on this head and with item 4, and fails on that variant:

def test_audit_reports_an_unclosed_code_fence_in_a_chapter_article(tmp_path: Path) -> None:
    # A chapter's README heads its chapter page, so its open fence swallows the leaves below it.
    blueprint = tmp_path / "blueprint"
    _coverage(blueprint)
    _article(blueprint, "chapter/result.md", declaration="theorem")
    readme = blueprint / "roadmap" / "chapter" / "README.md"
    readme.write_text("# Chapter\n\n```text\nsketch\n", encoding="utf-8")

    assert ("unclosed-code-fence", "code fence opened on line 3 never closes") in _finding_map(blueprint)[
        "roadmap/chapter/README.md"
    ]

Nits

  • autoform_cli/README.md:826-833: the Audit contract summary lists what the audit checks but says nothing about fences. unclosed-code-fence is a new error that fails consumer CI, so it should get a clause there, such as "and that no article leaves a code fence open".

Checked, no change needed

  • When the new keyword is left as None, mask_fences_and_comments and content() behave exactly as on main, so the graph and statements do not change.
  • There is no finding for closed fences, fences inside HTML comments, indented code, blockquote fences, or a fence indented under a list item.
  • A narrative leaf can get the finding even though it is published on its own page and cannot corrupt a chapter. That is harmless and keeps the rule simple.
  • test_bundled_example_has_no_statement_or_unclosed_fence_finding cannot fail today, because the bundled blueprint has no fence lines. Audit should report a code fence that never closes #199 asked for it and it costs nothing; just don't count it as coverage.
  • Keep implementation notes separate from mathematical articles #172 conflicts with this PR in audit_graph, where both insert a block right after article = _read_article(node.path). Keep both.

Posted by PR swarm: PR Swarm Lead

Co-authored-by: insisong <emmanuelisaacnsisong@gmail.com>
@1cbyc

1cbyc commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review in signed commit 5b383e4:

  • tightened fence recognition so inline triple-backtick text is not treated as a fence
  • compute the first unclosed fence during the guarded article read, excluding frontmatter and avoiding a second filesystem read
  • added chapter README.md, inline-code, removed-file, and frontmatter regression coverage
  • corrected the README/docstring wording and added the audit-contract clause

Validation: make lint, make test (2025 passed, 58 skipped, 1 xfailed), make check-example, and the focused Markdown/audit/graph/render suite (319 passed).

@Deicyde
Deicyde merged commit ee3a4b9 into facebookresearch:main Oct 12, 2026
5 checks passed
Deicyde added a commit that referenced this pull request Oct 13, 2026
Brings in main at 76e486d: the CI audit probe that runs no project code
(#190), the atlas release and library dependencies on Lean 4.34.1 (#211),
the project inspect cleanup (#174), lakefile.lean precedence (#189), roadmap
symlink refusal and the bounded parent walk (#197), unclosed-fence audit
findings (#202), migrate article-ids --write (#191), and complete scopes and
container boxes (#196).

Conflicts:

- autoform_cli/README.md: main's description of --write, with this branch's
  "The runtime" in place of "Runtime v3", since the runtime schema here is
  v4.
- autoform_cli/graph_pages.py: the explorer pages stay as they are here,
  with no per-node focus pages or Mermaid full page, so main's focus_views
  and container_nodes calls, its drawn statuses and its focus-page loop go;
  the explorer legend already leaves containers out. The project lead takes
  main's count, the items its scopes count, and main's "items" wording.
- autoform_cli/graph_views.py: ViewNode carries main's complete flag and
  this branch's catalog, summary, lean, area and internal count. Project,
  nested-scope and node-level container nodes are all built by main's
  _scope_node, extended with this branch's fields and rolled-up status, and
  full_view takes main's containers argument with this branch's title and
  summary. Main's target rule, a leaf that declares something or is marked
  not ready, becomes is_scope_target, which also keeps catalogs out; scope
  members, completion and status rings all use it, and checked catalogs
  still count apart. The unused _leaf_descendant_map goes.
- autoform_cli/mermaid.py: a container's tooltip gives main's scope summary
  and a checked catalog's still says "inventory checked"; the vault legend
  leaves out containers, as on main, and keeps only formalizable leaves, as
  here.
- autoform_cli/render.py: main's _FENCE, which opens no backtick fence whose
  info string holds a backtick, beside this branch's _MARKDOWN_LINK, which
  allows brackets inside a label.
- skills/roadmap/SKILL.md: this branch's existing-repository paragraph with
  main's instruction to run article-ids --write.

Adopting main's scope rule needed three changes without a textual conflict.
The explorer payload's member_count and its bubble size use item_count, so a
scope's "N items" counts what its status rows count, inventory row included.
autoform-visualize falls back to a scope's first article from group_nodes
rather than its first member, since members are now only targets. Four tests
now expect a node-level container's members to be the container, a fully
proved scope to draw as scope_complete, the project lead to say "1 item",
and the atlas fixture's counts to match its members.
Deicyde added a commit that referenced this pull request Oct 13, 2026
Test the frontmatter, inline-backtick and render fence fixes from #202
Deicyde added a commit that referenced this pull request Oct 14, 2026
#208 deferred two deletions until #202 landed. The test repeats the
comment cases that test_audit_requires_statement_prose_outside_code_comments_and_headings
and the title-line test already pin, and the audit contract's parenthetical
repeats its missing-statement-text bullet.
OrigamiClub pushed a commit to OrigamiClub/autoform-bot that referenced this pull request Oct 16, 2026
…ebookresearch#202

Each test fails with its fix reverted. The audit frontmatter test and the
masker test that were meant to cover two of them passed either way, and
render.py's fence pattern had no test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Audit should report a code fence that never closes

2 participants