spec-driven-development: Spec Patterns That Pay Off (field-verified, Hatchmate SA- review)

Claude-Session: https://claude.ai/code/session_01M2NgpY9AaKq6KsVmPXvG6L
This commit is contained in:
Paul O'Reilly
2026-08-18 21:20:53 +12:00
parent 24a105897c
commit a69d0da22c

View File

@@ -206,6 +206,26 @@ Don't load everything — agents perform better with focused context than with a
The spec→test→code workflow defines *when* to write tests. For *how* to write comprehensive tests — edge case discovery, property-based testing, mutation testing, AI agent testing patterns — see [Test-Driven Development](test-driven-development.md).
## Spec Patterns That Pay Off (Field-Verified)
Patterns observed in specs that survived multi-wave agent implementation with near-zero rework (extracted from the Hatchmate `SA-` social-attribution spec review, 2026-08). Each turns an implicit convention into an explicit, testable artifact.
1. **Per-operation authorization table when auth is enforced elsewhere.** When a module delegates permission checks to its caller (API layer), "the caller checks permissions" is not enough — include a table mapping *every* public function to the exact permission tuple the caller must enforce (resource_type, resource_id, noun, verb), including a row for deliberately unauthenticated operations. Without the table, each route author re-derives the mapping and they diverge.
2. **Cross-spec error ownership: assert types, not messages.** When function A raises an error class owned by another spec, the requirement should instruct tests to import the class and assert `isinstance`, never the message string. One spec owns each message; everyone else asserts the type. Prevents cross-spec test breakage when the owning spec rewords a message.
3. **Specify non-idempotency explicitly.** When repeated calls are *meant* to create new rows (e.g. one share row per promotion event), write a requirement saying so, with the rationale. Otherwise a reviewer or agent will "fix" it into idempotency and silently collapse distinct events.
4. **Security filters live in the owning module, not the caller.** For a filter that is the sole gate on public exposure (e.g. `approved=true` on a public widget query), require it inside the query function itself and say why: *"removing it would require changing this module, not just the API layer."* Defense in depth expressed as a code-locality requirement.
5. **Pin cross-module seams to exact signatures.** In Dependencies and in the requirements that cross a spec boundary, name the file path, function, and the argument list verbatim (`register_resource(resource_type=..., resource_id=..., actor=...)`). This is what makes the caller-side/callee-side consistency review mechanical instead of interpretive.
6. **Projection requirements for public endpoints.** When an internal row is returned by a function but only some columns may appear in an unauthenticated HTTP response, state the allowed projection *and* the columns that must never appear (e.g. `customer_id must not appear in the public response`). The negative list is the test.
7. **Every column must be reachable from a requirement.** A data-model column no requirement reads or writes (a dangling `is_active`) invites agents to invent behaviour. Either write the requirement, or label the column/table explicitly as a future-phase stub ("no Phase 1 logic touches this") — the stub label is itself testable.
8. **Validate stored URLs at every ingress, not just the obvious one.** If a URL column is served on a public surface, *every* function that writes it applies the same scheme/host validation — including late setters like "mark delivered" flows, not only the create path. Audit: for each URL/HTML-adjacent column, list the functions that can write it and check each validates.
## Post-Write Spec Audit
After writing specs, audit them against best practices before implementation. Common gap categories: