Test-Quality Doctrine — Artefact Series Proposal
Author: doctrine-daphne (curator scout) + operator refinements Date: 2026-07-26 Companion to: Research findings Feeds: #2935
This is a design proposal for a curator, not the artefacts themselves. It supersedes §8 of the research findings (which recommended a single new artifact from a false-uniqueness premise — see the reconciliation note below). No files are created by this document.
Binding constraints (added 2026-07-26, after the proposal was first written)
Two ADRs landed on this branch because of questions this proposal raised. They are binding on the curator and they change or retire parts of what follows:
- ADR 2026-07-26-1
— DRG edges are the canonical artefact-relationship authority. Inline
references:blocks in artefact YAML are pre-DRG residue: frozen, closed to growth, slated for migration. Consequences here:- Every relationship in this series is an edge. No new artefact carries an inline
references:block, and no existing block gains entries. - The
<kind>_reference.typeschema enums must not be widened.assetbeing unnameable inline is correct behaviour. This retires the "asset support is not end-to-end" reading of the #2918 finding (see the revised capability section below). DIRECTIVE_041is itself one of the legacy carriers (5 structured entries). Those five migrate to edges as part of FR-013 — and because editing041is its own reviewed change, do that migration with the intent split rather than as an anonymous line in the bulk pass. The new link to the paradigm is an edge from the start.
- Every relationship in this series is an edge. No new artefact carries an inline
- ADR 2026-07-26-2
— artefacts live at
<type>/<pack>/[<category>/]<name>. The pack layer (built-in) is mandatory and any category grouping nests inside it. Consequences here:- Asset blobs go to
assets/built-in/<file>orassets/built-in/<category>/<file>— neverassets/<category>/built-in/, which is the #2918 mistake. - A layout gate now enforces this, so a misplaced artefact fails a test instead of silently never loading.
- Asset blobs go to
Prior art found and removed (record, not a fold)
A dormant paradigm:test-first ("Test-First Doctrine") and directive:TEST_FIRST
("Test-First Development") were discovered outside the pack layer — never loaded, never
activated, invisible to node discovery. The operator's call was to reconcile them into
this series rather than drop them blind. On inspection there was nothing to fold:
between them they carried an id, a title, and a one-line summary, with no procedures, steps,
integrity rules, or validation criteria. They were deleted as part of the layout cleanup
(f7ee9fb02). Recorded here so the reconciliation is honest rather than dressed up as a
merge — but the names are a useful warning: do not let this series produce a third
tests-doctrine authority. DIRECTIVE_041 + the new paradigm are the authority.
Guiding correction — augment-heavy, not create-heavy
An overlap audit against the pack's existing testing doctrine found that most of the research doc's load is already carried by wired, activatable authority:
styleguide:test-desiderata-and-boundaries— the 12 desiderata + boundary-by-responsibility.tactic:test-boundaries-by-responsibility— the operative "mock only at the border" rule.tactic:function-over-form-testing— the interaction→outcome assertion shift.tactic:connascence-analysis— the connascence lens the research calls its backbone.directive:DIRECTIVE_041"Tests as Scaffold, Not Friction" (enforcement: required) — already binds "assert the observable contract, not the internal call graph."
So the series is augment-heavy, create-light. This also resolves the external review's finding #1 (PR #2936): the research doc's "currently unwritten" framing (§6) and its "create a new binding artifact" recommendation (§8) overclaimed uniqueness; the correct move is to reconcile and extend the existing wired authority, creating only the genuine deltas.
The genuine deltas: (1) the positive allow-list of legitimate mock seams as a citable rule; (2) the specific failure mode — mocking internal SUT logic to pin a call-contract, which manufactures a production-unreachable state; (3) a remediation procedure ("unfake a test"); (4) the mindset currently mis-homed inside a directive.
The operator's topology refinements (applied)
- "Tests as Scaffold, Not Friction" is a paradigm (mindset), not a directive. Create a
new paradigm; disambiguate
DIRECTIVE_041so the binding rule stays a directive and the mindset/why moves to the paradigm, relinked viarefines. - Test Desiderata is a complementary checklist that is part of the paradigm — relinked, not re-kinded (it stays a styleguide; its enumerated list becomes an asset).
- Checklists → assets. The
assetkind is a loose-contract blob (markdown/python via a*.asset.yamlsidecar), resolved-only, referenced viarequiresedges (working precedent:directive:DIRECTIVE_042 --requires--> asset:common-docs-structural-lint). Move duplicated checklists (the desiderata list; the Quad-A definition) into shared assets. - Exemplar assets for exhaustive examples. Short good/bad snippets stay inline; the full
UserRegistration slicing case and the #2934 before/after become exemplar assets, referenced
from the good artefacts (never emitted from anti-pattern nodes — those are
rejectstargets only).
Proposed series
Create
paradigm:tests-as-scaffold-not-friction[activatable] — the mindset: tests protect change; the false-confidence (mocked-out behaviour) / false-friction (implementation-coupled assertions) duality; Beck's desiderata as the value system; connascence framing; the policy-choice honesty clause. Carries the migratedDIRECTIVE_041.intent.directive:DIRECTIVE_047— Mock Only at the Responsibility Border [activatable,required] — the positive allow-list (other-domain / filesystem / DB / rare system-logic) + the manufactured-state failure mode.requiresDIRECTIVE_041; does not restate it.procedure:unfake-an-over-mocked-test[activatable] — the six-step remediation.anti_pattern:mock-internal-sut-logic[resolved-only],anti_pattern:assert-on-mock-interactions[resolved-only] — nodes inanti_pattern.graph.yaml,rejectstargets only.- Assets [resolved-only]:
asset:test-desiderata-checklist,asset:quad-a-test-structure,asset:exemplar-slicing-user-registration,asset:exemplar-2934-unfake-merge-test(markdown blobs underassets/built-in/+*.asset.yamlsidecars).
Augment (edit, no new node)
DIRECTIVE_041— moveintent(mindset) into the paradigm; shrink to a pointer; add--refines--> paradigm.styleguide:test-desiderata-and-boundaries— add the "Isolated ≠ isolate-via-mocks" and Gold-Plating traps + the connascence-of-a-mock line; extract the desiderata list to the asset.styleguide:testing-principles/python-conventions/java-conventions— extract the shared Quad-A definition to the asset; keep language-specific code examples inline.tactic:test-boundaries-by-responsibility— add the rare system-logic exception + afailure_modesentry;rejectsthe internal-mock anti-pattern;suggeststhe slicing exemplar.tactic:function-over-form-testing— add the #2934 one-liner;rejectsthe interaction-assert smell.
DIRECTIVE_041 disambiguation (stays vs moves)
- Stays in the directive (binding surface):
enforcement,scope, allprocedures,integrity_rules,validation_criteria,references. - Moves to the paradigm (mindset): the long
intentblock ("Tests must protect change, not obstruct it… tests that pass for the wrong reason are friction…"). Replace 041'sintentwith a one-line pointer to the paradigm. - Relink relation:
refines(directive sharpens the mindset into an enforceable rule). Notspecializes_from(that is agent_profile→agent_profile lineage only). Notrequires(a hard cascade-prerequisite; the rule can operate as a checklist independently) — unless the curator wants the paradigm auto-pulled whenever an action scopes 041, in which caserequiresis the documented fallback trade-off. This is the firstrefinesedge in the built-in graph — confirm it round-trips through the org→DRG bridge on regeneration.
DRG edge map (source --relation--> target)
Paradigm hub:
paradigm:tests-as-scaffold-not-friction --requires--> styleguide:test-desiderata-and-boundariesparadigm:… --suggests--> styleguide:testing-principles,tactic:connascence-analysis,asset:quad-a-test-structure,asset:exemplar-slicing-user-registrationparadigm:… --rejects--> anti_pattern:mock-internal-sut-logic,anti_pattern:assert-on-mock-interactions
Directives:
directive:DIRECTIVE_041 --refines--> paradigm:tests-as-scaffold-not-frictiondirective:DIRECTIVE_047 --refines--> paradigm:tests-as-scaffold-not-frictiondirective:DIRECTIVE_047 --requires--> directive:DIRECTIVE_041directive:DIRECTIVE_047 --suggests--> tactic:test-boundaries-by-responsibility,tactic:function-over-form-testing,styleguide:test-desiderata-and-boundaries,asset:exemplar-2934-unfake-merge-testdirective:DIRECTIVE_047 --rejects--> anti_pattern:mock-internal-sut-logic,anti_pattern:assert-on-mock-interactions
Procedure:
procedure:unfake-an-over-mocked-test --requires--> tactic:test-boundaries-by-responsibilityprocedure:… --suggests--> directive:DIRECTIVE_047,tactic:function-over-form-testing,asset:exemplar-2934-unfake-merge-testprocedure:… --rejects--> anti_pattern:mock-internal-sut-logic
Tactics / checklist-asset edges:
tactic:test-boundaries-by-responsibility --rejects--> anti_pattern:mock-internal-sut-logic;--suggests--> asset:exemplar-slicing-user-registrationtactic:function-over-form-testing --rejects--> anti_pattern:assert-on-mock-interactionsstyleguide:test-desiderata-and-boundaries --requires--> asset:test-desiderata-checkliststyleguide:testing-principles --requires--> asset:test-desiderata-checklist,asset:quad-a-test-structurestyleguide:python-conventions --requires--> asset:quad-a-test-structurestyleguide:java-conventions --requires--> asset:quad-a-test-structure
Every resolved-only node (asset / anti_pattern) has ≥1 inbound edge from an activatable node — no orphans.
Capability-awareness (intent dominates) — REVISED 2026-07-26
The sibling doctrine PR #2918 was read as showing that asset support is not end-to-end, on two counts. Investigation resolved both, and neither is an asset-support defect:
- "
type: assetinside a tactic is schema-rejected." — Not a defect. The<kind>_reference.typeenum belongs to the inlinereferences:surface, which ADR 2026-07-26-1 pins as frozen pre-DRG residue. A kind being unnameable there is the deprecated surface correctly refusing to grow. Widening it was drafted and abandoned: it would have entrenched a second relationship authority. (Worth knowing: all four schema copies of that enum had already drifted apart — they omitassetandglossary_pack, and thetacticcopy additionally omitsagent_profileandmission_step_contract. The fix is to close the surface, not to reconcile four copies of it.) - "The extractor misses
assets/<subdir>/built-in/**." — Not an extractor defect. The directory was misplaced: #2918 inverted pack and category. Per ADR 2026-07-26-2 the correct path isassets/built-in/audiences/, which the existing scan already finds viarglob. A widening of the extractor scan was drafted and abandoned in favour of fixing the layout and gating it.
This matters as a method note, not just bookkeeping: intent dominates over capability means
fix the tooling to serve the intended model — it does not mean assume every blocked path
is a tooling gap. Two of the three "prerequisite fixes" this proposal originally called for
dissolved once the intended model was actually pinned. The proposal's design was already
right by construction (edges + assets/built-in/ paths); what was wrong was the diagnosis of
why the alternatives failed.
Residual verification the curator must still run:
- Confirm the extractor picks up the four new
assets/built-in/*.mdblobs (the layout gate now catches misplacement, but node discovery is a separate assertion). - Re-evaluate the claimed CLI-vs-canonical-validator parity gap —
spec-kitty doctrine validatevs the strict canonical tests. Part of the originally-reported gap is likely the frozen inline surface behaving correctly; characterize what genuinely diverges before "fixing" it, and add a parity test for whatever remains. - If a genuine asset-support gap does surface, closing it is in-scope prerequisite work, not a reason to downgrade the artefacts.
Load-time / validation risks
- Asset path containment — relative, under
assets/, no../symlink escape (all usebuilt-in/<file>). - Asset mime/extension consistency —
text/markdown ↔ .md; never declare markdown on a.py. - Resolved-only discipline — assets/anti-patterns are never charter-activated; enter context only via inbound edges.
- URN/kind prefix must equal kind; confirm
DIRECTIVE_047is free (max present is 046). enhances/overridesare not usable on assets/anti-patterns — dedup is viarequires→asset edges.- Anti-pattern nodes emit no edges — route exemplar references through the good artefacts.
refinesfirst use — CONFIRMED, and it is a real risk. Measured relation histogram across all 14 shipped fragments:suggests332,requires259,scope157,rejects8,instantiates8,specializes_from4,reconciles_tension3,in_tension_with2,applies1,refines0. So the041 --refines--> paradigmedge is genuinely the first in any built-in fragment.refineshas been first-class since #2079 — which fixed a silentrefines→appliesdowngrade in the org→DRG bridge — but nothing built-in has ever exercised that fix. Verify the round-trip explicitly on regeneration; do not assume.- The 041 split must not author an accidental
in_tension_with— the relation isrefines(aligned, not competing). - Add each anti-pattern/asset node in the same change as its referencing edges; regenerate the compiled graph; validate to zero errors.
- Golden-count baselines move.
tests/doctrine/drg/migration/test_extractor_projection.pypins node/edge/orphan cardinality (currently 305/757/32) with a composition ledger in the constant's docstring. Every node and edge this series adds must extend that ledger with its own entry — the counts are a contract, not an incidental. - Regeneration is via the CLI, not by hand:
spec-kitty doctrine regenerate-graph. The fragments aregenerated_by: drg-migration-v1; hand-editing them desynchronizes the freshness test (test_shipped_graph_yaml_is_fresh).
Build order for the curator
- Prerequisite (not optional): freeze the four
<kind>_reference.typeenums with a pointer-comment to ADR 2026-07-26-1, and fix the ~10 source sites whose operator-facing migration hint namessrc/doctrine/graph.yaml— a file #2680 sharded out of existence. Authoring new doctrine while the system's own migration instruction is unfollowable just propagates the confusion this series is meant to reduce. - Asset blobs + sidecars at
assets/built-in/[<category>/]<file>; asset nodes inasset.graph.yaml. - Paradigm; migrate
041.intent→ paradigm; shrink 041's intent to a pointer. Editing the liveenforcement: requiredDIRECTIVE_041is its own reviewed change — audit inbound edges first. Do not touch its existing inlinereferences:block (ADR 2026-07-26-1). DIRECTIVE_047+procedure:unfake-an-over-mocked-test; two anti-pattern nodes.- Excise the desiderata list + language-neutral Quad-A definition from the styleguides (keep language-specific examples inline).
- Author all edges; regenerate the fragments with
spec-kitty doctrine regenerate-graph; extend the golden-count ledger; run the reference/cross-edge audit + pack validation to zero errors; confirm the Clear Test Boundaries URL. - Update the
doctrine-daphneprofile with the canonical doctrine/charter structure (layout convention + edges-not-inline-refs), so the next curator invocation starts from the pinned model rather than rediscovering it. A curator profile that does not carry these rules is how the #2918 layout mistake happened in the first place.