From 830de11c664d33aa8bd0abd94ac33f40fc939eaa Mon Sep 17 00:00:00 2001 From: KennySimpson <70093673+KennyMcSimpson@users.noreply.github.com> Date: Sun, 4 Oct 2026 14:54:08 +0800 Subject: [PATCH] fix(uploads): exclude indented bold lines from outlines (#6194) * fix(uploads): exclude indented bold lines from outlines * refactor(uploads): name the indented-code pattern --- README.md | 4 +- .../packages/harness/deerflow/utils/AGENTS.md | 6 +++ .../harness/deerflow/utils/file_outline.py | 7 +++ .../tests/test_file_outline_indented_bold.py | 51 +++++++++++++++++++ 4 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 backend/tests/test_file_outline_indented_bold.py diff --git a/README.md b/README.md index 35b87d2de1..43d6b1cdc0 100644 --- a/README.md +++ b/README.md @@ -1907,8 +1907,8 @@ AIO directory listings discard missing shell sessions so the next request can re After a dropped connection, directory listings and persistent shell commands report an unknown outcome without replaying the operation; later calls use a fresh session. -Uploaded Markdown outlines recognize ATX heading syntax, clean closing markers with a linear suffix scan, and skip fenced code examples, so hashtags and code comments do not -crowd out real document sections from the agent's heading preview. +Uploaded Markdown outlines recognize ATX heading syntax, clean closing markers with a linear suffix scan, and skip fenced and indented code examples, so hashtags and code comments do not +crowd out real document sections from the agent's heading preview. Indented bold examples are also excluded; PDF-style bold headings with up to three leading spaces remain supported. UTF-8 Markdown files with or without a byte-order mark (BOM) produce the same outlines and fallback previews, with original line numbers preserved. Outline titles are limited to 200 characters and fallback previews to 2,000 diff --git a/backend/packages/harness/deerflow/utils/AGENTS.md b/backend/packages/harness/deerflow/utils/AGENTS.md index 447abbe609..a7121e5250 100644 --- a/backend/packages/harness/deerflow/utils/AGENTS.md +++ b/backend/packages/harness/deerflow/utils/AGENTS.md @@ -32,6 +32,12 @@ optional leading UTF-8 BOM cannot hide a first-line heading or code fence, or occupy a preview line. Preserve physical line numbers, embedded U+FEFF characters, and the original file bytes. +All outline heading styles skip root-level indented code (four leading spaces +or a tab after up to three spaces), including PDF-style single and split bold +headings. Keep legitimate bold headings with up to three spaces and the shared +conversion-companion path working. Regression coverage lives in +`tests/test_file_outline_indented_bold.py`. + ### Active Content MIME Types `text_detection.py::_is_active_content_mime_type` is the shared download-safety diff --git a/backend/packages/harness/deerflow/utils/file_outline.py b/backend/packages/harness/deerflow/utils/file_outline.py index ae50e8420e..8077b80f85 100644 --- a/backend/packages/harness/deerflow/utils/file_outline.py +++ b/backend/packages/harness/deerflow/utils/file_outline.py @@ -51,6 +51,10 @@ _CODE_FENCE_RE = re.compile(r"^ {0,3}(`{3,}|~{3,})(.*)$") # Match the original indentation so indented code cannot become a heading. _ATX_HEADING_RE = re.compile(r"^ {0,3}#{1,6}(?:[ \t]+(.*))?$") +# Root-level indented code starts at four columns, including +# a tab after up to three spaces. Do not strip it into a heading. +_INDENTED_CODE_RE = re.compile(r"^( {4}| {0,3}\t)") + def _strip_atx_closing_hashes(raw: str) -> str: """Remove a whitespace-separated terminal hash run in linear time.""" @@ -141,6 +145,9 @@ def extract_outline(md_path: Path) -> list[dict]: fence_length = len(marker) continue + if _INDENTED_CODE_RE.match(line): + continue + stripped = line.strip() if not stripped: continue diff --git a/backend/tests/test_file_outline_indented_bold.py b/backend/tests/test_file_outline_indented_bold.py new file mode 100644 index 0000000000..dab30f30f7 --- /dev/null +++ b/backend/tests/test_file_outline_indented_bold.py @@ -0,0 +1,51 @@ +"""Indented bold examples must not displace document headings in outlines.""" + +import pytest + +from deerflow.uploads.companions import register_companion +from deerflow.utils.file_outline import MAX_OUTLINE_ENTRIES, extract_outline, extract_outline_for_file + + +@pytest.mark.parametrize("indent", [" ", "\t", " \t", " \t", " \t"]) +@pytest.mark.parametrize("text", ["**PART II**", "**1** **Code example**"]) +def test_indented_bold_code_preserves_real_heading_lines(tmp_path, indent, text): + path = tmp_path / "guide.md" + path.write_text(f"# Overview\n\n{indent}{text}\n\n## Findings\n", encoding="utf-8") + + assert extract_outline(path) == [{"title": "Overview", "line": 1}, {"title": "Findings", "line": 5}] + + +@pytest.mark.parametrize("indent", [" ", "\t"]) +@pytest.mark.parametrize("text", ["**PART II**", "**1** **Code example**"]) +def test_indented_bold_examples_cannot_exhaust_outline_slots(tmp_path, indent, text): + path = tmp_path / "guide.md" + path.write_text((indent + text + "\n") * (MAX_OUTLINE_ENTRIES + 1) + "\n# Actual findings\n", encoding="utf-8") + + assert extract_outline(path) == [{"title": "Actual findings", "line": MAX_OUTLINE_ENTRIES + 3}] + + +@pytest.mark.parametrize("indent", ["", " ", " ", " "]) +def test_bold_headings_with_up_to_three_spaces_are_preserved(tmp_path, indent): + path = tmp_path / "report.md" + path.write_text(f"{indent}**PART II**\n{indent}**1** **概述**\n", encoding="utf-8") + + assert extract_outline(path) == [{"title": "PART II", "line": 1}, {"title": "1 概述", "line": 2}] + + +def test_owned_conversion_uses_the_same_indentation_rule(tmp_path): + uploads = tmp_path / "thread/user-data/uploads" + uploads.mkdir(parents=True) + original = uploads / "report.pdf" + markdown = uploads / "report.md" + original.write_bytes(b"synthetic source fixture; converter not invoked") + markdown.write_text(" **PART II**\n\n# Actual section\n", encoding="utf-8") + register_companion(original, markdown) + + assert extract_outline_for_file(original) == ([{"title": "Actual section", "line": 3}], []) + + +def test_existing_fenced_code_and_heading_styles_are_preserved(tmp_path): + path = tmp_path / "guide.md" + path.write_text("~~~\n**PART II**\n~~~\n# Section\n**PART III**\n**2** **Details**\n", encoding="utf-8") + + assert extract_outline(path) == [{"title": "Section", "line": 4}, {"title": "PART III", "line": 5}, {"title": "2 Details", "line": 6}]