reqdeck 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- reqdeck/__init__.py +3 -0
- reqdeck/__main__.py +7 -0
- reqdeck/cli.py +706 -0
- reqdeck/config.py +185 -0
- reqdeck/editor.py +51 -0
- reqdeck/init.py +317 -0
- reqdeck/markers.py +65 -0
- reqdeck/model.py +393 -0
- reqdeck/ops.py +124 -0
- reqdeck/questions.py +62 -0
- reqdeck/render.py +140 -0
- reqdeck/skills/reqdeck-cli/SKILL.md +166 -0
- reqdeck/skills/reqdeck-method/SKILL.md +166 -0
- reqdeck/skills/reqdeck-method/references/markers.md +92 -0
- reqdeck/skills/reqdeck-method/references/writing-requirements.md +125 -0
- reqdeck/store.py +186 -0
- reqdeck/templates/OPEN-QUESTIONS.md +23 -0
- reqdeck/themes/README.md +31 -0
- reqdeck/themes/__init__.py +73 -0
- reqdeck/themes/atom-one-dark.toml +27 -0
- reqdeck/themes/atom-one-light.toml +27 -0
- reqdeck/themes/catppuccin-frappe.toml +27 -0
- reqdeck/themes/catppuccin-latte.toml +27 -0
- reqdeck/themes/catppuccin-macchiato.toml +27 -0
- reqdeck/themes/catppuccin-mocha.toml +27 -0
- reqdeck/themes/dracula.toml +27 -0
- reqdeck/themes/flexoki.toml +27 -0
- reqdeck/themes/gruvbox.toml +27 -0
- reqdeck/themes/monokai.toml +27 -0
- reqdeck/themes/nord.toml +27 -0
- reqdeck/themes/rose-pine-dawn.toml +27 -0
- reqdeck/themes/rose-pine-moon.toml +27 -0
- reqdeck/themes/rose-pine.toml +27 -0
- reqdeck/themes/solarized-dark.toml +27 -0
- reqdeck/themes/solarized-light.toml +27 -0
- reqdeck/themes/textual-dark.toml +27 -0
- reqdeck/themes/textual-light.toml +27 -0
- reqdeck/themes/tokyo-night.toml +27 -0
- reqdeck/viewer.py +1452 -0
- reqdeck-0.1.0.dist-info/METADATA +305 -0
- reqdeck-0.1.0.dist-info/RECORD +44 -0
- reqdeck-0.1.0.dist-info/WHEEL +4 -0
- reqdeck-0.1.0.dist-info/entry_points.txt +2 -0
- reqdeck-0.1.0.dist-info/licenses/LICENSE +21 -0
reqdeck/render.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Markdown views of the model, shared by `reqdeck doc --markdown` and the TUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .model import Model, Req
|
|
9
|
+
from .questions import Question
|
|
10
|
+
from .store import Unit
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _meta(m: Model, r: Req) -> list[str]:
|
|
14
|
+
meta = []
|
|
15
|
+
if r.rationale:
|
|
16
|
+
meta.append(f"*Rationale:* {r.rationale}")
|
|
17
|
+
if r.satisfies:
|
|
18
|
+
meta.append(f"*Satisfies:* {', '.join(r.satisfies)}")
|
|
19
|
+
if r.topics:
|
|
20
|
+
meta.append("*Topic:* " + ", ".join(f"{t} {m.topics.get(t, '?')}" for t in r.topics))
|
|
21
|
+
if r.source:
|
|
22
|
+
meta.append(f"*Source:* {r.source}")
|
|
23
|
+
for k, v in r.extra.items():
|
|
24
|
+
meta.append(f"*{k.capitalize()}:* {v}")
|
|
25
|
+
bits = []
|
|
26
|
+
if r.verification:
|
|
27
|
+
v = m.cfg.verification.get(r.verification)
|
|
28
|
+
bits.append(f"*Verification:* {r.verification}" + (f" — {v.name}" if v else ""))
|
|
29
|
+
if r.status:
|
|
30
|
+
bits.append(f"*Status:* {r.status}")
|
|
31
|
+
if r.security:
|
|
32
|
+
bits.append("*Security relevant*")
|
|
33
|
+
if bits:
|
|
34
|
+
meta.append(" · ".join(bits))
|
|
35
|
+
return meta
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def requirement(m: Model, r: Req, heading: str = "##", chain: bool = False) -> str:
|
|
39
|
+
out = [f"{heading} {r.id}" + (f" — {r.title}" if r.title else ""), "", r.statement, ""]
|
|
40
|
+
if r.detail:
|
|
41
|
+
out += [r.detail, ""]
|
|
42
|
+
meta = _meta(m, r)
|
|
43
|
+
if meta:
|
|
44
|
+
out += [" \n".join(meta), ""]
|
|
45
|
+
if chain:
|
|
46
|
+
up = [f"- {t} — {m.reqs[t].label if t in m.reqs else '(undefined)'}" for t in r.satisfies]
|
|
47
|
+
down = [f"- {t} — {m.reqs[t].label}" for t in m.satisfied_by.get(r.id, [])]
|
|
48
|
+
out += ["**Satisfies**", ""] + (up or ["- nothing"]) + [""]
|
|
49
|
+
out += ["**Satisfied by**", ""] + (down or ["- nothing yet"]) + [""]
|
|
50
|
+
refs = m.refs_for(r.id)
|
|
51
|
+
if refs:
|
|
52
|
+
out += ["**References**", ""]
|
|
53
|
+
out += [f"- `{x.file}:{x.line}` {'test' if x.is_test else 'code'} — {x.note}" for x in refs]
|
|
54
|
+
out.append("")
|
|
55
|
+
out.append(f"`{r.file}:{r.line}`")
|
|
56
|
+
return "\n".join(out).rstrip() + "\n"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def document(m: Model, unit: Unit) -> str:
|
|
60
|
+
out = [f"# {unit.title}", ""]
|
|
61
|
+
if unit.description:
|
|
62
|
+
out += [unit.description, ""]
|
|
63
|
+
for e in m.document(unit):
|
|
64
|
+
if e["type"] == "section":
|
|
65
|
+
s = e["section"]
|
|
66
|
+
out += [f"{'#' * min(e['depth'] + 1, 6)} {e['number']} {s.title}", ""]
|
|
67
|
+
if s.description:
|
|
68
|
+
out += [s.description, ""]
|
|
69
|
+
else:
|
|
70
|
+
out += [requirement(m, e["req"], heading="######")]
|
|
71
|
+
return "\n".join(out).rstrip() + "\n"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def question(q: Question, topic_title: str) -> str:
|
|
75
|
+
out = [f"## {q.id} — {q.title}", "", f"*Topic {q.topic} — {topic_title}*", "",
|
|
76
|
+
"**Question**", "", q.question or "—", "",
|
|
77
|
+
"**Answer**", "", q.answer or "*Open.*", ""]
|
|
78
|
+
if q.blocks:
|
|
79
|
+
out += ["**Blocks**", "", q.blocks, ""]
|
|
80
|
+
return "\n".join(out)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def excerpt(root: Path, file: str, line: int, before: int = 3, after: int = 8) -> list[tuple[int, str]]:
|
|
84
|
+
"""Numbered source lines around `line` of `file`, which is relative to `root`."""
|
|
85
|
+
try:
|
|
86
|
+
lines = (root / file).read_text(errors="replace").splitlines()
|
|
87
|
+
except OSError:
|
|
88
|
+
return []
|
|
89
|
+
start = max(1, line - before)
|
|
90
|
+
return [(n, lines[n - 1]) for n in range(start, min(len(lines), line + after) + 1)]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
COMMENT = ("//", "/*", "*", "#", '"""', "'''", "--", "///")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def origin(root: Path, file: str, line: int) -> str:
|
|
97
|
+
"""What the marker at `line` is attached to: the first line of code after its
|
|
98
|
+
comment, or "file header" when the comment is the file's own @file block."""
|
|
99
|
+
try:
|
|
100
|
+
mtime = (root / file).stat().st_mtime_ns
|
|
101
|
+
except OSError:
|
|
102
|
+
return ""
|
|
103
|
+
_seen.add((root, file, line, mtime))
|
|
104
|
+
return _origin(root, file, line, mtime)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def origin_cached(root: Path, file: str, line: int) -> bool:
|
|
108
|
+
"""Whether `origin` would answer without reading the file."""
|
|
109
|
+
try:
|
|
110
|
+
mtime = (root / file).stat().st_mtime_ns
|
|
111
|
+
except OSError:
|
|
112
|
+
return True
|
|
113
|
+
key = (root, file, line, mtime)
|
|
114
|
+
return key in _seen
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
_seen: set = set()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@functools.lru_cache(maxsize=4096)
|
|
121
|
+
def _origin(root: Path, file: str, line: int, mtime: int) -> str:
|
|
122
|
+
"""`origin`, cached until the file changes."""
|
|
123
|
+
try:
|
|
124
|
+
lines = (root / file).read_text(errors="replace").splitlines()
|
|
125
|
+
except OSError:
|
|
126
|
+
return ""
|
|
127
|
+
# The comment block the marker sits in, upwards then downwards.
|
|
128
|
+
start = line - 1
|
|
129
|
+
while start > 0 and lines[start - 1].strip().startswith(COMMENT):
|
|
130
|
+
start -= 1
|
|
131
|
+
block = lines[start:line]
|
|
132
|
+
end = line
|
|
133
|
+
while end < len(lines) and (not lines[end].strip() or lines[end].strip().startswith(COMMENT)):
|
|
134
|
+
block.append(lines[end])
|
|
135
|
+
end += 1
|
|
136
|
+
if any("@file" in b for b in block):
|
|
137
|
+
return "file header"
|
|
138
|
+
code = lines[end].strip() if end < len(lines) else ""
|
|
139
|
+
code = code.rstrip("{").rstrip()
|
|
140
|
+
return code if len(code) <= 72 else code[:71] + "…"
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reqdeck-cli
|
|
3
|
+
description: Query, add, edit and validate requirements and open questions with the reqdeck CLI. Use when looking up which requirement covers a behaviour, before implementing or changing a feature, when writing or reviewing a test, adding @REQ_REF markers, writing new requirements, checking traceability, listing open questions, or before claiming work is complete. Applies in any repository with a .reqdeck.toml.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# reqdeck — the CLI
|
|
7
|
+
|
|
8
|
+
A project using reqdeck has `.reqdeck.toml` in its root, and reqdeck installed
|
|
9
|
+
(`pip install reqdeck`, usually in the project's venv). Run it from anywhere inside the
|
|
10
|
+
project:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
reqdeck --json get TSR_009
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
If `reqdeck` is not on the path, `python3 -m reqdeck` is the same command. Every command
|
|
17
|
+
takes `--json`; prefer it, since it is stable and carries the transitive chain the
|
|
18
|
+
human-readable form draws as a tree.
|
|
19
|
+
|
|
20
|
+
The method behind the files — levels, how a requirement is written, the open-question
|
|
21
|
+
workflow, `@REQ_REF` rules — is the **reqdeck-method** skill. This one is the tool.
|
|
22
|
+
|
|
23
|
+
## The files
|
|
24
|
+
|
|
25
|
+
Requirements live in the folder `.reqdeck.toml` names, one *unit* per prefix:
|
|
26
|
+
|
|
27
|
+
| File | Holds | Edit with |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `<unit>.csv` | One requirement per row, ordered by ID, append-only | `reqdeck add`, `reqdeck edit` — never by hand |
|
|
30
|
+
| `<unit>.toml` | Level, prefix, title, description, `code`, sections | By hand |
|
|
31
|
+
| `<unit>.details.md` | Multi-line detail — tables, layouts — one `## <ID>` each | By hand |
|
|
32
|
+
|
|
33
|
+
**Never edit a CSV by hand.** Quoting is easy to break, a hand edit can set a status the
|
|
34
|
+
tool would refuse, and `check` rejects any file not in the exact form the tool writes. If a
|
|
35
|
+
CSV has been edited outside the tool, `reqdeck fmt` rewrites it canonically.
|
|
36
|
+
|
|
37
|
+
Open questions live in markdown (`questions =` in `.reqdeck.toml`) and are edited by hand.
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
| | |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `get <ID>` | One requirement with its chain up and down, detail, code and test refs |
|
|
44
|
+
| `search <regex>` | Over id, title, statement, rationale and detail |
|
|
45
|
+
| `list [--kind --unit --topic --section --verification --status --security] [-l]` | Filtered inventory |
|
|
46
|
+
| `tree <ID> [--up\|--down]` | Just the chain |
|
|
47
|
+
| `doc <unit> [--markdown]` | A unit as a document, sections numbered |
|
|
48
|
+
| `levels` | Levels, and the units in each |
|
|
49
|
+
| `topics` | Every topic: open and answered questions, requirement count |
|
|
50
|
+
| `questions [--topic NN] [--open\|--answered] [-l]` | The open questions |
|
|
51
|
+
| `coverage` | Where traceability is missing |
|
|
52
|
+
| `trace <ID>...` | Are these referenced from code and tests? Exit 1 if not |
|
|
53
|
+
| `check` | Validate everything. **Exit 1 on any problem** |
|
|
54
|
+
| `verification` | The verification methods this project uses |
|
|
55
|
+
| `add <PREFIX> ...` | Allocate the next ID and append a requirement |
|
|
56
|
+
| `edit <ID> --<field> ...` | Change fields of an existing requirement |
|
|
57
|
+
| `unit <file> --level --prefix --title [--code]` | Create a new unit |
|
|
58
|
+
| `fmt` | Rewrite the CSVs in canonical form |
|
|
59
|
+
| `url <ID>...` | Link to the requirement's row on GitHub, for an issue |
|
|
60
|
+
|
|
61
|
+
## Before implementing anything
|
|
62
|
+
|
|
63
|
+
Find what governs it, and read the statement before writing code:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
reqdeck --json search 'keyframe|interpolat'
|
|
67
|
+
reqdeck --json get TSSR_FWN_012
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If nothing matches, that is a finding — say so rather than inventing behaviour.
|
|
71
|
+
|
|
72
|
+
## Adding a requirement
|
|
73
|
+
|
|
74
|
+
**A new ID comes from `add`, never from typing one.** It allocates the next number, finds
|
|
75
|
+
the unit holding the prefix, validates every link, and writes `Proposed` — it has no way
|
|
76
|
+
to write anything else.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
reqdeck add TSSR_FWN --title "Channel PWM" \
|
|
80
|
+
--statement "The Node shall drive each channel with 12-bit PWM at 1 kHz." \
|
|
81
|
+
--satisfies TSR_025,TSR_026 --topic 06 --source "OQ-0608." --verification S \
|
|
82
|
+
[--rationale "..."] [--section render.pwm] [--security]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- `--topic` is required on levels that set `topic = true`; it must be a topic in
|
|
86
|
+
OPEN-QUESTIONS.
|
|
87
|
+
- `--satisfies` must name existing requirements of a level this one may satisfy.
|
|
88
|
+
- `--section` must be a key declared in the unit's `.toml`. Add the section there first.
|
|
89
|
+
- A column the unit has beyond the standard ones: `--field <column> <value>`.
|
|
90
|
+
- Stories: `add A --statement "As a …"` — no source, verification or status.
|
|
91
|
+
|
|
92
|
+
It refuses a prefix no unit holds. A new prefix is a new unit of the design — create it
|
|
93
|
+
deliberately with `reqdeck unit`, and only when the user has agreed the unit exists.
|
|
94
|
+
|
|
95
|
+
Multi-line detail does not go in the CSV. Append a block to `<unit>.details.md`:
|
|
96
|
+
|
|
97
|
+
```markdown
|
|
98
|
+
## ICD_BUS_014
|
|
99
|
+
|
|
100
|
+
| Off | Len | Field | Notes |
|
|
101
|
+
|---|---|---|---|
|
|
102
|
+
| 0 | 1 | Form and count | ... |
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Changing a requirement
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
reqdeck edit TSR_009 --statement "..." --source "OQ-0507. Reopened 2026-10-10 by OQ-0512: ..."
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`edit` changes any field but `id` and `status`. **Changing an Accepted requirement turns
|
|
112
|
+
it Reopened automatically** — say what changed and why in `source`. Nothing in the CLI
|
|
113
|
+
sets `Accepted`: only the user does, in the TUI (`reqdeck tui`, key `a`) or by hand.
|
|
114
|
+
Never work around that.
|
|
115
|
+
|
|
116
|
+
IDs are permanent. Never renumber, never reuse a retired ID, never delete a row that code,
|
|
117
|
+
tests or issues may point at.
|
|
118
|
+
|
|
119
|
+
## Sections
|
|
120
|
+
|
|
121
|
+
A requirement's `section` is a stable key; numbers are derived from declaration order in
|
|
122
|
+
the unit's `.toml`, so inserting a section never touches a row:
|
|
123
|
+
|
|
124
|
+
```toml
|
|
125
|
+
[[sections]]
|
|
126
|
+
key = "power"
|
|
127
|
+
title = "Power"
|
|
128
|
+
|
|
129
|
+
[[sections]]
|
|
130
|
+
key = "power.bus" # nests under power: shown as 1.1
|
|
131
|
+
title = "Bus supply"
|
|
132
|
+
description = "What the cable carries."
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Reorder or insert by editing the `.toml`. Move a requirement with `edit --section`.
|
|
136
|
+
|
|
137
|
+
## Interpreting `check` and `coverage`
|
|
138
|
+
|
|
139
|
+
`check` fails on anything structurally wrong: a dangling link, a link to the wrong level, a
|
|
140
|
+
duplicate ID, a malformed ID, a missing required field, an undeclared section or topic, a
|
|
141
|
+
non-canonical CSV, a detail block for a requirement that is not in that unit, and every
|
|
142
|
+
`@REQ_REF` rule. It prints the status tally and never fails on it.
|
|
143
|
+
|
|
144
|
+
`coverage` reports work not yet done:
|
|
145
|
+
|
|
146
|
+
| | |
|
|
147
|
+
|---|---|
|
|
148
|
+
| `dangling` | Satisfies something that does not exist — always a bug |
|
|
149
|
+
| `orphans` | No satisfies link. Legitimate only with the reason in the rationale |
|
|
150
|
+
| `unsatisfied` | A story, or a parent level, nothing satisfies yet |
|
|
151
|
+
| `untested` | Verification is automated, but no test names it or anything beneath it |
|
|
152
|
+
| `uncoded` | Unit has `code = true`, but no code names it |
|
|
153
|
+
| `not_accepted` | Awaiting the user's review — not a gap to close by editing |
|
|
154
|
+
|
|
155
|
+
Gaps are normal while topics are still being discussed. **Do not close them by inventing
|
|
156
|
+
links or markers.** Report them.
|
|
157
|
+
|
|
158
|
+
## Before claiming work is complete
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
reqdeck trace TSSR_FWN_012 ICD_BUS_014 # what the change claims to implement
|
|
162
|
+
reqdeck check
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`trace` refuses a level marked `traced = false` (typically the system level) and names
|
|
166
|
+
what satisfies it instead: a task names the level that reaches code.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reqdeck-method
|
|
3
|
+
description: The reqdeck way of designing a product with its user — product brief, topics, open questions answered one at a time, then requirements derived from the answers, traced to code and tests with @REQ_REF. Use when setting up a new project for requirements, starting or continuing design work, discussing or answering an open question, writing or reviewing requirements, deciding a requirement's level, or adding @REQ_REF markers to code and tests.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# The reqdeck method
|
|
7
|
+
|
|
8
|
+
A design is built *with* the user, not for them. The agent researches, drafts, lays out
|
|
9
|
+
options and recommends; **the user decides**. Every requirement traces to a decision the
|
|
10
|
+
user actually made, and the files record which requirements the user has actually read.
|
|
11
|
+
|
|
12
|
+
The tool is the **reqdeck-cli** skill. Two references go with this file — read them when the
|
|
13
|
+
work reaches them:
|
|
14
|
+
|
|
15
|
+
- [references/writing-requirements.md](references/writing-requirements.md) — levels,
|
|
16
|
+
fields, how a statement is written, verification methods, status.
|
|
17
|
+
- [references/markers.md](references/markers.md) — `@REQ_REF` in code and tests.
|
|
18
|
+
|
|
19
|
+
## The rules that matter most
|
|
20
|
+
|
|
21
|
+
**Do not invent requirements.** A requirement carries a `source` naming what decided it —
|
|
22
|
+
an answered question, an ADR, a dated choice by the user. If you cannot write that source,
|
|
23
|
+
it is not a requirement: it is a question, and it goes in OPEN-QUESTIONS. **A number
|
|
24
|
+
nobody chose is not a requirement either.** "At least 50 channels" is a requirement only
|
|
25
|
+
if the user chose 50.
|
|
26
|
+
|
|
27
|
+
**OPEN-QUESTIONS is the source of truth.** Requirements are derived from answered
|
|
28
|
+
questions; architecture notes, ADRs, READMEs and agent instructions restate them and go
|
|
29
|
+
stale. When a document disagrees with an answered question, the question is right and the
|
|
30
|
+
document is wrong — fix the document. Check a requirement against the question that
|
|
31
|
+
produced it rather than assuming it kept up.
|
|
32
|
+
|
|
33
|
+
**Never set a requirement to `Accepted`.** One you write is `Proposed`; one you change
|
|
34
|
+
that was `Accepted` becomes `Reopened`. Only the user accepts, after reading. An agent that
|
|
35
|
+
accepts its own work destroys the one signal recording what a human has reviewed. The CLI
|
|
36
|
+
cannot write `Accepted`; do not get around it.
|
|
37
|
+
|
|
38
|
+
**Work topic by topic.** Requirements for a topic are written after the topic is
|
|
39
|
+
discussed, not before.
|
|
40
|
+
|
|
41
|
+
**Ask rather than assume** when a decision would materially change the design. Give a
|
|
42
|
+
recommendation; let the user decide.
|
|
43
|
+
|
|
44
|
+
**A risk is not an open question.** A question is undecided and gets decided. A risk may
|
|
45
|
+
happen whatever is decided, and gets watched — in a risk register, not as a question.
|
|
46
|
+
|
|
47
|
+
## Phase 0 — Setting up
|
|
48
|
+
|
|
49
|
+
1. Install reqdeck: `pip install reqdeck`, in the project's venv if it has one.
|
|
50
|
+
2. Before running the setup, agree with the user what the parts of the product are. The
|
|
51
|
+
setup asks for the *shape* of the requirement tree — see
|
|
52
|
+
[writing-requirements.md](references/writing-requirements.md#levels) — and for one unit
|
|
53
|
+
per part (firmware, hardware, app, an interface between two of them). It is fine to
|
|
54
|
+
start with fewer units; `reqdeck unit` adds one later.
|
|
55
|
+
3. Run `reqdeck init` from the project root. It is interactive: relay each question to
|
|
56
|
+
the user rather than answering it yourself, or have the user run it. It writes
|
|
57
|
+
`.reqdeck.toml`, creates the units and OPEN-QUESTIONS, and copies both skills into
|
|
58
|
+
`.claude/skills/` and `.agents/skills/` for Claude Code and Codex. Commit them with the
|
|
59
|
+
project; after upgrading reqdeck, `reqdeck skills --update` refreshes them.
|
|
60
|
+
4. Record in the project's agent instructions (`AGENTS.md` / `CLAUDE.md`) that the project
|
|
61
|
+
uses reqdeck and this method, and which topics are complete as that changes.
|
|
62
|
+
|
|
63
|
+
## Phase 1 — Product brief
|
|
64
|
+
|
|
65
|
+
A short document: what the product is, for whom, the problem it solves, what it
|
|
66
|
+
deliberately is not, and what would make it a success. Draft it from conversation, then
|
|
67
|
+
let the user correct it. Keep it short — a page, not a spec. Everything later is checked
|
|
68
|
+
against it.
|
|
69
|
+
|
|
70
|
+
## Phase 2 — Personas and user stories
|
|
71
|
+
|
|
72
|
+
Personas are short prose in a product document: who they are, what they are comfortable
|
|
73
|
+
with, what they buy, what to avoid. User stories are the root of the requirement tree and
|
|
74
|
+
live in the stories unit, one row each:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
reqdeck add A --statement "As a collector, I want to turn the display on from my phone, so that I don't have to reach behind the shelf."
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
One story prefix per persona (`A-01`, `B-01`). Story IDs are permanent. If you drafted the
|
|
81
|
+
stories, say so, and make "are these stories real?" the first open question.
|
|
82
|
+
|
|
83
|
+
## Phase 3 — Topics
|
|
84
|
+
|
|
85
|
+
Agree with the user the list of areas where design decisions are needed, and write each as
|
|
86
|
+
a topic in OPEN-QUESTIONS:
|
|
87
|
+
|
|
88
|
+
```markdown
|
|
89
|
+
## Topic 04 — Bus physical layer
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Topics are numbered `01`–`99` and never renumbered. Typical topics: scope and market,
|
|
93
|
+
scale, each interface, each part's hardware and software, power, mechanical, verification,
|
|
94
|
+
compliance, security. Topics are a way to cut the work into sessions, so a topic should be
|
|
95
|
+
discussable in a few sittings.
|
|
96
|
+
|
|
97
|
+
## Phase 4 — Finding the open questions
|
|
98
|
+
|
|
99
|
+
For each topic, find the questions *with* the user. A question is something undecided
|
|
100
|
+
whose answer changes the design. While collecting, do not answer:
|
|
101
|
+
|
|
102
|
+
```markdown
|
|
103
|
+
### OQ-0403 — Connector
|
|
104
|
+
**Question:** Which connector joins a Node to the bus, given the 2 mm pitch of the bricks?
|
|
105
|
+
**Answer:**
|
|
106
|
+
**Blocks:** Rev A layout, TSSR_HWB
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
IDs are `OQ-TTNN`: topic, then the next free number within it. **Questions never move**,
|
|
110
|
+
and are never deleted — an answered one stays where it is. `Blocks` says what waits on it,
|
|
111
|
+
so the order to answer them in is visible.
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
reqdeck topics # open and answered per topic
|
|
115
|
+
reqdeck questions --open --topic 04 -l
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Phase 5 — Answering, one question at a time
|
|
119
|
+
|
|
120
|
+
One question, or a few tightly related ones, per session. For each:
|
|
121
|
+
|
|
122
|
+
1. **Read what is already decided.** Answered questions in this and neighbouring topics,
|
|
123
|
+
and requirements already derived from them.
|
|
124
|
+
2. **Research.** Datasheets, prior art, the market, standards, the existing code. Check
|
|
125
|
+
facts rather than recalling them; stock, prices and versions change.
|
|
126
|
+
3. **Lay out the options** with their trade-offs, and **recommend one**. Say what would
|
|
127
|
+
change your recommendation.
|
|
128
|
+
4. **The user decides.** Do not write an answer the user has not given.
|
|
129
|
+
5. **Write the answer in place**, with the date and the reasoning a later reader needs:
|
|
130
|
+
`**Chosen 2026-08-16.** JST SH, because …`. Expensive decisions get an ADR as well.
|
|
131
|
+
6. **New questions** surfaced while answering are added to their topic, not answered in
|
|
132
|
+
passing. A consequence that may hit whatever is decided is a risk.
|
|
133
|
+
7. **Fix downstream documents** that the answer contradicts.
|
|
134
|
+
|
|
135
|
+
## Phase 6 — Requirements from a finished topic
|
|
136
|
+
|
|
137
|
+
When a topic's questions are answered, derive its requirements — see
|
|
138
|
+
[writing-requirements.md](references/writing-requirements.md). Each takes its `source`
|
|
139
|
+
from the answers and its `topic` from the topic, and is written with `reqdeck add`.
|
|
140
|
+
|
|
141
|
+
Writing requirements finds gaps: a number nobody chose, a behaviour no answer covers, a
|
|
142
|
+
parent that does not exist yet. **Each gap becomes a new open question**, not a
|
|
143
|
+
requirement with an invented value. Tell the user what you found.
|
|
144
|
+
|
|
145
|
+
Then the user reviews. They read the `Proposed` requirements and accept them in the TUI:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
reqdeck list --status proposed --topic 06
|
|
149
|
+
reqdeck tui
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Phase 7 — Building it
|
|
153
|
+
|
|
154
|
+
Work is tracked as tasks that name the requirements they implement — at the level that
|
|
155
|
+
reaches code, never a level marked `traced = false`. Code and tests carry `@REQ_REF`
|
|
156
|
+
markers ([markers.md](references/markers.md)); `reqdeck trace` decides whether a change left
|
|
157
|
+
what it claims traced, and `reqdeck coverage` is the measure of progress, not task counts.
|
|
158
|
+
|
|
159
|
+
A change in behaviour that no requirement covers is a finding. Raise it as a question
|
|
160
|
+
rather than shipping unspecified behaviour.
|
|
161
|
+
|
|
162
|
+
## Documents in general
|
|
163
|
+
|
|
164
|
+
Minimum needed to understand the content. No status badges, progress tracking or
|
|
165
|
+
"to be written" placeholders. Describe only the current state — never what moved or what
|
|
166
|
+
used to be there; git holds the history.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# `@REQ_REF` — referencing requirements from code and tests
|
|
2
|
+
|
|
3
|
+
`reqdeck` scans the source tree for markers and links them to requirements. That link is
|
|
4
|
+
what makes `coverage` and `trace` statements about the code rather than about intentions.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
@REQ_REF(<ID>) <a few words on what the requirement asks for>
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## The rules
|
|
11
|
+
|
|
12
|
+
**One ID per marker, and a description after it.** A marker naming three requirements
|
|
13
|
+
reads as one obligation and hides that the three may have diverged; and source read on its
|
|
14
|
+
own should say roughly what it owes, without the requirement file open beside it. Three
|
|
15
|
+
requirements means three lines.
|
|
16
|
+
|
|
17
|
+
**On the code that implements the requirement — never on an interface.** A requirement is
|
|
18
|
+
discharged by the implementation, not by a declaration of intent, and an interface with
|
|
19
|
+
two implementations would otherwise claim the requirement twice while satisfying it once.
|
|
20
|
+
File stems listed in `interface_suffixes` are rejected.
|
|
21
|
+
|
|
22
|
+
**Once, on the declaration.** Where a language splits declaration and implementation (C,
|
|
23
|
+
C++, Objective-C), the marker goes on the header and never also in the `.c` — a promise
|
|
24
|
+
restated in two places is one that can disagree. `declaration_extensions` in
|
|
25
|
+
`.reqdeck.toml` enforces this; languages with one file per type (Swift, Kotlin, Python)
|
|
26
|
+
mark the type's declaration.
|
|
27
|
+
|
|
28
|
+
**In a test, on the test function — never on the fixture.** A fixture covers whatever its
|
|
29
|
+
tests happen to cover, which no one can check. Each test states what it verifies, so if
|
|
30
|
+
four tests cover one requirement, all four carry the marker.
|
|
31
|
+
|
|
32
|
+
**At the level that reaches code.** Mark the sub-system or interface requirement the code
|
|
33
|
+
implements. A level with `traced = false` is never marked; `check` rejects it.
|
|
34
|
+
|
|
35
|
+
**A path containing `test` or `spec`** (configurable as `test_paths`) is a test
|
|
36
|
+
reference; anything else is code.
|
|
37
|
+
|
|
38
|
+
## Examples
|
|
39
|
+
|
|
40
|
+
C, on the implementing header:
|
|
41
|
+
|
|
42
|
+
```c
|
|
43
|
+
/**
|
|
44
|
+
* @file pwm_py32.h
|
|
45
|
+
* @brief The PY32F003 implementation of pwm_interface.
|
|
46
|
+
*
|
|
47
|
+
* @REQ_REF(TSSR_FWN_023) no code above the abstraction may name a vendor register
|
|
48
|
+
*/
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Swift, on the type that implements it:
|
|
52
|
+
|
|
53
|
+
```swift
|
|
54
|
+
/// Keeps one connection per Master and addresses each independently.
|
|
55
|
+
///
|
|
56
|
+
/// @REQ_REF(TSSR_APP_001) several Masters from one client, none aware of another
|
|
57
|
+
final class MasterConnections { … }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
A test, with Given/When/Then — every test function gets this block, marker or not:
|
|
61
|
+
|
|
62
|
+
```cpp
|
|
63
|
+
/**
|
|
64
|
+
* @test Each colour reaches the PWM channel the board assigned to it.
|
|
65
|
+
*
|
|
66
|
+
* Given an indicator wired to channels 4, 5 and 6 for red, green and blue.
|
|
67
|
+
* When a distinct value is set on each element.
|
|
68
|
+
* Then each value is written to that element's own channel, and to no other.
|
|
69
|
+
*
|
|
70
|
+
* @REQ_REF(TSSR_FWM_006) a discrete RGB indicator under independent PWM per colour
|
|
71
|
+
*/
|
|
72
|
+
TEST_F(IndicatorTest, WritesEachElementToItsOwnChannel)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```swift
|
|
76
|
+
/// @test A Master keeps its name across a reconnect over a different transport.
|
|
77
|
+
///
|
|
78
|
+
/// Given a Master named "Station" over WiFi.
|
|
79
|
+
/// When it reconnects over USB.
|
|
80
|
+
/// Then the app shows it as "Station", as one Master.
|
|
81
|
+
///
|
|
82
|
+
/// @REQ_REF(TSSR_APP_002) names key to device identity, not to an address
|
|
83
|
+
func testNameSurvivesTransportChange() { … }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## What `check` enforces
|
|
87
|
+
|
|
88
|
+
A marker naming several IDs, a marker with no description, a marker on an interface, a
|
|
89
|
+
non-test marker outside `declaration_extensions`, a marker to an undefined requirement,
|
|
90
|
+
and a marker on a level that is never traced directly. Whether the *right* code carries
|
|
91
|
+
the marker is a judgement — make it honestly, and never add a marker to close a gap the
|
|
92
|
+
code does not close.
|