From 745fcea29316cf9d659f42883b933dbfcaa12210 Mon Sep 17 00:00:00 2001 From: rcmerci Date: Sat, 7 Nov 2026 21:35:59 +0800 Subject: [PATCH 1/2] docs: include Journal execution guide in Dune odoc output --- app/spec/README.md | 7 ++ app/spec/dune | 15 ++++ app/spec/index.mld | 10 +++ test/journal_documentation_test.py | 108 +++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+) create mode 100644 app/spec/dune create mode 100644 app/spec/index.mld create mode 100644 test/journal_documentation_test.py diff --git a/app/spec/README.md b/app/spec/README.md index 788782c..e28ad96 100644 --- a/app/spec/README.md +++ b/app/spec/README.md @@ -4,6 +4,13 @@ walkthrough of the important App, Worker and Sync paths using existing spec types and pseudocode. It exports no interface and adds no virtual library. +Run `dune build @doc` and open `_build/default/_doc/_html/logseq_journal/index.html`. +The package homepage links to the execution guide and the coordination API. +The guide's `.mld` page is generated from its single `.mli` documentation comment; +edit only the `.mli` body. After building, run +`python3 -B test/journal_documentation_test.py` from the repository root to check +the generated page, all examples and local navigation links. + Each area under this directory is an independent wrapped Dune virtual library. Do not use recursive `include_subdirs` to merge future specifications into one library. Add another area only when it has actual contracts and consumers. diff --git a/app/spec/dune b/app/spec/dune new file mode 100644 index 0000000..b26d233 --- /dev/null +++ b/app/spec/dune @@ -0,0 +1,15 @@ +; Keep the documentation-only .mli as the sole body source. Its single comment +; is checked by source_boundary_test; no OCaml module or library is added here. + +(rule + (target journal_execution_flow.mld) + (action + (with-stdout-to + %{target} + (progn + (echo "{0 Journal execution flow}\n\n") + (run sed -e "1s/^(\\*\\* *//" -e "$d" %{dep:journal_execution_flow.mli}))))) + +(documentation + (package logseq_journal) + (mld_files index journal_execution_flow)) diff --git a/app/spec/index.mld b/app/spec/index.mld new file mode 100644 index 0000000..794dd84 --- /dev/null +++ b/app/spec/index.mld @@ -0,0 +1,10 @@ +{0 Logseq Journal} + +{1 Execution guide} + +{{!page-journal_execution_flow}Journal execution flow}: follow startup, reads, +durable writes, synchronization and resource retirement through the existing specs. + +{1 Library logseq_journal.worker_coordination} + +The App/Worker coordination API is {!module-Journal_worker_coordination}. diff --git a/test/journal_documentation_test.py b/test/journal_documentation_test.py new file mode 100644 index 0000000..a5eccc8 --- /dev/null +++ b/test/journal_documentation_test.py @@ -0,0 +1,108 @@ +"""Check the real Dune @doc output, not a separately rendered copy of the guide. + +Run after `dune build @doc`: python3 -B test/journal_documentation_test.py +The build graph owns this regression; no application reducer can reproduce it. +""" + +from html.parser import HTMLParser +from pathlib import Path +import re +import unittest +from urllib.parse import unquote, urlsplit + + +ROOT = Path(__file__).resolve().parent.parent +BUILD = ROOT / "_build/default" +HTML = BUILD / "_doc/_html" +PACKAGE_INDEX = HTML / "logseq_journal/index.html" +GUIDE = HTML / "logseq_journal/journal_execution_flow.html" + + +def normalized(text): + return " ".join(text.split()) + + +class Page(HTMLParser): + def __init__(self, path): + super().__init__() + self.links = [] + self.ids = set() + self.code_blocks = [] + self.text = [] + self.pre = None + self.feed(path.read_text()) + + def handle_starttag(self, tag, attrs): + attrs = dict(attrs) + if "id" in attrs: + self.ids.add(attrs["id"]) + if tag == "a" and "href" in attrs: + self.links.append(attrs["href"]) + if tag == "pre": + self.pre = [] + + def handle_endtag(self, tag): + if tag == "pre" and self.pre is not None: + self.code_blocks.append("".join(self.pre)) + self.pre = None + + def handle_data(self, data): + self.text.append(data) + if self.pre is not None: + self.pre.append(data) + + +class JournalDocumentationTests(unittest.TestCase): + def test_generated_manual_uses_the_complete_mli_body(self): + source = (ROOT / "app/spec/journal_execution_flow.mli").read_text().strip() + self.assertTrue(source.startswith("(**") and source.endswith("*)")) + self.assertEqual(source.count("(*"), 1) + self.assertEqual(source.count("*)"), 1) + generated = BUILD / "app/spec/journal_execution_flow.mld" + self.assertTrue(generated.is_file(), "@doc did not generate the guide manual") + self.assertEqual( + generated.read_text().strip(), + "{0 Journal execution flow}\n\n" + source[3:-2].strip(), + ) + self.assertFalse((ROOT / "app/spec/journal_execution_flow.mld").exists(), + "The generated manual must not become a second source") + + def test_package_index_links_to_guide_and_existing_api(self): + self.assertTrue(PACKAGE_INDEX.is_file(), "@doc package index is missing") + links = Page(PACKAGE_INDEX).links + self.assertIn("journal_execution_flow.html", links) + self.assertIn("Journal_worker_coordination/index.html", links) + self.assertTrue(GUIDE.is_file(), "The guide link must resolve to generated HTML") + self.assertIn("logseq_journal/index.html", Page(HTML / "index.html").links) + + def test_rendered_guide_keeps_all_sections_and_examples(self): + self.assertTrue(GUIDE.is_file(), "@doc did not render the guide") + source = (ROOT / "app/spec/journal_execution_flow.mli").read_text() + page = Page(GUIDE) + rendered = normalized(" ".join(page.text)) + headings = re.findall(r"\{[12] ([^}]+)\}", source) + for heading in headings: + with self.subTest(heading=heading): + self.assertIn(normalized(heading), rendered) + blocks = re.findall(r"\{\[(.*?)\]\}|\{v(.*?)v\}", source, re.S) + expected = [normalized(code or trace) for code, trace in blocks] + self.assertEqual([normalized(code) for code in page.code_blocks], expected) + self.assertIn("It explains the major paths, not every UI branch, protocol error, " + "asset codec or storage implementation detail.", rendered) + + def test_index_and_guide_local_links_resolve(self): + for path in (PACKAGE_INDEX, GUIDE): + self.assertTrue(path.is_file(), f"Missing generated page: {path.name}") + for href in Page(path).links: + link = urlsplit(href) + if link.scheme or link.netloc: + continue + target = (path.parent / unquote(link.path)).resolve() if link.path else path + with self.subTest(page=path.name, href=href): + self.assertTrue(target.is_file(), f"Broken local link: {href}") + if link.fragment: + self.assertIn(unquote(link.fragment), Page(target).ids) + + +if __name__ == "__main__": + unittest.main() From 7bd2d0b0ef0a1bea51d9620cbdb1b1ae55745c19 Mon Sep 17 00:00:00 2001 From: rcmerci Date: Sat, 7 Nov 2026 21:41:50 +0800 Subject: [PATCH 2/2] docs: remove documentation-specific tests --- app/spec/README.md | 4 +- app/spec/dune | 4 +- test/journal_documentation_test.py | 108 ----------------------------- 3 files changed, 3 insertions(+), 113 deletions(-) delete mode 100644 test/journal_documentation_test.py diff --git a/app/spec/README.md b/app/spec/README.md index e28ad96..180cc7e 100644 --- a/app/spec/README.md +++ b/app/spec/README.md @@ -7,9 +7,7 @@ Run `dune build @doc` and open `_build/default/_doc/_html/logseq_journal/index.html`. The package homepage links to the execution guide and the coordination API. The guide's `.mld` page is generated from its single `.mli` documentation comment; -edit only the `.mli` body. After building, run -`python3 -B test/journal_documentation_test.py` from the repository root to check -the generated page, all examples and local navigation links. +edit only the `.mli` body. Each area under this directory is an independent wrapped Dune virtual library. Do not use recursive `include_subdirs` to merge future specifications into one diff --git a/app/spec/dune b/app/spec/dune index b26d233..2f60a9f 100644 --- a/app/spec/dune +++ b/app/spec/dune @@ -1,5 +1,5 @@ -; Keep the documentation-only .mli as the sole body source. Its single comment -; is checked by source_boundary_test; no OCaml module or library is added here. +; Keep the documentation-only .mli as the sole body source. +; Extract its single comment without adding an OCaml module or library. (rule (target journal_execution_flow.mld) diff --git a/test/journal_documentation_test.py b/test/journal_documentation_test.py deleted file mode 100644 index a5eccc8..0000000 --- a/test/journal_documentation_test.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Check the real Dune @doc output, not a separately rendered copy of the guide. - -Run after `dune build @doc`: python3 -B test/journal_documentation_test.py -The build graph owns this regression; no application reducer can reproduce it. -""" - -from html.parser import HTMLParser -from pathlib import Path -import re -import unittest -from urllib.parse import unquote, urlsplit - - -ROOT = Path(__file__).resolve().parent.parent -BUILD = ROOT / "_build/default" -HTML = BUILD / "_doc/_html" -PACKAGE_INDEX = HTML / "logseq_journal/index.html" -GUIDE = HTML / "logseq_journal/journal_execution_flow.html" - - -def normalized(text): - return " ".join(text.split()) - - -class Page(HTMLParser): - def __init__(self, path): - super().__init__() - self.links = [] - self.ids = set() - self.code_blocks = [] - self.text = [] - self.pre = None - self.feed(path.read_text()) - - def handle_starttag(self, tag, attrs): - attrs = dict(attrs) - if "id" in attrs: - self.ids.add(attrs["id"]) - if tag == "a" and "href" in attrs: - self.links.append(attrs["href"]) - if tag == "pre": - self.pre = [] - - def handle_endtag(self, tag): - if tag == "pre" and self.pre is not None: - self.code_blocks.append("".join(self.pre)) - self.pre = None - - def handle_data(self, data): - self.text.append(data) - if self.pre is not None: - self.pre.append(data) - - -class JournalDocumentationTests(unittest.TestCase): - def test_generated_manual_uses_the_complete_mli_body(self): - source = (ROOT / "app/spec/journal_execution_flow.mli").read_text().strip() - self.assertTrue(source.startswith("(**") and source.endswith("*)")) - self.assertEqual(source.count("(*"), 1) - self.assertEqual(source.count("*)"), 1) - generated = BUILD / "app/spec/journal_execution_flow.mld" - self.assertTrue(generated.is_file(), "@doc did not generate the guide manual") - self.assertEqual( - generated.read_text().strip(), - "{0 Journal execution flow}\n\n" + source[3:-2].strip(), - ) - self.assertFalse((ROOT / "app/spec/journal_execution_flow.mld").exists(), - "The generated manual must not become a second source") - - def test_package_index_links_to_guide_and_existing_api(self): - self.assertTrue(PACKAGE_INDEX.is_file(), "@doc package index is missing") - links = Page(PACKAGE_INDEX).links - self.assertIn("journal_execution_flow.html", links) - self.assertIn("Journal_worker_coordination/index.html", links) - self.assertTrue(GUIDE.is_file(), "The guide link must resolve to generated HTML") - self.assertIn("logseq_journal/index.html", Page(HTML / "index.html").links) - - def test_rendered_guide_keeps_all_sections_and_examples(self): - self.assertTrue(GUIDE.is_file(), "@doc did not render the guide") - source = (ROOT / "app/spec/journal_execution_flow.mli").read_text() - page = Page(GUIDE) - rendered = normalized(" ".join(page.text)) - headings = re.findall(r"\{[12] ([^}]+)\}", source) - for heading in headings: - with self.subTest(heading=heading): - self.assertIn(normalized(heading), rendered) - blocks = re.findall(r"\{\[(.*?)\]\}|\{v(.*?)v\}", source, re.S) - expected = [normalized(code or trace) for code, trace in blocks] - self.assertEqual([normalized(code) for code in page.code_blocks], expected) - self.assertIn("It explains the major paths, not every UI branch, protocol error, " - "asset codec or storage implementation detail.", rendered) - - def test_index_and_guide_local_links_resolve(self): - for path in (PACKAGE_INDEX, GUIDE): - self.assertTrue(path.is_file(), f"Missing generated page: {path.name}") - for href in Page(path).links: - link = urlsplit(href) - if link.scheme or link.netloc: - continue - target = (path.parent / unquote(link.path)).resolve() if link.path else path - with self.subTest(page=path.name, href=href): - self.assertTrue(target.is_file(), f"Broken local link: {href}") - if link.fragment: - self.assertIn(unquote(link.fragment), Page(target).ids) - - -if __name__ == "__main__": - unittest.main()