diff --git a/.gitignore b/.gitignore index 6a1063e..0305b36 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ log # The default `--cache`, which a run and the tests write into the directory # they are run from. .in2lambda-agent/ +# The default `--out`: a run writes the replies, the flags and the zip of a +# private document there. A run's output is evidence for a pull request, not +# part of the repository. +out/ diff --git a/docs/plan.md b/docs/plan.md index 0ee4cfe..49a997a 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -88,7 +88,9 @@ a word Mathpix misread passes every tier. The comparison with an exported set, o reader, detects it. An OCR check is separate work. The one misread the route detects is a separator line read as a minus sign: `stray_minus` flags a field whose display maths begins or ends with a minus sign, and a field holding a minus sign on a line of its own -beside a display maths. +beside a display maths. The check applies to a document Mathpix converted and not to one +pandoc converted, because pandoc reads no separator line and such a minus sign is the +author's own. ## Response areas diff --git a/in2lambda_agent/routes.py b/in2lambda_agent/routes.py index 0c68856..1204abd 100644 --- a/in2lambda_agent/routes.py +++ b/in2lambda_agent/routes.py @@ -8,8 +8,9 @@ passage of the source, never its own words (`adjudicate`); what neither settles is a flag for a person (`reconcile`). A field only one route filled is not a disagreement: the text of the route that filled it is taken, and no call is made. A minus sign inside or beside a -display maths, which Mathpix reads from a separator line, is flagged too (`stray_minus`). -`to_set` and `build` write the result with in2lambda. +display maths, which Mathpix reads from a separator line, is flagged too (`stray_minus`), +in a document Mathpix converted and not in one pandoc converted. `to_set` and `build` +write the result with in2lambda. `convert` converts one document. `convert_folder` converts a folder of them: it pairs each sheet with its solutions document, writes one filter from the first pair, and reports for @@ -143,7 +144,9 @@ def stray_minus(reply: Reply_) -> list[str]: """The fields holding a minus sign Mathpix read from a separator line. A display maths begins or ends with the minus sign, or the minus sign stands on a - line of its own beside the block. + line of its own beside the block. `convert` asks this of OCR markdown only, because + the same minus sign in a tex or docx document is the author's own; the function reads + a reply and asks nothing about the document the reply came from. """ found = [] for key, text in fields(reply).items(): @@ -547,10 +550,10 @@ def said(stage: str, message: str) -> None: # The build writes the zip here at the end of the run; the replies are written # into the same directory as each route answers, before the build makes it. out_dir.mkdir(parents=True, exist_ok=True) - read = [f"{Path(d).name}: {_read_as(d, cache_dir)}" for d in (document, solutions) if d is not None] + read = [(Path(d).name, _read_as(d, cache_dir)) for d in (document, solutions) if d is not None] markdown, images = markdown_of(document, cache_dir, settings) solutions_md = markdown_of(solutions, cache_dir, settings)[0] if solutions else None - said("ocr", "; ".join(read)) + said("ocr", "; ".join(f"{name}: {how}" for name, how in read)) source = markdown + ("\n" + solutions_md if solutions_md else "") if route_a is None: route_a, usage = direct(markdown, solutions_md, backend) @@ -588,9 +591,14 @@ def said(stage: str, message: str) -> None: reconciled.agreed, reconciled.defaulted, reconciled.adjudicated, ) said("route B", "ran") - for k in stray_minus(reply): - if not any(f.field == k for f in flags): - flags.append(Flag(k, fields(reply)[k], "", STRAY_MINUS)) + # The stray minus sign is Mathpix reading a printed separator line. Pandoc reads no + # such line, so a minus sign at the edge of a display maths of a tex or docx document + # is the author's own, and the check runs only where a document was not converted by + # pandoc. + if any(how != "pandoc" for _, how in read): + for k in stray_minus(reply): + if not any(f.field == k for f in flags): + flags.append(Flag(k, fields(reply)[k], "", STRAY_MINUS)) result = Converted( set=to_set(reply, name=name, directory=images), zip_path=None, flags=flags, reply=reply, route_a=route_a, route_b=route_b, tokens=tokens, diff --git a/tests/fixtures/tex-sheet.tex b/tests/fixtures/tex-sheet.tex index 3c5d779..8c761c6 100644 --- a/tests/fixtures/tex-sheet.tex +++ b/tests/fixtures/tex-sheet.tex @@ -17,6 +17,14 @@ \section*{Problem Sheet 1: Kinematics} \item Two ships sail from the same point. Are they on a crash course? If not, find their distance of closest approach. +% The display maths begins with the author's own minus sign, which the +% stray-minus check must not flag in a document pandoc converted. +\item The potential of a dipole on its axis is + \[ + -\frac{p}{4 \pi \varepsilon_0 z^2}. + \] + Find the field at a distance $z$. + \end{enumerate} \end{document} diff --git a/tests/test_routes.py b/tests/test_routes.py index 485df80..113a3d1 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -145,6 +145,36 @@ def test_convert_reports_the_stray_minus_as_a_flag(tmp_path): assert result.route_a == REPLY +@pytest.mark.skipif(shutil.which("pandoc") is None, reason="pandoc") +def test_a_minus_at_the_edge_of_a_maths_in_a_tex_document_is_the_authors(tmp_path): + # The tex fixture's third question opens a display maths with a minus sign the + # author wrote. Pandoc reads no separator line, so convert makes no flag of it, + # although the pattern names the field. + reply = [ + { + "title": "", + "main_text": "The potential of a dipole on its axis is", + "parts": [ + { + "content": "Find the field at a distance $z$.", + "options": [], + "answer": "", + "worked_solution": "$$-\\frac{p}{4 \\pi \\varepsilon_0 z^2}.$$", + } + ], + } + ] + assert routes.stray_minus(reply) == ["q1.p1.worked_solution"] + + result = routes.convert( + Path(__file__).parent / "fixtures" / "tex-sheet.tex", + out_dir=tmp_path / "out", + backend=FakeBackend(json.dumps(reply)), + settings=Settings(), + ) + assert result.flags == [] + + def test_a_reply_given_to_convert_is_route_as_and_no_call_is_made(tmp_path): # What a targets run hands back from the reply it saved: the same reading of # the document, so that two runs compare the same set with the export.