DM-52076: Add technote validate command - #316
Conversation
jonathansick
left a comment
There was a problem hiding this comment.
Reviewed 740d8e3 for #312.
Well-structured and well-tested command, but TN001 only catches pydantic.ValidationError, so a syntactically malformed technote.toml raises an uncaught tomllib.TOMLDecodeError traceback instead of the documented [TN001] finding — contradicting the check's stated contract and untested. Two smaller robustness/clarity notes accompany it.
3 findings (blocking):
- [error] TN001 misses TOML syntax errors:
TechnoteToml.parse_tomlrunstomllib.loadsbefore pydantic validation, so a malformed technote.toml raisestomllib.TOMLDecodeError(a ValueError, not a pydantic ValidationError). Theexcept ValidationErrorin validate() does not catch it, so the command crashes with a traceback instead of emitting[TN001]— contradicting the CHECKS['TN001'] description and the ValidationContext docstring ('report a failure as a finding'). This is the most common bad input (a config typo) and is untested; test_invalid_schema_short_circuits only exercises the pydantic-model branch. (src/documenteer/services/technotevalidation.py) - [warning] ValidationContext.from_dir calls
toml_path.read_text()unconditionally. The CLI's--dirusesclick.Path(exists=True)on the directory only, so running validate in a directory that lacks technote.toml raises an uncaught FileNotFoundError traceback instead of a clean, coded error — the same 'traceback instead of a linter finding' problem as f1. (src/documenteer/services/technotevalidation.py) - [info] TechnoteValidationService.init takes
author_dbas a separate parameter even though the ValidationContext already carriescontext.author_db, and the service uses the separately-passed one for lookups. The CLI passes the same object twice, but this leaves two sources of truth for the same dependency and invites a mismatch (a context built with one AuthorDb, a service given another). (src/documenteer/services/technotevalidation.py)
{
"stoker_review_version": 1,
"pr_number": 316,
"head_sha": "740d8e3a2ca6611a5232d4c84215f5637a802cea",
"task_issues": [
312
],
"blocking": true,
"summary": "Well-structured and well-tested command, but TN001 only catches pydantic.ValidationError, so a syntactically malformed technote.toml raises an uncaught tomllib.TOMLDecodeError traceback instead of the documented [TN001] finding \u2014 contradicting the check's stated contract and untested. Two smaller robustness/clarity notes accompany it.",
"findings": [
{
"id": "f1",
"severity": "error",
"category": "correctness",
"file": "src/documenteer/services/technotevalidation.py",
"line_start": 174,
"line_end": 183,
"summary": "TN001 misses TOML syntax errors: `TechnoteToml.parse_toml` runs `tomllib.loads` before pydantic validation, so a malformed technote.toml raises `tomllib.TOMLDecodeError` (a ValueError, not a pydantic ValidationError). The `except ValidationError` in validate() does not catch it, so the command crashes with a traceback instead of emitting `[TN001]` \u2014 contradicting the CHECKS['TN001'] description and the ValidationContext docstring ('report a failure as a finding'). This is the most common bad input (a config typo) and is untested; test_invalid_schema_short_circuits only exercises the pydantic-model branch.",
"suggested_fix": "Broaden the handler to also catch the TOML parse error, e.g. `except (ValidationError, tomllib.TOMLDecodeError) as e:` (import `tomllib`), and add a test that feeds syntactically invalid TOML and asserts a single [TN001] finding rather than an exception."
},
{
"id": "f2",
"severity": "warning",
"category": "correctness",
"file": "src/documenteer/services/technotevalidation.py",
"line_start": 125,
"line_end": 126,
"summary": "ValidationContext.from_dir calls `toml_path.read_text()` unconditionally. The CLI's `--dir` uses `click.Path(exists=True)` on the directory only, so running validate in a directory that lacks technote.toml raises an uncaught FileNotFoundError traceback instead of a clean, coded error \u2014 the same 'traceback instead of a linter finding' problem as f1.",
"suggested_fix": "Guard for the missing file (e.g. check `toml_path.exists()` in from_dir or wrap the read) and surface it as a clear error/finding (a dedicated code, or reuse TN001) rather than letting FileNotFoundError propagate."
},
{
"id": "f3",
"severity": "info",
"category": "clarity",
"file": "src/documenteer/services/technotevalidation.py",
"line_start": 162,
"line_end": 166,
"summary": "TechnoteValidationService.__init__ takes `author_db` as a separate parameter even though the ValidationContext already carries `context.author_db`, and the service uses the separately-passed one for lookups. The CLI passes the same object twice, but this leaves two sources of truth for the same dependency and invites a mismatch (a context built with one AuthorDb, a service given another).",
"suggested_fix": "Drop the redundant parameter and read `self._context.author_db` inside the service (or, conversely, don't store author_db on the context) so there is a single source of truth."
}
]
}
jonathansick
left a comment
There was a problem hiding this comment.
Reviewed 740d8e3..66c17ab for #313.
The new check_abstract content check (TN201/TN202) is correct, well-factored, and thoroughly tested across rST/MyST/notebook formats, and it does not weaken the existing metadata tests. No correctness or security defect blocks merge; only minor, non-blocking robustness edge cases remain.
3 findings (non-blocking):
- [info] Markdown abstract-heading detection only recognizes ATX headings ('## Abstract'), not Setext headings ('Abstract' on one line underlined by '----'). A MyST/notebook technote using a Setext 'Abstract' heading would be reported as TN201 (no abstract) rather than the more precise TN202 (wrong form). Both are still errors, so the verdict is unaffected — only the code/message differs. (src/documenteer/services/technotevalidation.py)
- [info] _read_notebook_markdown does json.loads(path.read_text()) with no error handling; a malformed index.ipynb raises an uncaught json.JSONDecodeError that propagates out of validate() as a traceback instead of a clean finding. Edge case (a corrupt notebook would also fail the Sphinx build), but the validate command's UX degrades to a stack trace. (src/documenteer/services/technotevalidation.py)
- [info] content_path.read_text() (line 289) and path.read_text() (line 323) rely on the platform default encoding. A technote with non-ASCII content read under a non-UTF-8 locale could raise UnicodeDecodeError. This is consistent with the pre-existing ValidationContext.from_dir toml read, so it is not a regression — just a latent robustness gap worth noting. (src/documenteer/services/technotevalidation.py)
{
"stoker_review_version": 1,
"pr_number": 316,
"head_sha": "66c17abe41ee41949ce08240bdec9cf3bc646dbd",
"base_sha": "740d8e3a2ca6611a5232d4c84215f5637a802cea",
"task_issues": [
313
],
"blocking": false,
"summary": "The new check_abstract content check (TN201/TN202) is correct, well-factored, and thoroughly tested across rST/MyST/notebook formats, and it does not weaken the existing metadata tests. No correctness or security defect blocks merge; only minor, non-blocking robustness edge cases remain.",
"findings": [
{
"id": "f1",
"severity": "info",
"category": "correctness",
"file": "src/documenteer/services/technotevalidation.py",
"line_start": 404,
"line_end": 406,
"summary": "Markdown abstract-heading detection only recognizes ATX headings ('## Abstract'), not Setext headings ('Abstract' on one line underlined by '----'). A MyST/notebook technote using a Setext 'Abstract' heading would be reported as TN201 (no abstract) rather than the more precise TN202 (wrong form). Both are still errors, so the verdict is unaffected \u2014 only the code/message differs.",
"suggested_fix": "If Setext headings are worth distinguishing, add a check for an 'Abstract' line followed by a '---'/'===' underline in _has_markdown_abstract_heading, mirroring the rST adornment logic. Otherwise this is acceptable as-is."
},
{
"id": "f2",
"severity": "info",
"category": "correctness",
"file": "src/documenteer/services/technotevalidation.py",
"line_start": 323,
"line_end": 323,
"summary": "_read_notebook_markdown does json.loads(path.read_text()) with no error handling; a malformed index.ipynb raises an uncaught json.JSONDecodeError that propagates out of validate() as a traceback instead of a clean finding. Edge case (a corrupt notebook would also fail the Sphinx build), but the validate command's UX degrades to a stack trace.",
"suggested_fix": "Optionally wrap the json.loads in a try/except and surface a finding (e.g. reuse TN201 or a schema-style error) when the notebook can't be parsed, rather than letting the decode error escape."
},
{
"id": "f3",
"severity": "info",
"category": "correctness",
"file": "src/documenteer/services/technotevalidation.py",
"line_start": 289,
"line_end": 289,
"summary": "content_path.read_text() (line 289) and path.read_text() (line 323) rely on the platform default encoding. A technote with non-ASCII content read under a non-UTF-8 locale could raise UnicodeDecodeError. This is consistent with the pre-existing ValidationContext.from_dir toml read, so it is not a regression \u2014 just a latent robustness gap worth noting.",
"suggested_fix": "Consider passing encoding=\"utf-8\" to read_text() here (and, for consistency, at the existing toml read site) so validation is locale-independent."
}
]
}
jonathansick
left a comment
There was a problem hiding this comment.
Reviewed 73e3c0c for #312, #313, #314, #315.
Solid, well-tested addition of the technote validate command with a clean code-keyed check framework, correct severity handling, and a sound AuthorNotFoundError/ValueError split (subclass caught first). Docs match the check codes and all cross-references resolve. No blocking correctness or security issues; only minor clarity/robustness nits.
4 findings (non-blocking):
- [info] The
technote validatecommand docstring (surfaced by--help) only describes schema conformance (TN001) and author internal_id checks (TN1xx), but the command now also runs abstract content checks (TN201/TN202) and requirements checks (TN002/TN003). (src/documenteer/cli.py) - [warning]
ValidationContext.from_dirreadstechnote.tomleagerly with no existence guard. Runningdocumenteer technote validatein a directory that lacks atechnote.tomlraises an unhandledFileNotFoundError, producing a raw Python traceback instead of a clean finding/message (CI still exits non-zero, so this is UX-only, not a safety issue). There is no test for the missing-file case. (src/documenteer/services/technotevalidation.py) - [info]
TechnoteValidationService.__init__takesauthor_dbas a second argument even though theValidationContextit receives already storesauthor_db(context.author_db). The context's copy is then never used, and the two could in principle diverge. (src/documenteer/services/technotevalidation.py) - [info]
_has_rst_abstract_directivetreats an.. abstract::directive whose only indented body is a directive option (e.g.:class: foo) with no prose as non-empty, since it just checks for any indented non-blank line. This is a very unlikely edge case for technote abstracts. (src/documenteer/services/technotevalidation.py)
{
"stoker_review_version": 1,
"pr_number": 316,
"head_sha": "73e3c0cb7a4cffa0e34d53ce6b1b4ff5057013ac",
"task_issues": [
312,
313,
314,
315
],
"blocking": false,
"summary": "Solid, well-tested addition of the `technote validate` command with a clean code-keyed check framework, correct severity handling, and a sound AuthorNotFoundError/ValueError split (subclass caught first). Docs match the check codes and all cross-references resolve. No blocking correctness or security issues; only minor clarity/robustness nits.",
"findings": [
{
"id": "f1",
"severity": "info",
"category": "clarity",
"file": "src/documenteer/cli.py",
"line_start": 214,
"line_end": 220,
"summary": "The `technote validate` command docstring (surfaced by `--help`) only describes schema conformance (TN001) and author internal_id checks (TN1xx), but the command now also runs abstract content checks (TN201/TN202) and requirements checks (TN002/TN003).",
"suggested_fix": "Extend the docstring to mention the abstract and requirements.txt checks so `--help` reflects everything the command validates."
},
{
"id": "f2",
"severity": "warning",
"category": "correctness",
"file": "src/documenteer/services/technotevalidation.py",
"line_start": 163,
"line_end": 164,
"summary": "`ValidationContext.from_dir` reads `technote.toml` eagerly with no existence guard. Running `documenteer technote validate` in a directory that lacks a `technote.toml` raises an unhandled `FileNotFoundError`, producing a raw Python traceback instead of a clean finding/message (CI still exits non-zero, so this is UX-only, not a safety issue). There is no test for the missing-file case.",
"suggested_fix": "Catch the missing file and surface it as a friendly click error (or a dedicated TN00x finding) rather than letting the traceback escape, and add a test for the missing-technote.toml path."
},
{
"id": "f3",
"severity": "info",
"category": "clarity",
"file": "src/documenteer/services/technotevalidation.py",
"line_start": 204,
"line_end": 208,
"summary": "`TechnoteValidationService.__init__` takes `author_db` as a second argument even though the `ValidationContext` it receives already stores `author_db` (`context.author_db`). The context's copy is then never used, and the two could in principle diverge.",
"suggested_fix": "Drop the redundant `author_db` parameter and use `context.author_db` internally, or document why the service takes it separately."
},
{
"id": "f4",
"severity": "info",
"category": "correctness",
"file": "src/documenteer/services/technotevalidation.py",
"line_start": 435,
"line_end": 452,
"summary": "`_has_rst_abstract_directive` treats an `.. abstract::` directive whose only indented body is a directive option (e.g. `:class: foo`) with no prose as non-empty, since it just checks for any indented non-blank line. This is a very unlikely edge case for technote abstracts.",
"suggested_fix": "Optional: skip option lines (`:name: value`) when deciding whether the directive has real content. Not worth doing unless it proves to matter in practice."
}
]
}
jonathansick
left a comment
There was a problem hiding this comment.
Reviewed 73e3c0c..73e3c0c.
No new commits since the last review. The review base (73e3c0c) equals the current HEAD, so the incremental diff is empty — there is nothing new to review, and no findings are raised.
No findings (non-blocking).
{
"stoker_review_version": 1,
"pr_number": 316,
"head_sha": "73e3c0cb7a4cffa0e34d53ce6b1b4ff5057013ac",
"base_sha": "73e3c0cb7a4cffa0e34d53ce6b1b4ff5057013ac",
"blocking": false,
"summary": "No new commits since the last review. The review base (73e3c0c) equals the current HEAD, so the incremental diff is empty \u2014 there is nothing new to review, and no findings are raised.",
"findings": []
}Establish the TechnoteValidationService and its extensible check framework, wired to a new `documenteer technote validate` CLI command. This tracer-bullet slice ships schema conformance (TN001) and the author internal_id checks (TN101/TN102/TN103), so CI can fail technote builds that lack the author IDs needed for DOI generation. Key decisions: - The central CHECKS registry (a frozen Check dataclass keyed by stable code) is the single source of truth for each check's name, description, and default severity; ValidationFinding.from_check reads severity from it so a future exception-config layer has one place to consult. - ValidationContext reads technote.toml text eagerly but defers parsing to parse_toml(), letting TN001 report a pydantic ValidationError as a finding and short-circuit the author check (which needs the model). - AuthorDb.get_author now raises AuthorNotFoundError (a ValueError subclass) on HTTP 404 versus a plain ValueError on transport failure, so a genuine 404 errors (TN102) while an unreachable authordb only warns (TN103). The ValueError subclassing keeps existing `except ValueError` callers (migration) working unchanged. - The validate command splits findings into errors then warnings, each line prefixed with its stable code; --strict/-s promotes warnings to errors and the command exits non-zero on any remaining error. Next-iteration notes: - Remaining PRD checks are unshipped: TN002/TN003 (requirements.txt) and TN201/TN202 (abstract content scan). ValidationContext already discovers requirements_path and content_path for them. - Docs page under docs/technotes/, the CLAUDE.md CLI list entry, and the tox/Makefile template wiring are separate PRD scope items, not in this slice. - The `nox -s docs` build fails offline (intersphinx cannot fetch the external `technote` inventory); this is pre-existing and identical on a pristine HEAD, unrelated to this change. Closes #312
Implement check_abstract in the TechnoteValidationService: it statically
scans the technote's index.{rst,md,ipynb} content file (no Sphinx build)
for a non-empty abstract directive, distinguishing a missing abstract
(TN201) from one written as an ordinary section heading (TN202). Both
codes are registered in the central CHECKS registry as default errors.
Key decisions:
- check_abstract is a module-level function taking the ValidationContext
(matching the PRD's §Design name), so it is unit-testable per format
independent of the schema/author checks; validate() calls it after the
author check, inside the schema-valid path so an invalid schema still
short-circuits to only TN001.
- Detection is format-aware: reStructuredText (.rst) uses the .. abstract::
directive and title-underline headings; MyST (.md and .ipynb markdown
cells) uses the ```{abstract} / :::{abstract} fences and ATX headings.
".ipynb" content is flattened to the concatenated source of its markdown
cells before scanning. "Non-empty" means the directive has a non-blank
body, so an empty directive falls through to TN201.
- The TN202 message always names the ".. abstract::" directive (its
canonical form) so authors know to switch, per the acceptance criteria.
- Existing metadata/CLI tests that wrote only technote.toml now also write
an index.rst with a well-formed abstract, keeping their assertions
focused on author/schema findings rather than tripping the new TN201.
Next-iteration notes:
- Remaining PRD checks are unshipped: TN002/TN003 (requirements.txt).
ValidationContext already discovers requirements_path for them.
- Docs page under docs/technotes/, the CLAUDE.md CLI list entry, and the
tox/Makefile template wiring remain separate PRD scope items.
- `nox -s docs` still fails offline because intersphinx cannot fetch the
external `technote` inventory; pre-existing and unrelated to this change.
Closes #313
Implement check_requirements in the TechnoteValidationService: it parses the technote's requirements.txt and emits a warning when documenteer is absent or lacks the [technote] extra (TN002) or when Sphinx is pinned as a separate requirement (TN003), so CI can flag dependency drift that would otherwise break a technote build. Key decisions: - check_requirements is a module-level function taking ValidationContext (matching the check_abstract pattern), so it is unit-testable independent of the schema/author checks; validate() calls it after the abstract check, inside the schema-valid path so an invalid schema still short-circuits to only TN001. - Added ValidationContext.requirements_text, read eagerly in from_dir like toml_text (None when the file is absent, treated as empty so a missing requirements.txt trips TN002); requirements_path is kept alongside it. - Parsing uses packaging.requirements.Requirement with canonicalize_name for name matching; _parse_requirements skips blank/comment/pip-option lines and silently drops non-PEP-508 lines (editable or URL installs). packaging is now declared as a direct dependency (previously only transitive via Sphinx) and the lockfile relocked. - TN003 flags any separately-declared sphinx requirement (with or without a version specifier), since documenteer[technote] already constrains Sphinx. Both TN002 and TN003 default to warning, so drift stays exit 0 unless --strict promotes them to errors. Next-iteration notes: - Remaining PRD scope items unshipped: the docs page under docs/technotes/, the CLAUDE.md CLI list entry, and the tox/Makefile template wiring ([testenv:validate] + make validate). - Updated the shared DM-52076 changelog fragment to describe the full check set (structural TN001-TN003, metadata TN101-TN103, content TN201-TN202); the abstract checks from #313 had not been reflected there. - nox -s docs still fails offline because intersphinx cannot fetch the external technote inventory; pre-existing and unrelated to this change. Closes #314
Complete the DM-52076 validation feature with its user-facing surface: a docs page describing the command, its options, and the full check-code table, plus tox/Makefile template wiring so migrated technotes and CI can invoke it as tox run -e validate / make validate. Key decisions: - validation.rst goes in the Getting started toctree (alongside migrate), since validate is a workflow command run during authoring/CI, and cross-links author-metadata (the TN1xx checks) and migrate (which sets up the Makefile/requirements.txt/abstract the checks rely on). - The check table is rendered as a list-table grouping TN0xx structural / TN1xx metadata / TN2xx content with each code's default severity, mirror- ing the central CHECKS registry so docs and code stay in lockstep. - Options are documented as a definition list (not .. option:: / :option: roles) to stay clean under the docs build's nitpicky (-n -W) mode. - Template tox.ini/Makefile mirror the existing add-author/sync-authors env+target pairs exactly. Next-iteration notes: - This is the final task (order 4) of PRD #311; all TN0xx/TN1xx/TN2xx checks, the CLI command, docs, CLAUDE.md entry, changelog fragment, and template wiring now ship. External rubin-sphinx-technote-workflows CI wiring remains a separate out-of-scope follow-up. - nox -s docs fails offline only because intersphinx cannot fetch the external developer.lsst.io/technote inventories (network unreachable); validation.rst itself builds with zero warnings. Pre-existing and unrelated, identical on pristine HEAD. Closes #315
Report a missing technote.toml as TN004 and a syntactically invalid technote.toml as TN005, each as a short-circuiting structural error rather than a Python traceback on exactly the bad inputs the command exists to catch. ValidationContext.toml_text becomes optional (None when the file is absent), and validate() gains a TN004 -> TN005 -> TN001 short-circuit ladder. Expand the CLI --help docstring to describe all three check groups now that the structural group is complete.
Wrap the notebook markdown read in check_abstract so a corrupt index.ipynb (invalid JSON) is reported as a TN203 content error instead of surfacing as a JSONDecodeError traceback.
Note the new TN004/TN005 technote.toml checks and the TN203 notebook check, that a malformed author record is a TN102 error (TN103 is now transport-only), and that failure modes are reported as coded findings rather than tracebacks.
73e3c0c to
8126731
Compare
A schema-invalid or unparseable technote.toml no longer hides the requirements (TN002/TN003) and content (TN2xx) findings; only the author checks that need the parsed model are skipped. When the schema failure stems from a historical author-name form (single-string name, or given_names/family_names), TN001 now names the legacy form, shows the modern replacement, and points at 'documenteer technote migrate' instead of leading with the raw pydantic dump.
Split "no index content file" out of TN201 into a structural TN006 check that fires only for Sphinx technotes (a conf.py but no index file). A directory with neither a content file nor a conf.py is now treated as a technote-series repo built by another tool (e.g. SQR-104's org-mode deck): only the technote.toml-based checks (TN004/TN005/TN001 and the author checks) run, so a healthy non-Sphinx technote passes.
Distinguish an abstract directive that exists but has an empty body (new TN204, with a file:line location and format-specific advice) from a missing directive (TN201) — the empty, unindented body was the most common real-world failure in fleet testing. TN202 messages now carry the heading's line number too. The scanners also match directives case-insensitively (docutils lowercases names), accept body text on the rST marker line, follow rST/MyST include directives one level deep when hunting for the abstract, and describe the MyST syntax correctly in messages.
When an author is missing an internal_id (TN101) or declares one the author database does not know (TN102), the validator now runs a single Ook author search for that author and appends a "Did you mean '<id>'?" suggestion. A match is only offered when it is confident: the declared ORCID matches exactly one result, or exactly one result scores as a near-exact name match without a conflicting ORCID. Any failure of the suggestion lookup degrades silently to the plain message, and the TN103 transport semantics of the primary check are unchanged.
Summary
documenteer technote validatecommand (Click) that checks a technote's metadata, structure, and content so CI can fail builds with incomplete data, delegating to a newTechnoteValidationServicewith an extensible, code-keyed check framework;--strict/-spromotes warnings to errors and every finding is prefixed with its stable code.TN001) and authorinternal_idresolution — missing id (TN101), id not in the author database (TN102), unreachable database (TN103, a warning) — withAuthorDb.get_authordistinguishing a 404 (AuthorNotFoundError, errors) from a transport failure (warns only). A missing or unknown id is not a dead end: the finding appends aDid you mean '<id>'?suggestion by searching the author database (AuthorDb.search_authors, Ook's fuzzy name search) and accepting only a confident match — an exact ORCID match against the results, or a single near-exact name match without a conflicting ORCID; any lookup failure silently degrades to the plain message.technote.tomlexists (TN004) and is valid TOML (TN005); a Sphinx technote has anindex.{rst,md,ipynb}content file (TN006);requirements.txtdeclaresdocumenteerwith the[technote]extra (TN002) and doesn't pin Sphinx separately (TN003).TN201, an abstract written as an ordinaryAbstractsection heading →TN202, an unparseable content file (e.g. corrupt notebook) →TN203, and an abstract directive that is present but has an empty body →TN204(the most common real-world failure: text left unindented under.. abstract::).TN202/TN204findings locate the problem asfile:line. The scanner matches directive names case-insensitively (docutils lowercases them), accepts body text on the.. abstract::marker line, and follows.. include::/```{include}directives one level deep.technote.toml(TN005/TN001) no longer short-circuits the run — only the author checks, which need the parsed model, are skipped. When aTN001failure comes from a historical author-name form (name = { name = "Full Name" }orgiven_names/family_names), the finding names the legacy form, shows the modern replacement, and points atdocumenteer technote migrate.conf.py(e.g. SQR-104's org-mode/reveal.js deck published through the shared technote CI) is validated for itstechnote.tomlalone — the requirements, content, andTN006checks are skipped, so a healthy non-Sphinx technote exits 0.docs/technotes/validation.rst(options, the full TN0xx/TN1xx/TN2xx check table, abstract-discovery rules, suggested-ID rules, and non-Sphinx behavior, linked from the technotes toctree), add it to theCLAUDE.mdCLI list, and wire[testenv:validate]/make validateinto the technote templates so migrated technotes and CI can invoke it.technote.toml(58sqr-*, 31rtn-*, 37dmtn-*) and every failure was investigated for tool-bug vs. legitimately-broken (triage report in the DM-52076 Jira comments, 2026-07-29). The improvements above came out of that round; after them the fleet reports 109 passes and 17 failures, each independently verified as a real technote defect.Validation steps
technote.tomlis schema-valid, whose authors all have resolvableinternal_ids, whose content has a non-empty abstract, and whoserequirements.txtdeclaresdocumenteer[technote], rundocumenteer technote validate -d <dir>and confirm it exits 0 with a success summary.documenteer technote validate -d demo/rst-technoteand confirm it exits 1 with one[TN101]error per author missing aninternal_id.internal_idat an id that returns 404 and confirm a[TN102]error (exit 1); simulate an unreachable authordb and confirm a[TN103]warning only (exit 0 with no other errors).internal_id, and confirm the[TN101]message appendsDid you mean '<id>' (matched by ORCID)?; use a wronginternal_idwith a recognizable name and confirm the[TN102]message appends a name-matched suggestion; make the suggestion lookup fail and confirm the plain message is unchanged.index.rst,index.md, andindex.ipynb, including with mixed-case directive names, body text on the.. abstract::marker line, and the abstract factored into a file pulled in via.. include::/```{include}.[TN201]error (exit 1); rewrite it as an ordinaryAbstractsection heading and confirm a[TN202]error whose message includes the heading'sfile:lineand points at the directive; leave the directive present but its body unindented (or a fenced directive empty) and confirm a[TN204]error locating the directive asfile:line.requirements.txt, drop the[technote]extra (or removedocumenteer) and confirm a[TN002]warning (exit 0); add a separatesphinxpin and confirm a[TN003]warning (exit 0); re-run either case with--strictand confirm it now exits 1.technote.tomlso it fails the schema and confirm the[TN001]error is reported alongside the requirements and content findings, with only the author checks skipped; use the legacyname = { name = "Full Name" }orgiven_names/family_namesauthor forms and confirm the[TN001]message names the legacy form and points atdocumenteer technote migrate.technote.tomlbut noindex.{rst,md,ipynb}and noconf.py(a non-Sphinx technote such as lsst-sqre/sqr-104), confirm validation exits 0; add aconf.pywithout an index file and confirm a[TN006]error.make validate(andtox run -e validate) and confirm it invokesdocumenteer technote validate.References