Skip to content

Record the doctest core architecture as an ADR - #92

Draft
tony wants to merge 24 commits into
masterfrom
adr-doctest-core-architecture
Draft

Record the doctest core architecture as an ADR#92
tony wants to merge 24 commits into
masterfrom
adr-doctest-core-architecture

Conversation

@tony

@tony tony commented Aug 2, 2026

Copy link
Copy Markdown
Member

Summary

  • Record a clean-slate architecture for a typed doctest core that preserves stock doctest.Example and doctest.DocTest objects while supporting docutils, MyST, pytest, Sphinx doctrees, and direct library use.
  • Make one pytest item own one shared-state group while retaining one real DocTest per source block for locations, gutters, and failure detail.
  • Separate settings, public contracts, inert models, and the immutable RegistrySnapshot; keep the mutable registry builder private.
  • Define the direct, pytest, Sphinx, and xdist contribution lifecycles in ADR 0007.
  • Add pinned-source analyses for CPython doctest, pytest, pytest-xdist, pytest-asyncio, asyncio, Sphinx doctest, docutils, MyST, and prior art.

This PR changes documentation only. The principal architecture record remains Status: Proposed; implementation is separate work.

Core architecture

The core has three compatibility lanes. Plain strings use the exact stdlib parser contract, Python objects use a DocTestFinder-shaped adapter, and reStructuredText or MyST use a typed DocumentParser followed by one doctree extractor. The markup parser does not subclass DocTestParser because their method signatures and return types are incompatible.

Parsed source keeps document_order for block/output pairing and block_ordinal for stable runnable identity. Projection owns grouping, wildcard cloning, phase order, pairing, name minting, :skipif:, and :pyversion:. Plans hold recipes, not mutable DocTest instances; each attempt materializes fresh stock objects against one item-owned mapping.

Prompt doctests retain CPython's per-example loop. Extended execution profiles own only the loop required for exec, top-level await, or later policies. Checker factories are contributable, and the checker that compares output also renders its difference.

Host and result contracts

Direct callers pass Contributor objects to build_registry(). pytest publishes its hookspec through pytest_addhooks and freezes initial-plugin contributions in pytest_configure(trylast=True), before xdist starts nodes or collection begins. Sphinx extensions contribute during setup(app) and freeze at config-inited, before documents are read.

The xdist manifest includes normalized session settings, ordered registry entries with provider versions, and doctest.OPTIONFLAGS_BY_NAME. It detects declared extension drift; it does not prove equal provider code or source closure. Version 1 therefore requires homogeneous worker environments.

GroupResult retains typed per-block results locally. The pytest item flattens doctest failures in block order into MultipleDoctestFailures, inherits pytest's representation for the default checker, and attaches only a versioned JSON-safe summary to reports. Only test-phase blocks determine pass versus skip; setup and cleanup are infrastructure. Process, debugger, and session aborts always propagate, while cleanup failures cannot erase a primary test outcome.

Evidence corrections

  • Sphinx runs one DocTest per ordinary or paired test, but combines setup blocks into one simulated DocTest and cleanup blocks into another.
  • Sphinx 9.0 changed the fallback for an unstamped bare doctest node; an unargumented directive still stamps the default group.
  • xdist collection depends on complete source closure, normalized settings, directive/plugin behavior, and the frozen registry, not only files, argv, and ini.
  • pytest-examples records Python string indices, not byte offsets, and one indent scalar does not invert dedent in general.
  • PR Share doctest namespaces across a page, and honour directive options #87 remains unshipped, so rejecting shared per-block items requires no feature removal or deprecation path.

Verification

  • uv run ruff format --check .
  • uv run ruff check .
  • uv run mypy src tests
  • uv run pytest — 105 passed
  • just build-docs — warnings-as-errors build succeeded
  • External source links use pinned tags or a trunk-reachable commit

tony added 10 commits August 2, 2026 10:10
why: Design decisions for the doctest engine have lived in commit
bodies and pull request threads, where a later reader cannot tell a
deliberate constraint from an accident. The project needs one place
that records what was decided, what forced it, and what it rules out.

what:
- Add docs/adrs/index.md stating what a record is for
- Fix numbering as sequential and permanent, and name the four
  statuses a record can carry
- Require pinned source links, since a blob/master anchor rots
  silently onto unrelated code while still resolving
- Wire the section into the docs toctree
why: The finder is one 320-line method spanning parsing, grouping and
naming; the runner reaches CPython's name-mangled loop through a
code-object clone; the plugin blocks pytest's doctest plugin and then
imports its privates; and a fork of xdist guards state xdist cannot
see. All four follow from one conflation: test identity and shared
state are separate axes, coupled here into a single setting.

what:
- Decide the item is the group and the DocTest is the block, so N
  DocTests report N locations under one unsplittable node id
- Record the three facts it rests on, each verified by running it:
  repr_failure reads locations per failure, _DocTestRunner__run is an
  ordinary attribute override, and an item-scoped mapping never
  crosses a process
- Fix the vocabulary where doctest, pytest and Sphinx collide on
  globs, namespace, group, scope, skip and name
- Tabulate the upstream constraints, each anchored at a pinned tag
- Record what was rejected and why, including prefix replay, IntFlag
  option surfaces and a whole-file docstring
- Stub the five deferred decisions as 0002 through 0006
why: ADR 0001 asserts constraints about six upstream projects. Those
assertions need a reviewable derivation, or the next person to
question one has to redo the reading. Keeping it beside the record
rather than inside it also keeps the record about the decision.

what:
- Add a per-system structural doc for CPython doctest, _pytest.doctest,
  pytest-xdist, pytest-asyncio, sphinx.ext.doctest, and docutils with
  myst-parser, each in the same section order so they compare
- Add asyncio as the stdlib's own worked example of a pluggable
  architecture, for contrast with doctest's four seams
- Add prior art on Sybil, xdoctest, pytest-examples and typeshed
- Add cross-cutting docs on data structures, data flows, extension
  seams, and the namespace-scope versus test-identity axis
- Add a bibliography collecting every anchor in one place
- Pin every citation to a tag, or to a trunk-reachable commit where a
  project publishes none
why: Review found the record described PR #87's unmerged design as the
status quo, and several claims did not survive checking against source.
Every corrected claim below was re-verified by reading a pinned tag or
by executing it.

what:
- Rewrite Context: trunk collects one DocTest per page; the groups,
  merge, skip lifting, exec runner and xdist scheduler are PR #87's
  proposal, named as such
- Drop "per-block SKIPPED" from what the shape buys free, and add an
  outcome contract: TestReport.outcome is one scalar per item, so a
  mixed group either erases the skip or over-reports the whole group.
  Record subtests as the only sanctioned alternative and why it is
  not adopted
- Credit sphinx.ext.doctest with the execution shape; the pytest
  identity is what is novel, not N DocTests per namespace
- Replace the bare DocTest tuple with PlannedBlock and GroupPlan, so
  run_group can order phases, evaluate the gate and guarantee cleanup
- Replace the compile-mode literal with a private ExecutionProfile,
  since PR #59's top-level await is a second policy a mode string
  cannot express
- Move docutils node vocabulary out of the stdlib-only leaf, and move
  settings below the layers that read it
- Add the item lifecycle contract, since half-reusing DoctestItem
  reintroduces the clear_globs wipe
- Correct Sphinx: :options: on testcode is an unknown-option error,
  not a silent discard; :pyversion: is the silent one; cleanup does
  not run after setup failure, so always-cleanup is a divergence
- Correct the parsefactories claim: conftest autouse fixtures arrive
  via FixtureManager.pytest_plugin_registered
- Correct the nominal-subclassing claim: stdlib accepts a duck-typed
  parser or finder; typeshed is what demands the class
- Narrow the xdist sentence: identical collection still binds
- Repin Sphinx anchors to v8.2.3, the version this project resolves
why: The record proposed deprecating doctest_docutils_namespace_items,
but neither that setting nor its scope twin has shipped — both live on
PR #87, in no release and on no tag. Deprecating an unshipped setting
fails the Published-Release Test, and there is no downstream to warn.

what:
- Retitle and reslug: the question is whether the shape should ship,
  not how to retire it
- State plainly that nothing shipped, so there is no migration path,
  no warning and no downstream grep
- Give the shape its due first: merging costs node ids, fixture
  lifetime and gutter locality, which is what per-block answers
- Then give the four reasons against, each with its guard: an id that
  NameErrors when selected, a mapping that cannot cross a worker, a
  mapping that cannot survive a rerun, and a crash tail that has no
  guard at all
- Note that the guards are the cost of the shape, not incidental
- Record the honest limit of the alternative: no per-block outcome and
  no per-block node id
why: The proposed mechanism does not work and was not needed. Nested
state machines build from nested_sm_kwargs, so substituting a parser
instance's state_classes never reaches the constructs with missing
lines; and RSTState.nested_sm_cache is a shared class attribute, so
the substitution is not scoped to one parse either. Meanwhile docutils
0.22 fixed the defect upstream.

what:
- Replace the substitution mechanism, the feature probe and the
  fallback design with a floor bump to docutils >=0.22
- Record why the mechanism failed, so it is not proposed again
- Name the real cost: docutils >=0.22 requires Sphinx >=9.1
- Version-qualify the line conventions, and state that an unqualified
  claim about docutils line numbers is a bug in the claim
- Keep the nullable line and the normalization layer, which serve
  .. include:: attribution regardless of version
why: Deselecting in pytest_collection_modifyitems runs too late.
DoctestTextfile.collect() reads and parses the page inside collect(),
so by then the built-in has already produced an item, or already
reported a collection error that deselection cannot retract.

what:
- Filter the built-in's collector out of the pytest_collect_file
  result in a hookimpl wrapper, before it parses anything
- Record that narrowing --doctest-glob cannot help either, because
  _is_doctest claims .rst initial paths before consulting the glob
- Move the compatibility failure from plugin registration to the
  collection of an affected document, matching 0001 and 0002 on not
  taking down a session whose tests never touch a doctest
- Replace the settled open question with the one the filter raises:
  scoping it to paths this plugin actually claims
why: TestResults is a two-field namedtuple carrying skipped off-tuple,
so == compares two of three values and a skip-count regression passes
the harness silently. attempted is also incremented before the SKIP
check, so a skip that wrongly executes moves neither counter.

what:
- Assert (failed, attempted, skipped) explicitly, and say why the
  tuple comparison is insufficient
- Add an exec-mode case to the matrix, and note it has no stock
  counterpart since a stock runner rejects a multi-statement body
why: Review checked these notes against source and found several
assertions wrong. Each correction below was re-verified by reading a
pinned tag or by executing it.

what:
- Repin to what the project resolves: docutils 0.21.2, Sphinx 8.2.3,
  and myst-parser split across two Python floors. Repin every Sphinx
  anchor, including the gated-drop line, which moved between tags
- Sphinx runs one DocTest per BLOCK against one shared group
  namespace. Move it into the N-DocTests row of the product space; its
  "one node id" is really no id, since every block shares DocTest.name
- Sphinx rejects :options: on a testcode with an unknown-option error
  rather than discarding it. The silent loss there is :pyversion:.
  Add the doctest-then-testoutput case, and the setup-failure
  short-circuit that skips cleanup
- parsefactories collects fixtures from the .py being collected;
  conftest autouse fixtures arrive via pytest_plugin_registered
- stdlib doctest accepts a duck-typed parser or finder. Typeshed is
  what names the class, so a nominal subclass buys type-checkability,
  not passability
- A regex can parse directive options; Sybil does. Justify the
  docutils dependency on host fidelity instead
- Version-qualify the line conventions, since docutils 0.22 changed
  them
- Correct the pytest-asyncio note: it does own an item class, it does
  convert str to enum, and the None sentinel is used on three of six
  options rather than all
- Record subtests as pytest's only sanctioned sub-item mechanism
why: The skipif-drop line moved between Sphinx tags, and the
vocabulary table still pointed at the v9.1.0 offset under a v8.2.3
path — an anchor that resolves onto the wrong lines.
@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.68%. Comparing base (baf73b4) to head (7f041dc).

Additional details and impacted files
@@           Coverage Diff           @@
##           master      #92   +/-   ##
=======================================
  Coverage   76.68%   76.68%           
=======================================
  Files          15       15           
  Lines        1025     1025           
=======================================
  Hits          786      786           
  Misses        239      239           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

tony added 14 commits August 2, 2026 14:56
why: A second review round found the corrected baseline was still
wrong, and three model claims did not survive execution. Trunk's _find
appends one DocTest per matched node named page.md[k] and the
collector yields one item per test, so released gp-libs already has
per-block identity with isolated copied globals — it lacks only a
sharing unit. Example.__eq__ gates on exact type identity, so a bare
subclass compares unequal to a stock Example in both directions.

what:
- Restate the baseline from trunk's source: per-block identity ships
  today, and it is what this design preserves rather than invents
- Restate the invariant as scheduling identity versus diagnostic
  identity, which is what the decoupling actually is
- Keep the Example subclass but override __eq__ with an isinstance
  check and rebind __hash__, pinned by a doctest asserting symmetry
  both ways; record the stock-Example fallback
- Correct Example.source: stdlib-normalized executable body, not the
  author's verbatim text, which is what Block.source holds
- Drop the purity claim. Includes read transitive files, directives
  execute during parsing, the registry is process-global and MyST
  plugins alter the tree; the contract is determinism over source
  closure, normalized settings and a frozen registry
- Split GroupPlan from GroupRun so planning stays immutable and the
  live mapping belongs to an attempt
- Add BlockResult and GroupResult as the channel for partial-skip
  detail, with its visibility and its accepted loss stated
- Record that a paired want depends on a run-time gate, that wildcard
  membership needs a distinct DocTest per group, and that the gate's
  namespace diverges from Sphinx's fresh context
- Complete the item lifecycle: seed restore, cleanup failure
  precedence, and the runner behaviours that must be reimplemented
why: A stock DocTestRunner does not raise on a multi-statement body —
the compile call sits inside the loop's own try, so it records an
unexpected exception and returns failed=1. Only DebugRunner
propagates. The harness also needs report_* events, since a skip that
wrongly executes moves neither counter.

what:
- State that stock records rather than raises, and assert the
  exec-mode case as a pair: stock fails, this runner passes
- Add report_* hook events and repeated runs to the matrix
- Exclude cross-block FAIL_FAST and cleanup aggregation, which belong
  to run_group's lifecycle tests rather than the per-example loop
why: Both mechanism assumptions were wrong. A docutils system_message
carries a level and text and nothing semantically stable, so keying
suppression on a stable code is impossible for exactly the messages
this record wants to suppress. And attaching an observer is additive —
the message still reaches the warning stream.

what:
- Say codes exist only for diagnostics this project emits, and that
  docutils-originated messages must be classified instead
- Give the full three-part recipe: halt_level above 4, report_level 5
  or warning_stream disabled, then the observer
- Open the classifier question, with the two candidate answers and the
  two docutils dialects a text table would have to handle
- Replace the promotion rule: registered-name is inverted for a typo
  and never fires for a swallowed foreign container. Promote on body
  content matching the example regex, with near-miss as an additive
  rule, and record that this is reST-only
- Mark the direction not yet implementable, rather than implying it is
why: 0005 said docutils >=0.22 requires Sphinx >=9.1. Sphinx 9.0
already permits it — but requires Python 3.11, and gp-sphinx's
sphinx<9 cap makes the requirement unsatisfiable regardless. 0006
proposed wrapper=True, which is gated on pluggy >=1.2 rather than
pytest 8, and pytest 7 permits a pluggy that raises at plugin import.

what:
- 0005: state the move as three parts, with the gp-sphinx cap named as
  the binding constraint and upstream of this repo; add the resolved
  matrix per interpreter; target >=0.22,<0.23 rather than an
  open-ended floor; correct "pins" to "resolves"; make dropping
  Python 3.10 the blocking open question
- 0006: use old-style hookwrapper with force_result, which needs no
  floor and was verified on pytest 7 and 9; name the minimum supported
  pytest; record that DoctestItem is public and the filtered collector
  is not
why: Several notes still described PR #87 as shipped, carried the
superseded GroupTest model, or repeated claims later verification
falsified. The true baseline is now known from trunk's source:
released gp-libs appends one DocTest per matched node named page.md[k]
and gives each its own copied globs.

what:
- Split the taxonomy rows: released gp-libs is one block, one item,
  isolated globals; PR #87 is a separate proposed row
- Move Sphinx into the N-DocTests row and drop "unoccupied", since
  Sphinx already executes that shape without addressable ids
- Rename the identity rule to never-source-coordinate-derived, and say
  an ordinal among extracted blocks satisfies it — which is what the
  released finder already uses
- Replace GroupTest with GroupPlan in the data-flow diagram, and
  attribute the clone and the synthetic page to PR #87
- Separate Sphinx's three units in 20: runner call per block, shared
  state per group, result as process-wide counters
- Drop BlockKind.node_types from the seam list and replace the
  rejected compile-policy callable with the private ExecutionProfile,
  matching ADR 0001
- Record that :pyversion: is declared on both testcode and testoutput
  and honoured on neither
- Correct the reporter section: an observer is additive and does not
  silence the stream, and a system_message carries no stable code
- Record why the state_classes substitution is not parse-scoped
- Fix the Sphinx heading and link that disagreed on version
why: A third review found the surrounding model incomplete or
self-contradictory even though the invariant holds. The Example
subclass was the worst of it: restoring equality with an isinstance
check makes two UNRELATED subclasses compare equal, and equal to any
third party's bare subclass. Verified by execution on 3.10 through
3.15.

what:
- Move execution policy off doctest.Example onto ProjectedBlock. The
  runner establishes an active execution request before delegating to
  stdlib run() and clears it in a finally, so stock Example objects
  stay stock and nothing in the kernel is subclassed for metadata
- Replace the type set with the lifecycle it actually has:
  ParsedBlock/ParsedOutput (inert) -> ProjectedBlock (stock DocTest,
  phase, profile, gate, gateable ExpectedOutput) -> GroupPlan
  (immutable) -> RunContext (live globs) -> BlockResult/GroupResult
- Drop want from the parsed layer: neither of its owners is the parsed
  block
- Give BlockKind a profile NAME so a public type stops holding a
  private one
- Add an error outcome and primary/secondary failures, with the
  precedence rule stated and pytest classification left to the adapter
- Design Settings as three facets with a stated precedence chain, and
  resolve the frozen registry alongside them rather than pointing at
  ADR 0006, which defines no freeze
- Record that entry-point discovery is not what breaks xdist
  determinism; nondeterministic contribution is
- Split DocumentParser from extract_blocks so a Sphinx extension can
  pass its own resolved doctree, and say why that is not a builder
- Move Python object discovery out of markup/: it takes an object, not
  text, and is DocTestFinder-shaped
- Replace the share axis with ungrouped = default | block, which is
  the question actually being answered
- Correct __lt__: it compares (name, filename, lineno, id), with name
  leading — the hazard holds, the old wording did not
- Correct the Sphinx precedent: setup blocks are combined into one
  simulated DocTest, likewise cleanup; only test blocks are per-block
why: Review found remaining inaccuracies, and the ADR 0001 model
changed under the notes. Each correction below was verified against a
pinned tag or by execution.

what:
- __lt__ compares (name, filename, lineno, id), with name leading. The
  hazard survives, and it is LIVE: find() sorts blocks named page.md[k],
  so an eleven-block page runs its eleventh block second
- parse() covers the NORMALIZED input, not the input exactly: tabs are
  expanded, common indent stripped, and a comment-only example is
  dropped outright
- pytest_xdist_make_scheduler is a broader affinity seam than
  _split_scope; the honest claim is scoped to the shipped schedulers
- myst_fence_as_directive runs a fence through the directive of the
  SAME name; python does not become testcode without an alias
- pytest-asyncio's _get_asyncio_mode is called from several sites, so
  the lesson is one conversion site, not one resolution
- pytest-examples stores Python string indices, and one indent scalar
  does not invert a dedent in general
- Reconcile with the corrected ADR model: ParsedBlock/ParsedOutput,
  compile mode on the projected block, ungrouped instead of share
- Replace note 22's reason for deferring entry points: discovery is
  identical across xdist workers, so nondeterministic contribution is
  the hazard, not discovery. Record that the freeze is real even
  though the registry object is not
why: A fourth review found the surrounding contracts did not compose.
GroupPlan was called immutable while holding a mutable DocTest whose
globs a run reassigns, so reruns would operate on last attempt's
state. DocumentParser was said to subclass doctest.DocTestParser,
whose parse(string, name) signature it cannot satisfy. And the record
promised a pluggable core while declaring nothing public.

what:
- Hold ingredients, not a DocTest: ProjectedBlock is a recipe and
  RunContext materializes fresh stock Example and DocTest objects per
  attempt, so a plan can never carry run state
- A gated testoutput makes its output ABSENT, not empty-with-options,
  which is what Sphinx does
- Replace the nullable result record with a discriminated union, so a
  passed result carrying an exception is unrepresentable
- Give exception precedence explicitly: control-flow, then body, then
  cleanup, with the loser recorded
- Split ExecutionProfile (immutable factory) from ExecutionRuntime
  (per attempt, one per profile a group uses), so a group may mix
  prompt, exec and async blocks and an async runtime owns one loop
- Run ordinary prompt blocks on CPython's untouched loop and reserve
  the owned __run for extended profiles, making the common lane
  compatible by construction
- Give the parser three lanes, since DocumentParser cannot be a
  DocTestParser: strings, markup, and DocTestFinder-shaped objects
- Define self.dtest as a synthetic zero-example group DocTest whose
  globs is the live mapping, and keep the darwin capture guard
- Add the report-attribute channel, since the controller sees
  serialized reports rather than items
- Give settings lifetimes (session, document, block) rather than one
  precedence sentence; move encoding to the loader and report style to
  the host; make wildcard and naming invariants, not knobs
- Publish a contributor protocol while keeping the registry private,
  and record the conftest-timing and heterogeneous-worker hazards
why: ADR 0001 now runs ordinary prompt blocks on CPython's untouched
loop, which shrinks what 0002 must prove and makes 0005 orthogonal to
the architecture. Review also found the notes still generalized
Sphinx's per-block execution and conflated xdist's two channels.

what:
- 0002: scope the harness to the extended lane; ordinary blocks are
  the reference rather than something to differentially prove
- 0005: state plainly that the floor is support policy, not core
  architecture, and that no part of 0001 depends on the answer
- 0005: correct the Sphinx version — the group fallback for a bare,
  unstamped doctest_block changed in 9.0, not 9.1, and directives
  always stamp groups so unargumented directives are unaffected
- Notes: Sphinx is per-block for its TEST phase only; setup blocks are
  combined into one simulated DocTest and cleanup into another
- Notes: separate xdist's scheduling channel from its reporting
  channel — the controller sees node-id strings when scheduling and
  serialized reports afterwards, and reports carry arbitrary extra
  attributes
- Notes: a nullable line is not unique to this design; Sphinx's
  get_line_number returns None too. What is new is per-block
  propagation
- Notes: narrow the affinity claim to the shipped schedulers, since
  pytest_xdist_make_scheduler substitutes a whole Scheduling
- Notes: PR #87's per-block mode is proposed, not shipped
- Notes: carry the block/runtime split into the data-flow diagram
why: Making the plan a recipe last round dropped information the
recipe must carry. One prompt block yields SEVERAL doctest.Example
objects — three for a block ending in a traceback — each with its own
source, want, exc_msg, lineno, indent and options, and pytest's
failure renderer needs the docstring besides. A single source string
and one lineno cannot rebuild that. The result types lost outcomes for
the same reason: continue_on_failure produces several failures from
one block, and a PASSING block can report attempted=2 skipped=1.

what:
- Add ExampleRecipe and carry examples plus docstring on
  ProjectedBlock, so materialization reproduces get_doctest exactly
  rather than approximating it. Pinned by a doctest asserting the
  three-example case
- Make Failed.failures plural, give Passed and Failed counts, and type
  SkipReason, since a skip may come from :skipif:, an inline flag,
  :pyversion: or a profile declining
- Replace the single precedence ladder with a phase-aware table:
  process aborts always win; a cleanup failure — including a
  pytest.skip raised in cleanup — never outranks a body failure
- Enter profile runtimes through an ExitStack so partial startup
  unwinds in reverse
- Rename position to source_ordinal and define it as the ordinal among
  runnable candidates before gating, filtering and group expansion, so
  inserting prose renames nothing
- Call GroupPlan structurally immutable and say the seed is
  shallow-copied per attempt, matching doctest's namespace semantics
- Retire the last PlannedBlock and plan-held-DocTest language, and
  restate wildcard membership as per-group materialization
why: The remaining review items were the contracts named but never
written: a registry that ADR 0001 depended on and ADR 0006 did not
define, a compatibility promise stated as one phrase covering
surfaces of very different strength, and an unknown-directive default
that could swallow a body holding doctests.

what:
- 0006: define the freeze lifecycle — Contributor and Registrar
  signatures, build_registry, duplicate names as an error unless
  replace=True, deterministic contribution order, and a freeze point
  per host
- 0006: nested conftests may not contribute, since they load after any
  freeze that precedes collection; their fixtures and hooks are
  unaffected
- 0006: workers compare a registry manifest rather than node ids, since
  two workers can collect identical ids while resolving one profile
  name to different code — which is what makes SSH and socket workers
  safe rather than assumed homogeneous
- 0001: decompose "vanilla-compatible" into a matrix, and narrow the
  Sphinx promise to consuming a resolved doctree, since no execution
  lifecycle or result channel is specified
- 0001: name the registry as a fifth input collection is not pure over
- 0001: correct the layer diagram, which still said the runner always
  owns the example loop
- 0004: split unknown roles from unknown body-owning directives. A role
  cannot swallow a code block; a container can, so it is a collection
  error by default and a project registers legitimate containers
  explicitly
- Notes: the execution profile is contributable, not private, matching
  0001's public contributor protocol; entry-point discovery is optional
  behind it rather than out of scope
- Notes: narrow the node-id claim to per-block ids over shared MUTABLE
  state, since ids over isolated state do keep the promise
why: The proposed core mixed settings with discovered capabilities and
left source ordering, host outcomes, checker rendering, and result
projection underspecified.

what:
- Separate contracts, settings, models, and registry snapshots
- Define stable source identity and complete gate/result records
- Specify pytest failure projection and phase-aware exceptions
why: Registry construction is a host-neutral extension contract, not a
pytest private-API detail, and each supported host freezes contributors
at a different point.

what:
- Add typed contributor, registrar, and snapshot contracts
- Define direct, pytest, Sphinx, and xdist lifecycles
- Keep mutable construction private and late registration explicit
why: Several supporting claims overstated Sphinx block granularity,
xdist determinism, stdlib subclass compatibility, and source offset
semantics.

what:
- Correct Sphinx 9.0 grouping and phase execution claims
- Separate markup protocols from stdlib-shaped facades
- Qualify xdist manifests and source rewrite metadata
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant