Feature Specifications¶
No implementation change without a spec it implements. The spec is the authoritative decision record; the code is downstream of it.
That rule earns its keep here more than in most projects. config is consumed by other
modules and by downstream tools, so a public API change is expensive to undo. And the
module's behaviour is deliberately counter-intuitive in several places — refusing writes
from inside observers, dropping superseded snapshots, handing back a usable store alongside
an error. Each of those is a decision someone made for a reason, and without the reason
written down the next person reads it as a bug and "fixes" it.
Where specs live¶
The ISO date prefix orders them by creation; the slug says what they are. Examples:
2026-07-18-structure-preserving-config-writes.md2026-07-19-store-architecture.md
When a spec is required¶
The bar is non-trivial or decision-bearing. If a reasonable person would ask "why was it done this way?", it needed a spec.
| Needs a spec | Does not |
|---|---|
| A new public type, function or interface | A typo or comment fix |
| A behaviour change a consumer could notice | A version-pin bump |
| A new backend, or a change to the Backend contract | An internal rename with no API effect |
| Anything touching write routing, notification ordering, or merge semantics | A test added for existing behaviour |
| Removing or renaming an exported name | Reformatting |
Forcing a spec onto a mechanical, reversible change is ceremony. Skipping one on a decision-bearing change is how a module loses its own reasoning.
Frontmatter¶
---
title: <what the spec decides, as a phrase not a sentence>
date: YYYY-MM-DD
author: <your name>
status: draft
issue: phpboyscout/go/config#<n> # optional
supersedes: <YYYY-MM-DD-slug>.md # optional
---
Status lifecycle¶
status is the single source of truth for where a decision stands.
| Status | Meaning |
|---|---|
draft |
Under discussion. Do not implement against it yet. |
approved |
Agreed. Safe to implement. |
implemented |
Shipped. Flip it when the change merges or tags. |
rejected |
Considered and decided against, or superseded by a later spec. |
Keep rejected specs. The value is the durable "we thought about X and chose not to,
for these reasons" record, so the question does not get re-litigated from scratch a year
later by someone who was not in the conversation. Add a ## Rejection rationale section
explaining why, and leave the rest of the document intact.
Structure¶
Adapt to the decision, but most specs want:
- Problem — the current pain, specifically. Not "improve X" but what breaks and for whom.
- Decisions, as a numbered list — D1, D2, … — so reviews, merge requests and later specs can cite "D7" precisely. This is the core of the document.
- Rejected alternatives — what else was considered, and why it lost. Frequently the most valuable section six months later.
- Public API — every exported name the change adds, removes or alters.
- Testing strategy — how each decision will be verified. Evaluate godog fit for behaviour that spans components and time.
- Migration & compatibility — what breaks, and what a consumer has to do about it.
- Open questions — what is still live. Resolve these with the human before the
status moves to
approved. - Implementation phases — a prioritised breakdown, each independently verifiable.
Numbering decisions¶
Number them and never renumber. A spec whose D7 means something different this month than last is worse than no spec, because everything citing it is now silently wrong. If a decision is withdrawn, mark it withdrawn in place and add a new one.
Recording revisions¶
A spec amended after the fact — a decision that proved wrong in practice, or was under-specified — gets a dated revision note, not a silent edit. The record should show the change of mind.
The convention here is a numbered revision section, R1, R2, …, each stating what was decided, what actually happened, and why the original was or was not right. Cross-link the decision it revises, so a reader arriving at D8 sees that R10 amended it.
That cross-link is not decoration. In this module a decision was recorded, half implemented, and the unimplemented half then documented as intended behaviour — because the person writing the docs read the code rather than the spec. The link is what makes that visible.
Working with an AI assistant¶
Specs here are usually drafted collaboratively with an AI assistant, which is good at enumerating edge cases and bad at knowing which trade-off you want.
Drafting¶
Give it the problem, the constraints, and the existing specs as format examples. Then:
- Answer its open questions yourself. If it picks the convenient option to keep moving, the decision has been made by nobody.
- Challenge the edge cases — concurrency, failure partway through, what a consumer sees when it goes wrong.
- Make it name what it rejected. A spec with no rejected alternatives has not thought about the problem.
Implementing¶
Once a spec is approved, implementation is test-first, phase by phase:
- Write the failing tests for the phase, derived from the spec's decisions and acceptance criteria.
- Implement the minimum that passes them.
- Refactor with the tests green.
just cibefore moving to the next phase.
Writing the tests first is also how you find out whether the assistant understood the spec. A misunderstanding shows up in an assertion, where it is cheap, rather than in the implementation, where it is not.
Verify the work, do not accept it
The specific failure mode to watch for is a test that passes for the wrong reason. This module has had at least five: a green test upheld by an unrelated bug, a concurrency test whose harness was broken rather than the code, a fidelity test whose assertion encoded the same confusion as the implementation.
A test that has never been watched to fail is not evidence of anything. See Testing.
Suggested drafting prompt¶
Draft a feature specification for the `config` module.
## Problem
<What breaks today, for whom, and why the current design cannot accommodate it.>
## Constraints
- The Store is the sole owner of configuration I/O — nothing else reads, writes or watches
a source. If this change needs a second owner, say so explicitly rather than working
around it.
- The module is pre-1.0 but consumed by other phpboyscout modules; public API changes need
a stated migration cost.
- Errors a caller might branch on are named sentinels matched with errors.Is.
- Backend capability is split across Backend / WritableBackend / WatchableBackend. Prefer a
new narrow interface over widening an existing one.
## Context
<@-reference the relevant source files, the existing specs, and the guides for anything
the change touches.>
## Instructions
- Structure decisions as a numbered list (D1, D2, …) so they can be cited precisely.
- Include a "Rejected alternatives" section, with reasoning. Do not skip it.
- List every exported name added, removed or changed, and what it costs consumers.
- Describe how each decision will be tested, including what would falsely pass.
- Raise open questions rather than resolving them yourself — I will answer them.
- Set status to `draft`, and save to
docs/development/specs/YYYY-MM-DD-<slug>.md
Context worth attaching¶
| Always | Why |
|---|---|
docs/development/index.md |
conventions, structure, the one architectural rule |
docs/development/specs/ |
existing specs, as format and depth examples |
go.mod |
module path and dependency versions |
| When relevant | |
|---|---|
store.go, snapshot.go, layer.go |
anything touching load, merge or precedence |
write.go, plan.go |
anything touching writing or routing |
notify.go, watch.go, settle.go |
anything touching observers or change detection |
backend.go |
anything touching the backend contract |
docs/explanation/ |
the reasoning the change has to remain consistent with |
Err toward too much context rather than too little. An assistant can ignore a file it does not need; it cannot infer one it was never given.
Current specs¶
| Spec | Status |
|---|---|
| config-vault adapter | approved |
| config-iofs adapter | approved |
| config-billy adapter | approved |
| config-aws-s3 adapter | approved |
| config-gcp-gcs adapter | approved |
| config-azure-blob adapter | approved |
| config-sftp adapter | approved |
| Filesystem adapters | approved |
| config-aws-ssm adapter | approved |
| config-azure-appconfig adapter | approved |
| config-gcp-parameter adapter | approved |
| config-consul adapter | implemented |
| Dynamic backend adapters | approved |
| Filesystem abstraction | approved |
| Non-YAML format adapters | approved |
| Store architecture | approved |
| Structure-preserving config writes | superseded |