Skip to content

DM-52076: Add technote validate command - #316

Open
jonathansick wants to merge 12 commits into
mainfrom
tickets/DM-52076
Open

DM-52076: Add technote validate command#316
jonathansick wants to merge 12 commits into
mainfrom
tickets/DM-52076

Conversation

@jonathansick

@jonathansick jonathansick commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

  • Add a documenteer technote validate command (Click) that checks a technote's metadata, structure, and content so CI can fail builds with incomplete data, delegating to a new TechnoteValidationService with an extensible, code-keyed check framework; --strict/-s promotes warnings to errors and every finding is prefixed with its stable code.
  • Metadata checks: schema conformance (TN001) and author internal_id resolution — missing id (TN101), id not in the author database (TN102), unreachable database (TN103, a warning) — with AuthorDb.get_author distinguishing a 404 (AuthorNotFoundError, errors) from a transport failure (warns only). A missing or unknown id is not a dead end: the finding appends a Did 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.
  • Structural checks: technote.toml exists (TN004) and is valid TOML (TN005); a Sphinx technote has an index.{rst,md,ipynb} content file (TN006); requirements.txt declares documenteer with the [technote] extra (TN002) and doesn't pin Sphinx separately (TN003).
  • Content checks: statically scan the content file (no Sphinx build) for a non-empty abstract directive — no abstract at all → TN201, an abstract written as an ordinary Abstract section 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/TN204 findings locate the problem as file: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.
  • Robust reporting: an unparseable or schema-invalid technote.toml (TN005/TN001) no longer short-circuits the run — only the author checks, which need the parsed model, are skipped. When a TN001 failure comes from a historical author-name form (name = { name = "Full Name" } or given_names/family_names), the finding names the legacy form, shows the modern replacement, and points at documenteer technote migrate.
  • Non-Sphinx technotes: a technote-series repo with no index content file and no conf.py (e.g. SQR-104's org-mode/reveal.js deck published through the shared technote CI) is validated for its technote.toml alone — the requirements, content, and TN006 checks are skipped, so a healthy non-Sphinx technote exits 0.
  • Document the command in 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 the CLAUDE.md CLI list, and wire [testenv:validate] / make validate into the technote templates so migrated technotes and CI can invoke it.
  • Fleet-tested upstream: the command was run against all 126 technote repos with a technote.toml (58 sqr-*, 31 rtn-*, 37 dmtn-*) 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

  • In a technote whose technote.toml is schema-valid, whose authors all have resolvable internal_ids, whose content has a non-empty abstract, and whose requirements.txt declares documenteer[technote], run documenteer technote validate -d <dir> and confirm it exits 0 with a success summary.
  • Run documenteer technote validate -d demo/rst-technote and confirm it exits 1 with one [TN101] error per author missing an internal_id.
  • Point an author's internal_id at 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).
  • Give an author an ORCID that the author database knows but omit the internal_id, and confirm the [TN101] message appends Did you mean '<id>' (matched by ORCID)?; use a wrong internal_id with 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.
  • Confirm a non-empty abstract directive passes when written in index.rst, index.md, and index.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}.
  • Remove the abstract and confirm a [TN201] error (exit 1); rewrite it as an ordinary Abstract section heading and confirm a [TN202] error whose message includes the heading's file:line and 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 as file:line.
  • In requirements.txt, drop the [technote] extra (or remove documenteer) and confirm a [TN002] warning (exit 0); add a separate sphinx pin and confirm a [TN003] warning (exit 0); re-run either case with --strict and confirm it now exits 1.
  • Corrupt technote.toml so 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 legacy name = { name = "Full Name" } or given_names/family_names author forms and confirm the [TN001] message names the legacy form and points at documenteer technote migrate.
  • In a directory with a valid technote.toml but no index.{rst,md,ipynb} and no conf.py (a non-Sphinx technote such as lsst-sqre/sqr-104), confirm validation exits 0; add a conf.py without an index file and confirm a [TN006] error.
  • Confirm the "Validate a technote" page renders and is linked from the technotes index, and that its options and TN0xx/TN1xx/TN2xx check table match the command.
  • In a migrated technote, run make validate (and tox run -e validate) and confirm it invokes documenteer technote validate.

References

@jonathansick jonathansick left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_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] — 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 --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 — the same 'traceback instead of a linter finding' problem as f1. (src/documenteer/services/technotevalidation.py)
  • [info] 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). (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 jonathansick left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 jonathansick left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 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). (src/documenteer/cli.py)
  • [warning] 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. (src/documenteer/services/technotevalidation.py)
  • [info] 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. (src/documenteer/services/technotevalidation.py)
  • [info] _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. (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 jonathansick left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant