From e70a42a3b346551d629d69db2c4bb37cb6879532 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 28 Jun 2026 21:52:16 +0200 Subject: [PATCH 1/4] Add mdast snapshot assertions plan Describe the approved path for replacing documentation snapshot checks with normalized mdast JSON assertions in both the parent template tests and generated Rust projects. --- docs/execplans/mdast-snapshot-assertions.md | 282 ++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 docs/execplans/mdast-snapshot-assertions.md diff --git a/docs/execplans/mdast-snapshot-assertions.md b/docs/execplans/mdast-snapshot-assertions.md new file mode 100644 index 0000000..bd3f265 --- /dev/null +++ b/docs/execplans/mdast-snapshot-assertions.md @@ -0,0 +1,282 @@ +# Validate Documentation Snapshots with mdast JSON + +This ExecPlan (execution plan) is a living document. The sections +`Constraints`, `Tolerances`, `Risks`, `Progress`, `Surprises & Discoveries`, +`Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work +proceeds. + +Status: DRAFT + +## Purpose / Big Picture + +Documentation snapshot tests should fail when the meaning or structure of a +Markdown document changes, not when line wrapping, table alignment, or other +formatting-only details change. Markdown formatting is already checked by +`markdownlint-cli2` and `mdtablefix`; snapshot tests should instead compare a +normalized Markdown abstract syntax tree. + +After this change, the parent template tests and the tests rendered into +generated Rust projects will parse documentation files into mdast JSON. An +mdast document is the Markdown abstract syntax tree used by the unified +ecosystem: headings, paragraphs, lists, links, tables, code blocks, and other +document nodes represented as structured JSON. The snapshot tests will remove +position and offset fields before snapshotting so formatting changes do not +churn snapshots. A reviewer can observe success by changing only line wrapping +in a documented file and seeing the mdast snapshot remain stable, then changing +a heading, link target, list item, table cell, or code block content and seeing +the focused snapshot fail. + +This plan does not derive from a roadmap task. It is a direct change request +for the `mdast-snapshot-assertions` branch. + +## Constraints + +The branch for this work is `mdast-snapshot-assertions`, tracking +`origin/mdast-snapshot-assertions`. The plan file is +`docs/execplans/mdast-snapshot-assertions.md`. + +This repository is a Copier template. Files under `template/` are rendered +into downstream Rust projects, while files under `tests/` validate rendered +output through `pytest-copier`. Any generated-project test behaviour must be +implemented in `template/` and proven from the parent repository tests. + +Follow the repository guidance in `AGENTS.md`: prefer Makefile targets, run +long gates through `tee` into `/tmp`, do not run format, lint, or test suites in +parallel, and commit after each completed and gated change. + +The work must validate document semantics, not Markdown formatting. Do not use +raw Markdown text, bytes, or binary file snapshots for documentation semantics. +Do not remove `markdownlint-cli2` or `mdtablefix`; those tools remain +responsible for formatting. + +Snapshot JSON must be deterministic and reviewable. It must omit parser source +locations such as byte offsets, line numbers, and column numbers. It must not +include filesystem-specific absolute paths, temporary directory names, or other +nondeterministic values. + +Parent test dependencies must be declared in the Makefile's `uvx --with ...` +invocation before imported in tests. Generated Rust test dependencies must be +declared in `template/Cargo.toml.jinja` so rendered projects can run their +public test gates without manual setup. + +The initial ExecPlan draft is an approval artifact only. Do not implement the +mdast conversion until the user explicitly approves this plan or asks for +revisions. + +## Tolerances (Exception Triggers) + +Stop and ask for direction if a Markdown parser cannot preserve GitHub +Flavoured Markdown constructs that this template relies on, such as tables, +task-list items, footnotes, fenced code blocks, or inline HTML. + +Stop and ask for direction if supporting true mdast JSON requires adding a +runtime dependency outside the parent Python test environment and the generated +Rust dev-dependency set. For example, do not introduce a Node, Bun, or npm +snapshot pipeline without approval. + +Stop and ask for direction if the generated project `make test` or parent +repository `make test` cannot pass within one 1200 second command and cannot be +split into smaller public gates. + +Stop and ask for direction before deleting existing snapshot coverage rather +than replacing it with equivalent mdast JSON assertions. + +Stop and ask for direction if the implementation requires changing generated +project public commands in a way unrelated to documentation snapshot +semantics. + +## Risks + +Python and Rust Markdown parsers may not emit identical mdast shapes. The plan +does not require identical implementations across languages; it requires both +parent and generated-project tests to snapshot normalized JSON whose nodes +represent document semantics. The mitigation is to define a small local +normalization contract in each environment and snapshot those normalized +objects, not parser-private representations. + +The existing parent snapshot test in +`tests/test_template/test_snapshots.py` covers structured tooling files rather +than documentation files. Implementation may discover separate documentation +assertions instead of a direct replacement there. The mitigation is to search +for every documentation snapshot or raw Markdown snapshot assertion before +editing, then add the minimum missing coverage if no documentation snapshot +test currently exists. + +Generated projects currently contain guidance about using `insta` snapshots, +but they may not yet render documentation snapshot tests. The mitigation is to +add generated tests only where they prove this requested behaviour and to keep +them focused on documentation files under `docs/`. + +Markdown parser dependency versions may change after this plan is drafted. The +mitigation is to resolve current compatible versions during implementation +using the package manager commands already used by the repository, then record +the selected versions in this plan's `Decision Log`. + +Snapshot updates can accidentally bless a formatting-sensitive representation. +The mitigation is to add an explicit regression test that parses two Markdown +strings with different wrapping but the same semantics and asserts their +normalized mdast JSON is equal. + +## Progress + +- [x] 2026-06-28: Loaded the `leta` skill first as required for code + navigation, then loaded the `execplans` skill for this draft and the + `rust-router` skill for generated Rust test planning. +- [x] 2026-06-28: Confirmed the starting branch was `session/aab84371`, not + `main`, and renamed it locally to `mdast-snapshot-assertions`. +- [x] 2026-06-28: Inspected `AGENTS.md`, `docs/developers-guide.md`, + `template/AGENTS.md.jinja`, `tests/test_template/test_snapshots.py`, and + current snapshot files to identify the relevant test surfaces. +- [x] 2026-06-28: Drafted this ExecPlan as a review artifact before + implementation. +- [x] 2026-06-28: Ran `make test 2>&1 | tee + /tmp/test-agent-template-rust-mdast-snapshot-assertions-plan.out`; result was + 50 passed and 1 skipped. +- [ ] Obtain explicit user approval for implementation. +- [ ] Establish failing parent-repository tests for normalized mdast snapshots. +- [ ] Establish failing generated-project tests for normalized mdast snapshots. +- [ ] Implement parent-repository mdast normalization and snapshot assertions. +- [ ] Implement generated Rust-project mdast normalization and snapshot + assertions. +- [ ] Update documentation for the new semantic snapshot contract. +- [ ] Run formatting, linting, and test gates sequentially with `/tmp` logs. +- [ ] Commit the approved implementation changes after gates pass. + +## Surprises & Discoveries + +The parent repository has one existing syrupy snapshot file at +`tests/test_template/__snapshots__/test_snapshots.ambr`, but that snapshot +currently covers structured tooling outputs such as Cargo config, Makefile +text, and GitHub Actions workflows. The obvious documentation-related parent +tests are currently contract assertions in `tests/helpers/tooling_contracts/` +rather than Markdown document snapshots. + +The generated template contains guidance in `template/AGENTS.md.jinja` that +recommends `insta` snapshots for stable output boundaries, but the rendered +template currently has no obvious generated `insta` documentation snapshot +test file. + +The repository documentation already states that parent tests use `syrupy` for +generated structured file snapshots. That wording will need to remain true for +tooling snapshots while new wording explains mdast JSON snapshots for +documentation semantics. + +## Decision Log + +Use normalized mdast JSON as the documentation snapshot contract rather than +raw Markdown. This follows the request directly and separates semantic +assertions from formatting checks owned by `markdownlint-cli2` and +`mdtablefix`. + +Keep parent and generated-project implementations local to their runtimes. The +parent repository should use a Python helper under `tests/helpers/` so pytest +tests can snapshot JSON-compatible objects with syrupy. Generated Rust projects +should use Rust dev-dependencies and `insta` JSON snapshots so `make test` +continues to be the public generated-project gate. + +Treat parser source positions as formatting noise. Normalization must remove +`position`, `start`, `end`, `offset`, `line`, and `column` style fields before +snapshot comparison. + +Keep raw tooling-file snapshots out of scope unless they are documentation +snapshots. Existing snapshots for `Makefile`, `.cargo/config.toml`, and +GitHub Actions workflows validate structured tooling output, not Markdown +documentation semantics. + +## Implementation Plan + +Begin by inventorying current snapshot and documentation assertions. Search +`tests/`, `template/`, and generated output for `snapshot`, `insta`, +`assert_snapshot`, `docs/`, and Markdown file reads. Record every in-scope +documentation snapshot assertion in this plan. If no generated-project +documentation snapshot test exists, create one as new coverage rather than +pretending an existing test was converted. + +For the parent repository red step, add a helper test that proves +normalization is semantic. The test should parse two Markdown snippets that +have equivalent structure but different line wrapping or table spacing and +assert the normalized mdast JSON objects are equal. Add or update a +documentation snapshot test that renders a project, reads a representative +generated documentation file such as `docs/contents.md` or the relevant +section of `docs/repository-layout.md`, converts it to normalized mdast JSON, +and compares that JSON-compatible object with a syrupy snapshot. Run the +focused pytest command and confirm it fails for the expected reason before +implementation. + +For the parent repository green step, add a Markdown-to-mdast helper under +`tests/helpers/`, using a Python dependency declared in the Makefile's +`uvx --with ...` list. The helper should expose a small API such as +`parse_markdown_semantics(markdown: str) -> object`. It should parse Markdown, +convert the parser result into plain dictionaries and lists, and recursively +remove source-position fields. Re-run the focused pytest command until it +passes, then update the syrupy snapshot only after checking that the JSON +contains meaningful headings, links, lists, tables, and code block nodes. + +For the generated-project red step, add a rendered Rust test file under +`template/tests/` that reads one or more generated documentation files with +`include_str!` or `std::fs`, parses them to normalized mdast JSON, and compares +the result with `insta` JSON snapshots. Also add a unit test that parses two +formatting variants of the same Markdown and asserts their normalized JSON is +equal. Render a temporary project through the parent tests and run the +generated public test gate, expecting it to fail before the Rust helper and +snapshots exist. + +For the generated-project green step, add the required Rust dev-dependencies +to `template/Cargo.toml.jinja`. Prefer a Rust Markdown parser that can produce +mdast-compatible nodes and serialize them through `serde_json`. Add a small +normalization helper inside the generated test module that removes source +positions and returns deterministic JSON. Keep snapshots scoped to +documentation semantics, for example headings and link structure in +`docs/contents.md` plus the repository tree section in +`docs/repository-layout.md`. Re-run the generated public gate until it passes. + +For refactor, remove duplication between parent pytest assertions if multiple +documentation files need the same normalization. In generated Rust tests, keep +helper functions private to the test module unless another generated test +needs them. Update `docs/developers-guide.md` and the generated +`template/docs/developers-guide.md.jinja` if contributor workflow or snapshot +update instructions change. + +## Validation + +Run focused parent tests first, logging to `/tmp`: + +```sh +uvx \ + --with pytest-copier \ + --with pyyaml \ + --with syrupy \ + --with make-parser \ + --with hypothesis \ + pytest tests/test_template -k 'snapshot or documentation' \ + 2>&1 | tee /tmp/test-mdast-snapshot-assertions-focused-parent.out +``` + +The red run should fail because the expected normalized mdast snapshot or +helper behaviour does not exist yet. The green run should pass after the +helper and snapshots are implemented. + +Render and validate a generated project through the parent test harness rather +than hand-editing rendered output. If a focused parent test is added for the +generated documentation snapshot test, run it with `tee` and confirm it invokes +the generated public test gate. + +Run the full parent gate after focused checks pass: + +```sh +make test 2>&1 | tee /tmp/test-agent-template-rust-mdast-snapshot-assertions.out +``` + +Expected result: pytest reports all tests passing. If output is truncated, +inspect the `/tmp` log before committing. + +If documentation files or generated Markdown tooling instructions change, run +the generated or parent formatting and Markdown checks through their public +Makefile targets where available, sequentially and with `/tmp` logs. + +## Outcomes & Retrospective + +This section is intentionally empty in the draft. During implementation, record +the final parser choices, snapshot files added or changed, gates run, log +paths, commit hashes, and any lessons about parser compatibility or snapshot +scope. From 02006b46c4f83442ce7dd762445b19875dc25f42 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 28 Jun 2026 22:18:35 +0200 Subject: [PATCH 2/4] Add parent mdast documentation snapshots Parse generated Markdown documentation into normalized mdast JSON before snapshotting so wrapping and table alignment changes do not churn semantic snapshots. --- Makefile | 2 +- docs/execplans/mdast-snapshot-assertions.md | 49 +- tests/helpers/markdown_semantics.py | 51 + tests/test_helpers.py | 4 +- .../test_documentation_snapshots.ambr | 1069 +++++++++++++++++ .../test_documentation_snapshots.py | 63 + 6 files changed, 1231 insertions(+), 7 deletions(-) create mode 100644 tests/helpers/markdown_semantics.py create mode 100644 tests/test_template/__snapshots__/test_documentation_snapshots.ambr create mode 100644 tests/test_template/test_documentation_snapshots.py diff --git a/Makefile b/Makefile index 6d09da7..7158a95 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ test: ## Run template tests echo "uvx is required to run template tests. Install uv from https://docs.astral.sh/uv/getting-started/installation/" >&2; \ exit 1; \ fi - $(ACT_TEST_ENV) $(UV) --with pytest-copier --with pyyaml --with syrupy --with make-parser --with hypothesis pytest tests/ + $(ACT_TEST_ENV) $(UV) --with pytest-copier --with pyyaml --with syrupy --with make-parser --with hypothesis --with mdast pytest tests/ help: ## Show available targets @grep -E '^[a-zA-Z_-]+:.*?##' $(MAKEFILE_LIST) | \ diff --git a/docs/execplans/mdast-snapshot-assertions.md b/docs/execplans/mdast-snapshot-assertions.md index bd3f265..3f6dcac 100644 --- a/docs/execplans/mdast-snapshot-assertions.md +++ b/docs/execplans/mdast-snapshot-assertions.md @@ -5,7 +5,7 @@ This ExecPlan (execution plan) is a living document. The sections `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. -Status: DRAFT +Status: IMPLEMENTING ## Purpose / Big Picture @@ -132,10 +132,33 @@ normalized mdast JSON is equal. - [x] 2026-06-28: Ran `make test 2>&1 | tee /tmp/test-agent-template-rust-mdast-snapshot-assertions-plan.out`; result was 50 passed and 1 skipped. -- [ ] Obtain explicit user approval for implementation. -- [ ] Establish failing parent-repository tests for normalized mdast snapshots. +- [x] 2026-06-28: Received explicit user approval to implement the planned + functionality and began execution. +- [x] 2026-06-28: Removed the `Plan: ` prefix from PR #43 and renamed the + Lody session to `Validate documentation snapshots with mdast JSON`. +- [x] 2026-06-28: Inventoried snapshot and documentation assertion surfaces. + The only existing syrupy snapshot test is + `tests/test_template/test_snapshots.py`, covering generated tooling files. + Existing generated documentation assertions live in + `tests/helpers/tooling_contracts/documentation.py` and are raw string + contract checks rather than semantic snapshots. No generated-project + `insta` documentation snapshot tests currently exist. +- [x] 2026-06-28: Established the parent red test by adding + `tests/test_template/test_documentation_snapshots.py` before the helper + existed. The focused run + `uvx --with pytest-copier --with pyyaml --with syrupy --with make-parser + --with hypothesis pytest tests/test_template -k 'snapshot or documentation' + 2>&1 | tee /tmp/test-mdast-snapshot-assertions-focused-parent-red.out` + failed during collection with `ModuleNotFoundError: + tests.helpers.markdown_semantics`. +- [x] 2026-06-28: Implemented the parent mdast helper in + `tests/helpers/markdown_semantics.py`, declared `mdast` in the parent + `Makefile` uvx dependency list, updated the parent Makefile contract test, + generated `tests/test_template/__snapshots__/test_documentation_snapshots.ambr`, + and reran the focused parent gate with `--with mdast`; result was 3 passed, + 19 deselected. - [ ] Establish failing generated-project tests for normalized mdast snapshots. -- [ ] Implement parent-repository mdast normalization and snapshot assertions. +- [x] Implement parent-repository mdast normalization and snapshot assertions. - [ ] Implement generated Rust-project mdast normalization and snapshot assertions. - [ ] Update documentation for the new semantic snapshot contract. @@ -161,6 +184,17 @@ generated structured file snapshots. That wording will need to remain true for tooling snapshots while new wording explains mdast JSON snapshots for documentation semantics. +Python package discovery found a `mdast` package that emits unified-style +mdast dictionaries and supports opt-in GitHub Flavoured Markdown extensions +for tables, task-list items, footnotes, strikethrough, and autolink literals. +Those extension flags must be enabled explicitly; otherwise tables and +task-list checkboxes are parsed as plain paragraph or text content. + +Rust crate discovery found `markdown = 1.0.0`, the Rust `markdown-rs` crate, +which describes itself as CommonMark compliant with ASTs and extensions and +offers a `json` feature backed by `serde`. `insta = 1.48.0` is the current +snapshot crate version and has a `json` feature for JSON snapshots. + ## Decision Log Use normalized mdast JSON as the documentation snapshot contract rather than @@ -183,6 +217,13 @@ snapshots. Existing snapshots for `Makefile`, `.cargo/config.toml`, and GitHub Actions workflows validate structured tooling output, not Markdown documentation semantics. +Use Python `mdast` for the parent pytest helper, with GitHub Flavoured +Markdown extensions enabled through `ParseOptions`. Declare it in the parent +`Makefile` `uvx --with ...` invocation before importing it in tests. Use Rust +`markdown = { version = "1.0.0", features = ["json"] }`, `serde_json`, and +`insta = { version = "1.48.0", features = ["json"] }` for generated-project +documentation snapshot tests. + ## Implementation Plan Begin by inventorying current snapshot and documentation assertions. Search diff --git a/tests/helpers/markdown_semantics.py b/tests/helpers/markdown_semantics.py new file mode 100644 index 0000000..936de6c --- /dev/null +++ b/tests/helpers/markdown_semantics.py @@ -0,0 +1,51 @@ +"""Normalize Markdown documents as mdast JSON for semantic assertions.""" + +from __future__ import annotations + +import re +from typing import Any + +import mdast + +_POSITION_KEYS = frozenset({"position", "line", "column", "offset"}) +_SOFT_WHITESPACE = re.compile(r"[ \t\r\n]+") + + +def parse_markdown_semantics(markdown: str) -> object: + """Parse Markdown into normalized mdast JSON.""" + tree = mdast.md_to_ast(markdown, config=_gfm_parse_options()) + return _normalise_node(tree) + + +def _gfm_parse_options() -> mdast.ParseOptions: + return mdast.ParseOptions( + gfm_autolink_literal=True, + gfm_footnote_definition=True, + gfm_label_start_footnote=True, + gfm_strikethrough=True, + gfm_table=True, + gfm_task_list_item=True, + ) + + +def _normalise_node(value: Any) -> object: + if isinstance(value, dict): + normalised = {} + node_type = value.get("type") + for key, child in value.items(): + if key in _POSITION_KEYS: + continue + if key == "value" and node_type == "text" and isinstance(child, str): + normalised[key] = _normalise_soft_text(child) + continue + normalised[key] = _normalise_node(child) + return normalised + + if isinstance(value, list): + return [_normalise_node(child) for child in value] + + return value + + +def _normalise_soft_text(value: str) -> str: + return _SOFT_WHITESPACE.sub(" ", value) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 0b29068..9580030 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -403,10 +403,10 @@ def test_parent_makefile_test_target_contract() -> None: ) assert ( "$(UV) --with pytest-copier --with pyyaml --with syrupy --with make-parser " - "--with hypothesis pytest tests/" + "--with hypothesis --with mdast pytest tests/" ) in makefile, ( "expected parent Makefile test target to run pytest through $(UV) with " - "pytest-copier, pyyaml, syrupy, make-parser, and hypothesis" + "pytest-copier, pyyaml, syrupy, make-parser, hypothesis, and mdast" ) diff --git a/tests/test_template/__snapshots__/test_documentation_snapshots.ambr b/tests/test_template/__snapshots__/test_documentation_snapshots.ambr new file mode 100644 index 0000000..eb8d305 --- /dev/null +++ b/tests/test_template/__snapshots__/test_documentation_snapshots.ambr @@ -0,0 +1,1069 @@ +# serializer version: 1 +# name: test_generated_documentation_semantic_snapshots + dict({ + 'docs/contents.md': dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Documentation contents', + }), + ]), + 'depth': 1, + 'type': 'heading', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Documentation contents', + }), + ]), + 'type': 'link', + 'url': 'contents.md', + }), + dict({ + 'type': 'text', + 'value': " is the index for DocumentationSnapshotExample's documentation set.", + }), + ]), + 'type': 'paragraph', + }), + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Project guides', + }), + ]), + 'depth': 2, + 'type': 'heading', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'User guide', + }), + ]), + 'type': 'link', + 'url': 'users-guide.md', + }), + dict({ + 'type': 'text', + 'value': ' explains how to use the generated project and its public build and test commands.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Developer guide', + }), + ]), + 'type': 'link', + 'url': 'developers-guide.md', + }), + dict({ + 'type': 'text', + 'value': ' explains the local workflow and implementation tooling for contributors.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Repository layout', + }), + ]), + 'type': 'link', + 'url': 'repository-layout.md', + }), + dict({ + 'type': 'text', + 'value': " explains the generated project's top-level files, directories, and ownership boundaries.", + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Documentation style guide', + }), + ]), + 'type': 'link', + 'url': 'documentation-style-guide.md', + }), + dict({ + 'type': 'text', + 'value': ' defines the spelling, structure, Markdown, Architecture Decision Record (ADR), Request for Comments (RFC), and roadmap conventions used by this documentation set.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + ]), + 'ordered': False, + 'spread': False, + 'type': 'list', + }), + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Rust reference material', + }), + ]), + 'depth': 2, + 'type': 'heading', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Reliable testing in Rust via dependency injection', + }), + ]), + 'type': 'link', + 'url': 'reliable-testing-in-rust-via-dependency-injection.md', + }), + dict({ + 'type': 'text', + 'value': ' explains how to keep tests deterministic by injecting environment, clock, filesystem, and other external dependencies.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': "Rust doctest Don't Repeat Yourself guide", + }), + ]), + 'type': 'link', + 'url': 'rust-doctest-dry-guide.md', + }), + dict({ + 'type': 'text', + 'value': ' explains how to write maintainable, executable Rust documentation examples.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Rust testing with ', + }), + dict({ + 'type': 'inlineCode', + 'value': 'rstest', + }), + dict({ + 'type': 'text', + 'value': ' fixtures', + }), + ]), + 'type': 'link', + 'url': 'rust-testing-with-rstest-fixtures.md', + }), + dict({ + 'type': 'text', + 'value': ' explains fixture-based, parameterized, and asynchronous testing with ', + }), + dict({ + 'type': 'inlineCode', + 'value': 'rstest', + }), + dict({ + 'type': 'text', + 'value': '.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + ]), + 'ordered': False, + 'spread': False, + 'type': 'list', + }), + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Engineering practice', + }), + ]), + 'depth': 2, + 'type': 'heading', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Complexity antipatterns and refactoring strategies', + }), + ]), + 'type': 'link', + 'url': 'complexity-antipatterns-and-refactoring-strategies.md', + }), + dict({ + 'type': 'text', + 'value': ' explains cognitive complexity, the bumpy-road antipattern, and refactoring approaches for maintainable code.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Scripting standards', + }), + ]), + 'type': 'link', + 'url': 'scripting-standards.md', + }), + dict({ + 'type': 'text', + 'value': ' explains the preferred Python scripting stack, command execution patterns, and test expectations for helper scripts.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + ]), + 'ordered': False, + 'spread': False, + 'type': 'list', + }), + ]), + 'type': 'root', + }), + 'docs/repository-layout.md': dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Repository layout', + }), + ]), + 'depth': 1, + 'type': 'heading', + }), + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'This document describes the generated DocumentationSnapshotExample repository layout. It is the canonical reference for where source code, tests, configuration, automation, and long-lived documentation belong.', + }), + ]), + 'type': 'paragraph', + }), + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Top-level tree', + }), + ]), + 'depth': 2, + 'type': 'heading', + }), + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'The tree below shows the generated repository structure. It is intentionally compact and omits build output such as ', + }), + dict({ + 'type': 'inlineCode', + 'value': 'target/', + }), + dict({ + 'type': 'text', + 'value': '.', + }), + ]), + 'type': 'paragraph', + }), + dict({ + 'lang': 'plaintext', + 'type': 'code', + 'value': ''' + . + ├── .cargo/ + │ └── config.toml + ├── .github/ + │ ├── dependabot.yml + │ └── workflows/ + │ ├── act-validation.yml + │ ├── ci.yml + + │ └── release.yml + + ├── docs/ + │ ├── contents.md + │ ├── developers-guide.md + │ ├── repository-layout.md + │ ├── users-guide.md + │ └── ... + ├── src/ + + │ ├── lib.rs + │ └── main.rs + + ├── tests/ + │ └── stub.rs + ├── AGENTS.md + ├── Cargo.toml + ├── LICENSE + ├── Makefile + ├── README.md + ├── clippy.toml + ├── codecov.yml + └── rust-toolchain.toml + ''', + }), + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Path responsibilities', + }), + ]), + 'depth': 2, + 'type': 'heading', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': '.cargo/config.toml', + }), + dict({ + 'type': 'text', + 'value': ': Configures Cargo defaults for local development, including Linux linker and code-generation settings.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': '.github/dependabot.yml', + }), + dict({ + 'type': 'text', + 'value': ': Configures automated dependency update checks.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': '.github/workflows/act-validation.yml', + }), + dict({ + 'type': 'text', + 'value': ': Runs the generated workflow validation through ', + }), + dict({ + 'type': 'inlineCode', + 'value': 'act', + }), + dict({ + 'type': 'text', + 'value': ' separately from main CI.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': '.github/workflows/ci.yml', + }), + dict({ + 'type': 'text', + 'value': ": Runs the generated project's continuous integration checks.", + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': '.github/workflows/release.yml', + }), + dict({ + 'type': 'text', + 'value': ': Builds and publishes binary release artefacts for the application flavour.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': 'docs/', + }), + dict({ + 'type': 'text', + 'value': ': Holds long-lived reference documentation, guides, style rules, and design material.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': 'docs/contents.md', + }), + dict({ + 'type': 'text', + 'value': ': Indexes the documentation set and should be updated when documentation files are added, renamed, or removed.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': 'docs/users-guide.md', + }), + dict({ + 'type': 'text', + 'value': ': Explains how to use the generated project and its public build and test commands.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': 'docs/developers-guide.md', + }), + dict({ + 'type': 'text', + 'value': ': Explains the contributor workflow and local tooling used to work on the generated project.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': 'docs/repository-layout.md', + }), + dict({ + 'type': 'text', + 'value': ': Documents the repository tree and path responsibilities.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': 'src/lib.rs', + }), + dict({ + 'type': 'text', + 'value': ': Contains library support for application logic and doctested examples.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': 'src/main.rs', + }), + dict({ + 'type': 'text', + 'value': ': Contains the application entrypoint and top-level executable wiring.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': 'tests/', + }), + dict({ + 'type': 'text', + 'value': ': Holds integration and behavioural tests that exercise public behaviour.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': 'tests/stub.rs', + }), + dict({ + 'type': 'text', + 'value': ': Keeps the generated test directory valid until real tests replace it.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': 'AGENTS.md', + }), + dict({ + 'type': 'text', + 'value': ': Provides repository-specific working instructions for agents and contributors.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': 'Cargo.toml', + }), + dict({ + 'type': 'text', + 'value': ': Defines package metadata, dependencies, lint policy, and Cargo configuration.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': 'LICENSE', + }), + dict({ + 'type': 'text', + 'value': ': Records the project licence text.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': 'Makefile', + }), + dict({ + 'type': 'text', + 'value': ': Provides the public build, lint, test, coverage, and documentation validation commands.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': 'README.md', + }), + dict({ + 'type': 'text', + 'value': ': Introduces the project and gives the shortest useful getting-started path.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': 'clippy.toml', + }), + dict({ + 'type': 'text', + 'value': ': Configures Clippy lint behaviour that is not expressed directly in ', + }), + dict({ + 'type': 'inlineCode', + 'value': 'Cargo.toml', + }), + dict({ + 'type': 'text', + 'value': '.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': 'codecov.yml', + }), + dict({ + 'type': 'text', + 'value': ': Configures coverage reporting behaviour.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'inlineCode', + 'value': 'rust-toolchain.toml', + }), + dict({ + 'type': 'text', + 'value': ': Pins the Rust toolchain channel and required components.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + ]), + 'ordered': False, + 'spread': True, + 'type': 'list', + }), + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Ownership boundaries', + }), + ]), + 'depth': 2, + 'type': 'heading', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Keep generated source code under ', + }), + dict({ + 'type': 'inlineCode', + 'value': 'src/', + }), + dict({ + 'type': 'text', + 'value': '. Add modules below ', + }), + dict({ + 'type': 'inlineCode', + 'value': 'src/', + }), + dict({ + 'type': 'text', + 'value': ' when a feature grows beyond a small entrypoint or crate root.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Keep black-box integration tests and externally observable workflow tests under ', + }), + dict({ + 'type': 'inlineCode', + 'value': 'tests/', + }), + dict({ + 'type': 'text', + 'value': '.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Keep reusable documentation under ', + }), + dict({ + 'type': 'inlineCode', + 'value': 'docs/', + }), + dict({ + 'type': 'text', + 'value': '. Update ', + }), + dict({ + 'type': 'inlineCode', + 'value': 'docs/contents.md', + }), + dict({ + 'type': 'text', + 'value': ' whenever a documentation file is added, renamed, or removed.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Keep build and validation entrypoints in ', + }), + dict({ + 'type': 'inlineCode', + 'value': 'Makefile', + }), + dict({ + 'type': 'text', + 'value': '; prefer adding or extending a Make target over documenting an ad hoc command.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Keep continuous integration workflow changes under ', + }), + dict({ + 'type': 'inlineCode', + 'value': '.github/workflows/', + }), + dict({ + 'type': 'text', + 'value': ' and dependency-update policy under ', + }), + dict({ + 'type': 'inlineCode', + 'value': '.github/dependabot.yml', + }), + dict({ + 'type': 'text', + 'value': '.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + dict({ + 'children': list([ + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Do not commit generated build output such as ', + }), + dict({ + 'type': 'inlineCode', + 'value': 'target/', + }), + dict({ + 'type': 'text', + 'value': ', coverage artefacts, or local editor state.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'spread': False, + 'type': 'listItem', + }), + ]), + 'ordered': False, + 'spread': False, + 'type': 'list', + }), + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Updating this document', + }), + ]), + 'depth': 2, + 'type': 'heading', + }), + dict({ + 'children': list([ + dict({ + 'type': 'text', + 'value': 'Update this document when the repository gains a new top-level directory, a new long-lived documentation category, a new workflow file, or a changed ownership boundary that would otherwise make the tree misleading.', + }), + ]), + 'type': 'paragraph', + }), + ]), + 'type': 'root', + }), + }) +# --- diff --git a/tests/test_template/test_documentation_snapshots.py b/tests/test_template/test_documentation_snapshots.py new file mode 100644 index 0000000..6691e7a --- /dev/null +++ b/tests/test_template/test_documentation_snapshots.py @@ -0,0 +1,63 @@ +"""Rendered documentation semantic snapshot tests.""" + +from __future__ import annotations + +from pathlib import Path + +from pytest_copier.plugin import CopierFixture +from syrupy.assertion import SnapshotAssertion + +from tests.helpers.markdown_semantics import parse_markdown_semantics +from tests.helpers.rendering import APP, render_project + + +def test_markdown_semantics_ignore_formatting_noise() -> None: + """Equivalent Markdown formatting yields the same normalized mdast JSON.""" + compact = """\ +# Documentation + +The generated project links to [docs](docs/contents.md) and explains +the workflow in one paragraph. + +| Path | Purpose | +| - | - | +| `docs/contents.md` | Index | + +- [x] Keep snapshots semantic +""" + reflowed = """\ +# Documentation + +The generated project links to [docs](docs/contents.md) and explains the +workflow in one paragraph. + +| Path | Purpose | +| -------------------- | ------- | +| `docs/contents.md` | Index | + +- [x] Keep snapshots semantic +""" + + assert parse_markdown_semantics(compact) == parse_markdown_semantics(reflowed) + + +def test_generated_documentation_semantic_snapshots( + tmp_path: Path, copier: CopierFixture, snapshot: SnapshotAssertion +) -> None: + """Generated documentation matches reviewed normalized mdast snapshots.""" + project = render_project( + tmp_path, + copier, + project_name="DocumentationSnapshotExample", + package_name="documentation_snapshot_example", + flavour=APP, + ) + + assert { + "docs/contents.md": parse_markdown_semantics( + (project / "docs/contents.md").read_text(encoding="utf-8") + ), + "docs/repository-layout.md": parse_markdown_semantics( + (project / "docs/repository-layout.md").read_text(encoding="utf-8") + ), + } == snapshot From 437431503282fc1b9355151ced98059feb013499 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 28 Jun 2026 22:30:56 +0200 Subject: [PATCH 3/4] Add generated mdast documentation snapshots Add generated Rust tests that parse documentation with markdown-rs and compare normalized mdast JSON with insta snapshots. Document the snapshot workflow for template and generated-project contributors. --- docs/developers-guide.md | 12 +- docs/execplans/mdast-snapshot-assertions.md | 52 +- template/Cargo.toml.jinja | 5 + template/docs/developers-guide.md.jinja | 13 + template/tests/documentation_snapshots.rs | 106 +++ ...tation_snapshots__docs_contents.snap.jinja | 330 ++++++++ ...ots__docs_repository_layout_app.snap.jinja | 709 ++++++++++++++++++ ...ots__docs_repository_layout_lib.snap.jinja | 671 +++++++++++++++++ 8 files changed, 1892 insertions(+), 6 deletions(-) create mode 100644 template/tests/documentation_snapshots.rs create mode 100644 template/tests/snapshots/documentation_snapshots__docs_contents.snap.jinja create mode 100644 template/tests/snapshots/documentation_snapshots__docs_repository_layout_app.snap.jinja create mode 100644 template/tests/snapshots/documentation_snapshots__docs_repository_layout_lib.snap.jinja diff --git a/docs/developers-guide.md b/docs/developers-guide.md index ea9469b..68655aa 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -13,7 +13,7 @@ make test ``` The target uses -`uvx --with pytest-copier --with pyyaml --with syrupy --with make-parser --with hypothesis pytest tests/`, +`uvx --with pytest-copier --with pyyaml --with syrupy --with make-parser --with hypothesis --with mdast pytest tests/`, so Python test dependencies must be added to that invocation before tests import them. Keep long runs logged through `tee` into `/tmp`, following the example in `AGENTS.md`. @@ -22,6 +22,13 @@ The tests render both library and application projects, run generated public gates such as `make all`, validate generated Makefiles with `mbake`, and parse generated `Cargo.toml` files as TOML. +Documentation snapshot tests parse Markdown files into normalized mdast JSON +before comparing snapshots. The normalization removes parser source positions +and treats soft line wrapping as formatting noise, while preserving document +structure such as headings, links, lists, tables, inline code, and code blocks. +Markdown formatting remains covered separately by `markdownlint-cli2` and +`mdtablefix`. + Generated audit coverage is tested without network access by replacing Cargo with a fake executable. The regression verifies that `make rust-audit` derives the workspace root from `cargo metadata`, ignores manifests outside workspace @@ -55,6 +62,7 @@ projects: - `syrupy` for generated structured file snapshots in tests. - `make-parser` for generated Makefile structure assertions in tests. - `hypothesis` for generated-file schema helper property tests. +- `mdast` for parent documentation semantic snapshot tests. - Rust and Cargo through `rustup`. - cargo-nextest for generated fast test execution in CI, while generated Makefiles still fall back to `cargo test` for contributors. @@ -87,6 +95,8 @@ Reusable test support lives under `tests/helpers/`: project command helpers. - `tests/helpers/generated_files.py` centralizes generated text, TOML, and YAML parsing with pytest failure messages. +- `tests/helpers/markdown_semantics.py` parses Markdown as normalized mdast JSON + for semantic documentation snapshots. - `tests/helpers/tooling_contracts/` contains generated Makefile, Cargo, documentation, CI, release, and coverage-action contract assertions. - `tests/conftest.py` provides the session-scoped `act_ready` fixture, which diff --git a/docs/execplans/mdast-snapshot-assertions.md b/docs/execplans/mdast-snapshot-assertions.md index 3f6dcac..a2b87db 100644 --- a/docs/execplans/mdast-snapshot-assertions.md +++ b/docs/execplans/mdast-snapshot-assertions.md @@ -157,13 +157,32 @@ normalized mdast JSON is equal. generated `tests/test_template/__snapshots__/test_documentation_snapshots.ambr`, and reran the focused parent gate with `--with mdast`; result was 3 passed, 19 deselected. -- [ ] Establish failing generated-project tests for normalized mdast snapshots. +- [x] 2026-06-28: Established the generated red test by adding + `template/tests/documentation_snapshots.rs` without declaring its + dev-dependencies. After one formatting-only correction, the focused generated + gate + `uvx --with pytest-copier --with pyyaml --with syrupy --with make-parser + --with hypothesis --with mdast pytest tests/test_template/test_tooling_contracts.py + -x 2>&1 | tee /tmp/test-mdast-snapshot-assertions-generated-red-2.out` + failed during generated `make all` with unresolved `serde_json`, `insta`, + and `markdown` crates. - [x] Implement parent-repository mdast normalization and snapshot assertions. -- [ ] Implement generated Rust-project mdast normalization and snapshot +- [x] 2026-06-28: Implemented generated Rust-project mdast normalization, + `insta` JSON snapshots, and Rust dev-dependencies. The focused generated + gate passed across lib, app, and alternate-target lib variants: + `uvx --with pytest-copier --with pyyaml --with syrupy --with make-parser + --with hypothesis --with mdast pytest tests/test_template/test_tooling_contracts.py + 2>&1 | tee /tmp/test-mdast-snapshot-assertions-generated-green-6.out`; + result was 3 passed. +- [x] Implement generated Rust-project mdast normalization and snapshot assertions. -- [ ] Update documentation for the new semantic snapshot contract. -- [ ] Run formatting, linting, and test gates sequentially with `/tmp` logs. -- [ ] Commit the approved implementation changes after gates pass. +- [x] Update documentation for the new semantic snapshot contract. +- [x] 2026-06-28: Ran the full parent gate after parent and generated + implementation: + `make test 2>&1 | tee /tmp/test-agent-template-rust-mdast-snapshot-assertions-full.out`; + result was 52 passed and 1 skipped in 121.45 seconds. +- [x] Run formatting, linting, and test gates sequentially with `/tmp` logs. +- [x] Commit the approved implementation changes after gates pass. ## Surprises & Discoveries @@ -195,6 +214,17 @@ which describes itself as CommonMark compliant with ASTs and extensions and offers a `json` feature backed by `serde`. `insta = 1.48.0` is the current snapshot crate version and has a `json` feature for JSON snapshots. +The Rust `markdown::to_mdast` API returns `markdown::message::Message` on +failure, and that message type does not implement `std::error::Error`. +Generated tests must convert it explicitly at the helper boundary rather than +using `?` directly into `Box`. + +Generated repository-layout documentation differs between library and +application flavours. A single rendered `insta` snapshot cannot cover both +flavours; generated tests now choose `docs_repository_layout_lib` or +`docs_repository_layout_app` based on whether the rendered layout mentions the +release workflow. + ## Decision Log Use normalized mdast JSON as the documentation snapshot contract rather than @@ -224,6 +254,11 @@ Markdown extensions enabled through `ParseOptions`. Declare it in the parent `insta = { version = "1.48.0", features = ["json"] }` for generated-project documentation snapshot tests. +Use separate generated repository-layout snapshots for library and application +flavours. This preserves flavour-specific documentation semantics rather than +normalizing away public files such as `.github/workflows/release.yml` and +`src/main.rs`. + ## Implementation Plan Begin by inventorying current snapshot and documentation assertions. Search @@ -321,3 +356,10 @@ This section is intentionally empty in the draft. During implementation, record the final parser choices, snapshot files added or changed, gates run, log paths, commit hashes, and any lessons about parser compatibility or snapshot scope. + +Parent mdast snapshot assertions are implemented with Python `mdast` and +stored in `tests/test_template/__snapshots__/test_documentation_snapshots.ambr`. +Generated Rust projects use `markdown = 1.0.0`, `serde_json`, and +`insta = 1.48.0` to parse and snapshot selected documentation files. Generated +repository-layout snapshots are split by flavour so app-only paths remain part +of the semantic contract. diff --git a/template/Cargo.toml.jinja b/template/Cargo.toml.jinja index 69f4e91..19e207e 100644 --- a/template/Cargo.toml.jinja +++ b/template/Cargo.toml.jinja @@ -14,6 +14,11 @@ categories = [{% for category in package_category_values %}"{{ category }}"{% if [dependencies] +[dev-dependencies] +insta = { version = "1.48.0", features = ["json"] } +markdown = { version = "1.0.0", features = ["json"] } +serde_json = "1.0" + {% if flavour == 'app' -%} # The package.metadata.binstall pkg-url intentionally mixes Jinja2 values # rendered at project generation with single-brace cargo-binstall variables diff --git a/template/docs/developers-guide.md.jinja b/template/docs/developers-guide.md.jinja index 21dd314..2636227 100644 --- a/template/docs/developers-guide.md.jinja +++ b/template/docs/developers-guide.md.jinja @@ -17,6 +17,15 @@ The main `.github/workflows/ci.yml` workflow deliberately does not run `make test WITH_ACT=1`; the separate Act workflow runs those slower container-backed checks in parallel. +Documentation snapshot tests live in `tests/documentation_snapshots.rs`. They +parse selected Markdown files into normalized mdast JSON before comparing +`insta` snapshots, so line wrapping and table alignment changes do not churn +semantic snapshots. Markdown formatting is still checked by `make markdownlint` +and formatted by `make fmt`. When a documentation meaning or structure change +is intentional, update the affected `tests/snapshots/*.snap` files with +`INSTA_UPDATE=always cargo test --test documentation_snapshots`, then review +the JSON diff before committing it. + ## Tooling Development builds use Cranelift for debug code generation. On Linux targets, @@ -27,6 +36,10 @@ LLVM-compatible linker behaviour. Install `clang`, `lld`, `mold`, `python3`, and `cargo-audit` before running the full generated workflow locally on Linux. +The documentation snapshot tests use the `markdown`, `serde_json`, and `insta` +dev-dependencies declared in `Cargo.toml`; no Node or npm Markdown snapshot +pipeline is required. + ### Security audit ignores Security audit jobs may set `CARGO_AUDIT_IGNORES` for narrowly scoped diff --git a/template/tests/documentation_snapshots.rs b/template/tests/documentation_snapshots.rs new file mode 100644 index 0000000..c8983eb --- /dev/null +++ b/template/tests/documentation_snapshots.rs @@ -0,0 +1,106 @@ +//! Semantic documentation snapshot tests. + +use serde_json::Value; + +fn parse_markdown_semantics(markdown: &str) -> Result> { + let tree = markdown::to_mdast(markdown, &markdown::ParseOptions::gfm()) + .map_err(|message| std::io::Error::other(message.to_string()))?; + let mut value = serde_json::to_value(tree)?; + normalise_node(&mut value); + Ok(value) +} + +fn normalise_node(value: &mut Value) { + match value { + Value::Object(object) => { + let node_type = object + .get("type") + .and_then(Value::as_str) + .map(str::to_owned); + object.remove("position"); + object.remove("line"); + object.remove("column"); + object.remove("offset"); + + if node_type.as_deref() == Some("text") + && let Some(Value::String(text)) = object.get_mut("value") + { + *text = normalise_soft_text(text); + } + + for child in object.values_mut() { + normalise_node(child); + } + } + Value::Array(children) => { + for child in children { + normalise_node(child); + } + } + _ => {} + } +} + +fn normalise_soft_text(value: &str) -> String { + value.split_whitespace().collect::>().join(" ") +} + +fn parse_markdown_semantics_or_panic(markdown: &str) -> Value { + match parse_markdown_semantics(markdown) { + Ok(value) => value, + Err(error) => panic!("failed to parse Markdown semantics: {error}"), + } +} + +#[test] +fn markdown_semantics_ignore_formatting_noise() { + let compact = "\ +# Documentation + +The generated project links to [docs](docs/contents.md) and explains +the workflow in one paragraph. + +| Path | Purpose | +| - | - | +| `docs/contents.md` | Index | + +- [x] Keep snapshots semantic +"; + let reflowed = "\ +# Documentation + +The generated project links to [docs](docs/contents.md) and explains the +workflow in one paragraph. + +| Path | Purpose | +| -------------------- | ------- | +| `docs/contents.md` | Index | + +- [x] Keep snapshots semantic +"; + + assert_eq!( + parse_markdown_semantics_or_panic(compact), + parse_markdown_semantics_or_panic(reflowed) + ); +} + +#[test] +fn generated_documentation_matches_semantic_snapshots() { + let repository_layout = include_str!("../docs/repository-layout.md"); + let repository_layout_snapshot = if repository_layout.contains(".github/workflows/release.yml") + { + "docs_repository_layout_app" + } else { + "docs_repository_layout_lib" + }; + + insta::assert_json_snapshot!( + "docs_contents", + parse_markdown_semantics_or_panic(include_str!("../docs/contents.md")) + ); + insta::assert_json_snapshot!( + repository_layout_snapshot, + parse_markdown_semantics_or_panic(repository_layout) + ); +} diff --git a/template/tests/snapshots/documentation_snapshots__docs_contents.snap.jinja b/template/tests/snapshots/documentation_snapshots__docs_contents.snap.jinja new file mode 100644 index 0000000..0d49113 --- /dev/null +++ b/template/tests/snapshots/documentation_snapshots__docs_contents.snap.jinja @@ -0,0 +1,330 @@ +--- +source: tests/documentation_snapshots.rs +expression: "parse_markdown_semantics_or_panic(include_str!(\"../docs/contents.md\"))" +--- +{ + "children": [ + { + "children": [ + { + "type": "text", + "value": "Documentation contents" + } + ], + "depth": 1, + "type": "heading" + }, + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Documentation contents" + } + ], + "type": "link", + "url": "contents.md" + }, + { + "type": "text", + "value": "is the index for {{ project_name }}'s documentation set." + } + ], + "type": "paragraph" + }, + { + "children": [ + { + "type": "text", + "value": "Project guides" + } + ], + "depth": 2, + "type": "heading" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "User guide" + } + ], + "type": "link", + "url": "users-guide.md" + }, + { + "type": "text", + "value": "explains how to use the generated project and its public build and test commands." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Developer guide" + } + ], + "type": "link", + "url": "developers-guide.md" + }, + { + "type": "text", + "value": "explains the local workflow and implementation tooling for contributors." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Repository layout" + } + ], + "type": "link", + "url": "repository-layout.md" + }, + { + "type": "text", + "value": "explains the generated project's top-level files, directories, and ownership boundaries." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Documentation style guide" + } + ], + "type": "link", + "url": "documentation-style-guide.md" + }, + { + "type": "text", + "value": "defines the spelling, structure, Markdown, Architecture Decision Record (ADR), Request for Comments (RFC), and roadmap conventions used by this documentation set." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + } + ], + "ordered": false, + "spread": false, + "type": "list" + }, + { + "children": [ + { + "type": "text", + "value": "Rust reference material" + } + ], + "depth": 2, + "type": "heading" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Reliable testing in Rust via dependency injection" + } + ], + "type": "link", + "url": "reliable-testing-in-rust-via-dependency-injection.md" + }, + { + "type": "text", + "value": "explains how to keep tests deterministic by injecting environment, clock, filesystem, and other external dependencies." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Rust doctest Don't Repeat Yourself guide" + } + ], + "type": "link", + "url": "rust-doctest-dry-guide.md" + }, + { + "type": "text", + "value": "explains how to write maintainable, executable Rust documentation examples." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Rust testing with" + }, + { + "type": "inlineCode", + "value": "rstest" + }, + { + "type": "text", + "value": "fixtures" + } + ], + "type": "link", + "url": "rust-testing-with-rstest-fixtures.md" + }, + { + "type": "text", + "value": "explains fixture-based, parameterized, and asynchronous testing with" + }, + { + "type": "inlineCode", + "value": "rstest" + }, + { + "type": "text", + "value": "." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + } + ], + "ordered": false, + "spread": false, + "type": "list" + }, + { + "children": [ + { + "type": "text", + "value": "Engineering practice" + } + ], + "depth": 2, + "type": "heading" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Complexity antipatterns and refactoring strategies" + } + ], + "type": "link", + "url": "complexity-antipatterns-and-refactoring-strategies.md" + }, + { + "type": "text", + "value": "explains cognitive complexity, the bumpy-road antipattern, and refactoring approaches for maintainable code." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Scripting standards" + } + ], + "type": "link", + "url": "scripting-standards.md" + }, + { + "type": "text", + "value": "explains the preferred Python scripting stack, command execution patterns, and test expectations for helper scripts." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + } + ], + "ordered": false, + "spread": false, + "type": "list" + } + ], + "type": "root" +} diff --git a/template/tests/snapshots/documentation_snapshots__docs_repository_layout_app.snap.jinja b/template/tests/snapshots/documentation_snapshots__docs_repository_layout_app.snap.jinja new file mode 100644 index 0000000..15f16cd --- /dev/null +++ b/template/tests/snapshots/documentation_snapshots__docs_repository_layout_app.snap.jinja @@ -0,0 +1,709 @@ +--- +source: tests/documentation_snapshots.rs +expression: "parse_markdown_semantics_or_panic(include_str!(\"../docs/repository-layout.md\"))" +--- +{ + "children": [ + { + "children": [ + { + "type": "text", + "value": "Repository layout" + } + ], + "depth": 1, + "type": "heading" + }, + { + "children": [ + { + "type": "text", + "value": "This document describes the generated {{ project_name }} repository layout. It is the canonical reference for where source code, tests, configuration, automation, and long-lived documentation belong." + } + ], + "type": "paragraph" + }, + { + "children": [ + { + "type": "text", + "value": "Top-level tree" + } + ], + "depth": 2, + "type": "heading" + }, + { + "children": [ + { + "type": "text", + "value": "The tree below shows the generated repository structure. It is intentionally compact and omits build output such as" + }, + { + "type": "inlineCode", + "value": "target/" + }, + { + "type": "text", + "value": "." + } + ], + "type": "paragraph" + }, + { + "lang": "plaintext", + "type": "code", + "value": ".\n├── .cargo/\n│ └── config.toml\n├── .github/\n│ ├── dependabot.yml\n│ └── workflows/\n│ ├── act-validation.yml\n│ ├── ci.yml\n\n│ └── release.yml\n\n├── docs/\n│ ├── contents.md\n│ ├── developers-guide.md\n│ ├── repository-layout.md\n│ ├── users-guide.md\n│ └── ...\n├── src/\n\n│ ├── lib.rs\n│ └── main.rs\n\n├── tests/\n│ └── stub.rs\n├── AGENTS.md\n├── Cargo.toml\n├── LICENSE\n├── Makefile\n├── README.md\n├── clippy.toml\n├── codecov.yml\n└── rust-toolchain.toml" + }, + { + "children": [ + { + "type": "text", + "value": "Path responsibilities" + } + ], + "depth": 2, + "type": "heading" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": ".cargo/config.toml" + }, + { + "type": "text", + "value": ": Configures Cargo defaults for local development, including Linux linker and code-generation settings." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": ".github/dependabot.yml" + }, + { + "type": "text", + "value": ": Configures automated dependency update checks." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": ".github/workflows/act-validation.yml" + }, + { + "type": "text", + "value": ": Runs the generated workflow validation through" + }, + { + "type": "inlineCode", + "value": "act" + }, + { + "type": "text", + "value": "separately from main CI." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": ".github/workflows/ci.yml" + }, + { + "type": "text", + "value": ": Runs the generated project's continuous integration checks." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": ".github/workflows/release.yml" + }, + { + "type": "text", + "value": ": Builds and publishes binary release artefacts for the application flavour." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "docs/" + }, + { + "type": "text", + "value": ": Holds long-lived reference documentation, guides, style rules, and design material." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "docs/contents.md" + }, + { + "type": "text", + "value": ": Indexes the documentation set and should be updated when documentation files are added, renamed, or removed." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "docs/users-guide.md" + }, + { + "type": "text", + "value": ": Explains how to use the generated project and its public build and test commands." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "docs/developers-guide.md" + }, + { + "type": "text", + "value": ": Explains the contributor workflow and local tooling used to work on the generated project." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "docs/repository-layout.md" + }, + { + "type": "text", + "value": ": Documents the repository tree and path responsibilities." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "src/lib.rs" + }, + { + "type": "text", + "value": ": Contains library support for application logic and doctested examples." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "src/main.rs" + }, + { + "type": "text", + "value": ": Contains the application entrypoint and top-level executable wiring." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "tests/" + }, + { + "type": "text", + "value": ": Holds integration and behavioural tests that exercise public behaviour." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "tests/stub.rs" + }, + { + "type": "text", + "value": ": Keeps the generated test directory valid until real tests replace it." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "AGENTS.md" + }, + { + "type": "text", + "value": ": Provides repository-specific working instructions for agents and contributors." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "Cargo.toml" + }, + { + "type": "text", + "value": ": Defines package metadata, dependencies, lint policy, and Cargo configuration." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "LICENSE" + }, + { + "type": "text", + "value": ": Records the project licence text." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "Makefile" + }, + { + "type": "text", + "value": ": Provides the public build, lint, test, coverage, and documentation validation commands." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "README.md" + }, + { + "type": "text", + "value": ": Introduces the project and gives the shortest useful getting-started path." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "clippy.toml" + }, + { + "type": "text", + "value": ": Configures Clippy lint behaviour that is not expressed directly in" + }, + { + "type": "inlineCode", + "value": "Cargo.toml" + }, + { + "type": "text", + "value": "." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "codecov.yml" + }, + { + "type": "text", + "value": ": Configures coverage reporting behaviour." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "rust-toolchain.toml" + }, + { + "type": "text", + "value": ": Pins the Rust toolchain channel and required components." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + } + ], + "ordered": false, + "spread": true, + "type": "list" + }, + { + "children": [ + { + "type": "text", + "value": "Ownership boundaries" + } + ], + "depth": 2, + "type": "heading" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Keep generated source code under" + }, + { + "type": "inlineCode", + "value": "src/" + }, + { + "type": "text", + "value": ". Add modules below" + }, + { + "type": "inlineCode", + "value": "src/" + }, + { + "type": "text", + "value": "when a feature grows beyond a small entrypoint or crate root." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Keep black-box integration tests and externally observable workflow tests under" + }, + { + "type": "inlineCode", + "value": "tests/" + }, + { + "type": "text", + "value": "." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Keep reusable documentation under" + }, + { + "type": "inlineCode", + "value": "docs/" + }, + { + "type": "text", + "value": ". Update" + }, + { + "type": "inlineCode", + "value": "docs/contents.md" + }, + { + "type": "text", + "value": "whenever a documentation file is added, renamed, or removed." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Keep build and validation entrypoints in" + }, + { + "type": "inlineCode", + "value": "Makefile" + }, + { + "type": "text", + "value": "; prefer adding or extending a Make target over documenting an ad hoc command." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Keep continuous integration workflow changes under" + }, + { + "type": "inlineCode", + "value": ".github/workflows/" + }, + { + "type": "text", + "value": "and dependency-update policy under" + }, + { + "type": "inlineCode", + "value": ".github/dependabot.yml" + }, + { + "type": "text", + "value": "." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Do not commit generated build output such as" + }, + { + "type": "inlineCode", + "value": "target/" + }, + { + "type": "text", + "value": ", coverage artefacts, or local editor state." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + } + ], + "ordered": false, + "spread": false, + "type": "list" + }, + { + "children": [ + { + "type": "text", + "value": "Updating this document" + } + ], + "depth": 2, + "type": "heading" + }, + { + "children": [ + { + "type": "text", + "value": "Update this document when the repository gains a new top-level directory, a new long-lived documentation category, a new workflow file, or a changed ownership boundary that would otherwise make the tree misleading." + } + ], + "type": "paragraph" + } + ], + "type": "root" +} diff --git a/template/tests/snapshots/documentation_snapshots__docs_repository_layout_lib.snap.jinja b/template/tests/snapshots/documentation_snapshots__docs_repository_layout_lib.snap.jinja new file mode 100644 index 0000000..7acded0 --- /dev/null +++ b/template/tests/snapshots/documentation_snapshots__docs_repository_layout_lib.snap.jinja @@ -0,0 +1,671 @@ +--- +source: tests/documentation_snapshots.rs +expression: "parse_markdown_semantics_or_panic(include_str!(\"../docs/repository-layout.md\"))" +--- +{ + "children": [ + { + "children": [ + { + "type": "text", + "value": "Repository layout" + } + ], + "depth": 1, + "type": "heading" + }, + { + "children": [ + { + "type": "text", + "value": "This document describes the generated {{ project_name }} repository layout. It is the canonical reference for where source code, tests, configuration, automation, and long-lived documentation belong." + } + ], + "type": "paragraph" + }, + { + "children": [ + { + "type": "text", + "value": "Top-level tree" + } + ], + "depth": 2, + "type": "heading" + }, + { + "children": [ + { + "type": "text", + "value": "The tree below shows the generated repository structure. It is intentionally compact and omits build output such as" + }, + { + "type": "inlineCode", + "value": "target/" + }, + { + "type": "text", + "value": "." + } + ], + "type": "paragraph" + }, + { + "lang": "plaintext", + "type": "code", + "value": ".\n├── .cargo/\n│ └── config.toml\n├── .github/\n│ ├── dependabot.yml\n│ └── workflows/\n│ ├── act-validation.yml\n│ ├── ci.yml\n\n├── docs/\n│ ├── contents.md\n│ ├── developers-guide.md\n│ ├── repository-layout.md\n│ ├── users-guide.md\n│ └── ...\n├── src/\n\n│ └── lib.rs\n\n├── tests/\n│ └── stub.rs\n├── AGENTS.md\n├── Cargo.toml\n├── LICENSE\n├── Makefile\n├── README.md\n├── clippy.toml\n├── codecov.yml\n└── rust-toolchain.toml" + }, + { + "children": [ + { + "type": "text", + "value": "Path responsibilities" + } + ], + "depth": 2, + "type": "heading" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": ".cargo/config.toml" + }, + { + "type": "text", + "value": ": Configures Cargo defaults for local development, including Linux linker and code-generation settings." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": ".github/dependabot.yml" + }, + { + "type": "text", + "value": ": Configures automated dependency update checks." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": ".github/workflows/act-validation.yml" + }, + { + "type": "text", + "value": ": Runs the generated workflow validation through" + }, + { + "type": "inlineCode", + "value": "act" + }, + { + "type": "text", + "value": "separately from main CI." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": ".github/workflows/ci.yml" + }, + { + "type": "text", + "value": ": Runs the generated project's continuous integration checks." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "docs/" + }, + { + "type": "text", + "value": ": Holds long-lived reference documentation, guides, style rules, and design material." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "docs/contents.md" + }, + { + "type": "text", + "value": ": Indexes the documentation set and should be updated when documentation files are added, renamed, or removed." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "docs/users-guide.md" + }, + { + "type": "text", + "value": ": Explains how to use the generated project and its public build and test commands." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "docs/developers-guide.md" + }, + { + "type": "text", + "value": ": Explains the contributor workflow and local tooling used to work on the generated project." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "docs/repository-layout.md" + }, + { + "type": "text", + "value": ": Documents the repository tree and path responsibilities." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "src/lib.rs" + }, + { + "type": "text", + "value": ": Contains the library crate root and exported public API surface." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "tests/" + }, + { + "type": "text", + "value": ": Holds integration and behavioural tests that exercise public behaviour." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "tests/stub.rs" + }, + { + "type": "text", + "value": ": Keeps the generated test directory valid until real tests replace it." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "AGENTS.md" + }, + { + "type": "text", + "value": ": Provides repository-specific working instructions for agents and contributors." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "Cargo.toml" + }, + { + "type": "text", + "value": ": Defines package metadata, dependencies, lint policy, and Cargo configuration." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "LICENSE" + }, + { + "type": "text", + "value": ": Records the project licence text." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "Makefile" + }, + { + "type": "text", + "value": ": Provides the public build, lint, test, coverage, and documentation validation commands." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "README.md" + }, + { + "type": "text", + "value": ": Introduces the project and gives the shortest useful getting-started path." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "clippy.toml" + }, + { + "type": "text", + "value": ": Configures Clippy lint behaviour that is not expressed directly in" + }, + { + "type": "inlineCode", + "value": "Cargo.toml" + }, + { + "type": "text", + "value": "." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "codecov.yml" + }, + { + "type": "text", + "value": ": Configures coverage reporting behaviour." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "inlineCode", + "value": "rust-toolchain.toml" + }, + { + "type": "text", + "value": ": Pins the Rust toolchain channel and required components." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + } + ], + "ordered": false, + "spread": true, + "type": "list" + }, + { + "children": [ + { + "type": "text", + "value": "Ownership boundaries" + } + ], + "depth": 2, + "type": "heading" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Keep generated source code under" + }, + { + "type": "inlineCode", + "value": "src/" + }, + { + "type": "text", + "value": ". Add modules below" + }, + { + "type": "inlineCode", + "value": "src/" + }, + { + "type": "text", + "value": "when a feature grows beyond a small entrypoint or crate root." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Keep black-box integration tests and externally observable workflow tests under" + }, + { + "type": "inlineCode", + "value": "tests/" + }, + { + "type": "text", + "value": "." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Keep reusable documentation under" + }, + { + "type": "inlineCode", + "value": "docs/" + }, + { + "type": "text", + "value": ". Update" + }, + { + "type": "inlineCode", + "value": "docs/contents.md" + }, + { + "type": "text", + "value": "whenever a documentation file is added, renamed, or removed." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Keep build and validation entrypoints in" + }, + { + "type": "inlineCode", + "value": "Makefile" + }, + { + "type": "text", + "value": "; prefer adding or extending a Make target over documenting an ad hoc command." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Keep continuous integration workflow changes under" + }, + { + "type": "inlineCode", + "value": ".github/workflows/" + }, + { + "type": "text", + "value": "and dependency-update policy under" + }, + { + "type": "inlineCode", + "value": ".github/dependabot.yml" + }, + { + "type": "text", + "value": "." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + }, + { + "children": [ + { + "children": [ + { + "type": "text", + "value": "Do not commit generated build output such as" + }, + { + "type": "inlineCode", + "value": "target/" + }, + { + "type": "text", + "value": ", coverage artefacts, or local editor state." + } + ], + "type": "paragraph" + } + ], + "spread": false, + "type": "listItem" + } + ], + "ordered": false, + "spread": false, + "type": "list" + }, + { + "children": [ + { + "type": "text", + "value": "Updating this document" + } + ], + "depth": 2, + "type": "heading" + }, + { + "children": [ + { + "type": "text", + "value": "Update this document when the repository gains a new top-level directory, a new long-lived documentation category, a new workflow file, or a changed ownership boundary that would otherwise make the tree misleading." + } + ], + "type": "paragraph" + } + ], + "type": "root" +} From 8910cf72552365fb1ca2a3966cd0156f52075c5a Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 28 Jun 2026 22:36:48 +0200 Subject: [PATCH 4/4] Record mdast snapshot implementation outcome Mark the execplan complete and capture the shipped parser choices, validation evidence, commits, and CodeRabbit review results. --- docs/execplans/mdast-snapshot-assertions.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/execplans/mdast-snapshot-assertions.md b/docs/execplans/mdast-snapshot-assertions.md index a2b87db..d095033 100644 --- a/docs/execplans/mdast-snapshot-assertions.md +++ b/docs/execplans/mdast-snapshot-assertions.md @@ -5,7 +5,7 @@ This ExecPlan (execution plan) is a living document. The sections `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. -Status: IMPLEMENTING +Status: COMPLETE ## Purpose / Big Picture @@ -182,6 +182,11 @@ normalized mdast JSON is equal. `make test 2>&1 | tee /tmp/test-agent-template-rust-mdast-snapshot-assertions-full.out`; result was 52 passed and 1 skipped in 121.45 seconds. - [x] Run formatting, linting, and test gates sequentially with `/tmp` logs. +- [x] 2026-06-28: Committed parent mdast snapshot coverage in `8d2c02c` and + generated Rust-project mdast snapshot coverage in `0c0e924`. +- [x] 2026-06-28: Ran `coderabbit review --agent` after each major milestone. + Both the parent snapshot milestone and generated snapshot milestone completed + with 0 findings. - [x] Commit the approved implementation changes after gates pass. ## Surprises & Discoveries @@ -363,3 +368,13 @@ Generated Rust projects use `markdown = 1.0.0`, `serde_json`, and `insta = 1.48.0` to parse and snapshot selected documentation files. Generated repository-layout snapshots are split by flavour so app-only paths remain part of the semantic contract. + +The implemented behavior is observable in two places. In the parent template +repository, `tests/test_template/test_documentation_snapshots.py` renders a +project and snapshots normalized mdast JSON for `docs/contents.md` and +`docs/repository-layout.md` with syrupy. In generated projects, +`tests/documentation_snapshots.rs` parses the rendered documentation with +`markdown-rs`, removes source-position metadata through `serde_json`, and +compares `insta` JSON snapshots. The final full validation was +`make test 2>&1 | tee /tmp/test-agent-template-rust-mdast-snapshot-assertions-full.out`, +which passed with 52 tests and 1 skip.