9479babfe9
format.md listed `wiki:` among the domain fields, between depends and origin, and issue.py had never heard of it. The field fell into extra and rendered with the foreign keys — sorted in after the sync fields, which the same document forbids one line below the table. Written without brackets it parsed as a single string, and nothing but a text editor could set it. Implemented rather than de-documented: page_ls.py --titles already prints these titles, so the field was designed and only unwired. DOMAIN_KEYS and LIST_KEYS learn it, Issue carries it, and issue_new.py gets a repeatable --wiki flag. Titles only, as the format says: no path, no sub_url, no lookup. The tracker has no field for it, so it is never sent and a pull does not bring it back — format.md now says so. Closes #32 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
376 lines
14 KiB
Markdown
376 lines
14 KiB
Markdown
# Issue format
|
|
|
|
Canonical format for every issue in this project, whether it ever reaches a
|
|
tracker or not. Designed to be unambiguous for both humans and LLMs: fixed
|
|
English section headers in a fixed order, verifiable acceptance criteria, one
|
|
issue = one deliverable. Source spec: the project wiki
|
|
([Issues-Workflow](https://git.noodles.cam/claude-skills/tea/wiki/Issues-Workflow)).
|
|
|
|
Nothing here depends on Gitea. How these files are mapped onto a tracker is the
|
|
sync layer's business — see `/tea:sync`.
|
|
|
|
## Identity
|
|
|
|
An issue is one file, `tmp/issues/<id>.md`, and `id` is a slug: lowercase
|
|
ASCII, digits, single dashes, derived from the title. **The slug is the
|
|
identity.** It is stable for the life of the issue — a retitled issue keeps its
|
|
slug; an issue pushed to a tracker, deleted locally and fetched back a month
|
|
later keeps it too. Tracker numbers are a foreign key stored in a field, never
|
|
the name of anything.
|
|
|
|
```
|
|
tmp/issues/wire-sqlc-appclick.md
|
|
```
|
|
|
|
A slug never contains a dot, which is how the store tells an issue from the
|
|
files parked beside it (`<id>.comments.md`).
|
|
|
|
Stability is a promise the format makes, so something has to keep it once the
|
|
file is gone. That is the sync layer's problem and its answer is a marker in the
|
|
body — see `/tea:sync`; the domain neither writes nor reads it, and it never
|
|
appears in the file on disk.
|
|
|
|
## Metadata block
|
|
|
|
One field per line, lists inline, so plain `grep` works without a parser:
|
|
|
|
```markdown
|
|
---
|
|
id: wire-sqlc-appclick
|
|
state: open
|
|
labels: [type/task, tech/sql]
|
|
assignees: [naudachu]
|
|
milestone: v0.2
|
|
depends: [migrate-schema]
|
|
wiki: [Simple Chains/Ideas/Chain core]
|
|
origin: gitea
|
|
branch: feat/wire-sqlc
|
|
gitea: claude-skills/tea#42
|
|
remote-updated: 2026-08-09T18:24:01Z
|
|
synced: 2026-08-09T18:40:00Z
|
|
url: https://git.noodles.cam/claude-skills/tea/issues/42
|
|
---
|
|
# Wire sqlc into the appclick repo layer
|
|
|
|
## Summary
|
|
…
|
|
```
|
|
|
|
| Field | Owner | Meaning |
|
|
|---|---|---|
|
|
| `id` | domain | slug; equals the file name |
|
|
| `state` | domain | `open` or `closed` |
|
|
| `labels` | domain | see namespaces below; exactly one `type/*` |
|
|
| `assignees` | domain | logins; may be empty |
|
|
| `milestone` | domain | title, or `none` |
|
|
| `depends` | domain | ids this issue depends on — **the authoritative graph** |
|
|
| `wiki` | domain | page **titles** this issue is written up in; may be empty. Titles, not URLs — a title is a name for a document and stays in this layer, a URL is tracker bookkeeping. `/tea:page` owns what those titles mean; `page_ls.py --titles` prints them. Set it with `issue_new.py --wiki "<title>"` (repeatable) or by editing the line. The tracker has no field for it, so it is never sent — and a pull, which merges nothing but checkbox state, does not bring it back |
|
|
| `origin` | domain | `local`, or the name of a tracker this also lives in |
|
|
| `gitea` | sync | the handle in that tracker: `owner/repo#N` |
|
|
| `branch` | sync | the tracker's branch link (Gitea `ref`); push fills an empty one with the current git branch, and never overwrites a filled one |
|
|
| `url`, `synced`, `remote-updated`, `comments` | sync | bookkeeping |
|
|
|
|
Domain fields render first, in the order above; sync fields follow, sorted.
|
|
|
|
`origin` is domain-owned on purpose: *whether* a piece of work exists anywhere
|
|
but here is a fact about the work. *Where* that is, and how to reach it, is the
|
|
sync layer's business — the domain carries `gitea:` and the rest through
|
|
load/save verbatim and never reads them. That passthrough is why one file can
|
|
represent a local issue and a synced one without a second format.
|
|
|
|
`origin: local` is a **complete state, not a pending one.** An issue that never
|
|
leaves this machine is valid and finished work; pushing it is optional and
|
|
nothing here treats it as a draft.
|
|
|
|
It is not a *permanent* state, and it is what the file's fate depends on:
|
|
|
|
| `origin:` | what the file is | what a push does to it | what eviction does to it |
|
|
|---|---|---|---|
|
|
| `local` | the issue itself — the only copy there is | creates it in the tracker, then deletes the file | **nothing, ever** — in any state, named or not |
|
|
| a tracker | a working copy of something the tracker already has | updates the tracker, then deletes the file | removes it once `state: closed` |
|
|
|
|
**A successful push deletes `tmp/issues/<id>.md`** (and `<id>.comments.md`), on
|
|
create and on `--update` alike. What is in the store is what has not left this
|
|
machine; everything else is fetched again when it is needed. The rule, its
|
|
safety conditions, and how the slug survives are `/tea:sync`'s to state.
|
|
|
|
**A closed issue is evicted from the store** by `issue_evict.py` — same trade,
|
|
one condition more: the work is done *and* it exists somewhere else. An
|
|
`origin: local` issue is never evicted, because there is nowhere to fetch it
|
|
back from. The store is a working set, not an archive; `pull.py <n>` fetches a
|
|
closed issue again whenever it is wanted.
|
|
|
|
The `id` never changes across that round trip, which is why `depends:` in other
|
|
issues keeps working. That is the format's promise; the mechanism is not.
|
|
|
|
## Language rules
|
|
|
|
- **Issue title**: English, imperative mood, no type prefix — the type lives in
|
|
the label, not the title. Good: `Fix tea-guard crash on empty settings file`.
|
|
Bad: `fix: crash`, `[bug] crash`, `Крашится гвард`.
|
|
- **Section headers**: the exact English literals below, as `##` headings, in
|
|
the given order. Do not translate, rename, or reorder them.
|
|
- **Body prose** (text inside sections): Russian.
|
|
|
|
## Label namespaces
|
|
|
|
Four namespaces classify an issue. Two are exclusive (at most one label from
|
|
the namespace), two are free-form:
|
|
|
|
| Namespace | Exclusive | Purpose |
|
|
|---|---|---|
|
|
| `type/*` | yes | What kind of work; primarily its business value. Mandatory, exactly one. |
|
|
| `severity/*` | yes | Business impact. At most one; apply when the impact is known. |
|
|
| `tech/*` | no | Technology the issue is bound to. Any number. |
|
|
| `comp/*` | no | System component of this repo. Any number; no preset — project-specific. |
|
|
|
|
### `type/*` — mandatory, exactly one
|
|
|
|
| Label | Meaning |
|
|
|---|---|
|
|
| `type/bug` | Something behaves incorrectly in existing code |
|
|
| `type/task` | Implementation of new functionality |
|
|
| `type/refactor` | Internal restructuring: file moves, architecture; behavior must not change |
|
|
| `type/test` | Writing or fixing tests |
|
|
| `type/feature` | Container: several issues delivering one unit of business value |
|
|
| `type/draft` | Idea captured for later; not ready for work |
|
|
|
|
### `severity/*` — at most one
|
|
|
|
`severity/low`, `severity/medium`, `severity/high`, `severity/showstopper`,
|
|
`severity/critical`.
|
|
|
|
### `tech/*` — any number
|
|
|
|
Technology-bound labels, e.g. `tech/sql` (pgx, sqlc, sql-migrate — persistent
|
|
storage), `tech/obs` (grafana, loki, prometheus, alloy — observability),
|
|
`tech/postgres`.
|
|
|
|
### `comp/*` — any number
|
|
|
|
Components of this repo's system, e.g. `comp/appclick`. No preset list —
|
|
derive from the project.
|
|
|
|
> Label **colors** are not part of the format: a hex code is how a tracker
|
|
> paints a chip, not what an issue is. They live in `skills/sync/scripts/map.py`
|
|
> and are applied on push.
|
|
|
|
## Dependencies
|
|
|
|
`depends:` in the metadata block is the graph, and it holds **ids**:
|
|
|
|
```markdown
|
|
depends: [migrate-schema, add-pool-cfg]
|
|
```
|
|
|
|
An optional `## Depends on` section, placed right after `## Spec`, carries the
|
|
human explanation — one reference per line, with a reason where it helps:
|
|
|
|
```markdown
|
|
## Depends on
|
|
- migrate-schema — нужна схема БД из этого issue
|
|
- add-pool-cfg
|
|
```
|
|
|
|
The section is prose and is passed to and from a tracker unchanged; only
|
|
`depends:` is walked when the graph is computed. Keeping them consistent is on
|
|
you — `issue_check.py` warns when the section names an id that `depends:` does
|
|
not list. Omit the section when there are no dependencies; never write an empty
|
|
one.
|
|
|
|
A `type/feature` container writes the same relation under `## Issues` instead
|
|
(see the template below). Same direction, same rule: every id named there also
|
|
belongs in that issue's `depends:`. The warning names whichever of the two
|
|
sections the reference actually came from.
|
|
|
|
Draw the graph with `issue_tree.py`. The reverse direction is a grep:
|
|
|
|
```bash
|
|
grep -ln 'depends:.*migrate-schema' tmp/issues/*.md
|
|
```
|
|
|
|
## Shared rules
|
|
|
|
- `## Summary` is always the first section; `## Acceptance criteria` is always
|
|
present (exception: `type/draft`). These two are the anchors every reader
|
|
(human or LLM) relies on.
|
|
- `## Spec` is **mandatory in every type**. Its value is a repo path
|
|
(`docs/specs/auth.md`), a URL, or the literal `none` when no spec exists.
|
|
Never omit the section and never invent a link — `none` is an explicit,
|
|
valid answer.
|
|
- Acceptance criteria are `- [ ]` checkboxes; each item is an objectively
|
|
checkable condition, not an aspiration.
|
|
- A checkbox is **item markup, not a property of one section**: `- [ ]`
|
|
unticked, `- [x]` ticked, and it means the same under `## Issues` as under
|
|
`## Acceptance criteria`. An item that wraps continues on an indented line
|
|
and is still one item. A `- [ ]` inside a ``` code fence is an example of the
|
|
markup, not state. Tick them with `issue_ac.py`, which reads the whole body
|
|
on exactly these rules and rewrites one character; progress (`3/7`) is
|
|
counted off the body and is never a metadata field.
|
|
- Code references use the `path/file.ext:line` form; related issues by id.
|
|
- Screenshots are allowed but their content must be duplicated as text — an
|
|
LLM reading these files cannot see images.
|
|
- If acceptance criteria grow past ~5 unrelated items, split the issue (or
|
|
promote it to a `type/feature` container with child issues).
|
|
|
|
## Template: `type/bug`
|
|
|
|
```markdown
|
|
## Summary
|
|
Что сломано и где проявляется, одно-два предложения.
|
|
|
|
## Spec
|
|
`docs/specs/auth.md`, URL — или `none`.
|
|
|
|
## Steps to reproduce
|
|
1. …
|
|
2. …
|
|
|
|
## Expected
|
|
Что должно было произойти.
|
|
|
|
## Actual
|
|
Что происходит на самом деле: вывод команды, лог.
|
|
|
|
## Environment
|
|
Только релевантное: версии, ОС, конфигурация.
|
|
|
|
## Acceptance criteria
|
|
- [ ] баг не воспроизводится по шагам выше
|
|
- [ ] добавлена проверка на регрессию (если применимо)
|
|
```
|
|
|
|
## Template: `type/task`
|
|
|
|
```markdown
|
|
## Summary
|
|
Что нужно сделать, одно-два предложения.
|
|
|
|
## Spec
|
|
Ссылка или `none`.
|
|
|
|
## Motivation
|
|
Какую проблему пользователя/системы это решает.
|
|
|
|
## Acceptance criteria
|
|
- [ ] проверяемое условие
|
|
- [ ] …
|
|
|
|
## Constraints
|
|
Что НЕ входит в объём; технические рамки. (опционально)
|
|
```
|
|
|
|
## Template: `type/refactor`
|
|
|
|
```markdown
|
|
## Summary
|
|
Что перестраиваем и в каких файлах (`path/file:line`).
|
|
|
|
## Spec
|
|
Ссылка или `none`.
|
|
|
|
## Motivation
|
|
Чем плохо текущее состояние: дублирование, связность, читаемость.
|
|
|
|
## Invariants
|
|
Что НЕ должно измениться: поведение, публичные API, форматы данных.
|
|
|
|
## Acceptance criteria
|
|
- [ ] проверяемое условие (тесты зелёные, старый путь удалён, …)
|
|
```
|
|
|
|
## Template: `type/test`
|
|
|
|
```markdown
|
|
## Summary
|
|
Что покрываем тестами и где (`path/file:line`).
|
|
|
|
## Spec
|
|
Ссылка или `none`.
|
|
|
|
## Motivation
|
|
Зачем: регрессия после бага, пробел в покрытии, флаки-тест.
|
|
|
|
## Test cases
|
|
- сценарий → ожидаемый результат
|
|
- …
|
|
|
|
## Acceptance criteria
|
|
- [ ] перечисленные кейсы покрыты и зелёные
|
|
- [ ] тесты проходят в CI
|
|
```
|
|
|
|
## Template: `type/feature`
|
|
|
|
A container: one unit of business value delivered by several child issues.
|
|
Child issues carry their own `type/*` (`task`, `bug`, `test`, …) and know
|
|
nothing about the container.
|
|
|
|
**The container depends on its children, never the reverse.** Every child id
|
|
goes in the container's own `depends:` and, as prose, in its `## Issues`
|
|
section; a child's `depends:` is for that child's real dependencies and must
|
|
not point back at the container. Keep implementation detail in the children;
|
|
the feature body stays at business level.
|
|
|
|
That direction is not a convention picked at random. "The container is closed
|
|
when its children are closed" *is* a dependency relation. "This child belongs
|
|
to that feature" is a membership relation, and membership has no place in a
|
|
dependency graph. Pointed the other way the two rules contradict each other:
|
|
the moment the container listed a child that already depended on it,
|
|
`issue_check.py` would report `ERROR cycle`. With the edge going down, the
|
|
graph reads as nesting — `issue_tree.py` draws the container as the root with
|
|
its children beneath it — and the check is green.
|
|
|
|
So the container's metadata block carries the children:
|
|
|
|
```markdown
|
|
depends: [wire-sqlc-appclick, add-pool-cfg]
|
|
```
|
|
|
|
and its body repeats them for a human:
|
|
|
|
```markdown
|
|
## Summary
|
|
Бизнес-ценность одним-двумя предложениями.
|
|
|
|
## Spec
|
|
Ссылка или `none`.
|
|
|
|
## Motivation
|
|
Какую проблему пользователя/системы это решает.
|
|
|
|
## Issues
|
|
- [ ] wire-sqlc-appclick — краткое описание части
|
|
- [ ] add-pool-cfg — краткое описание части
|
|
|
|
## Acceptance criteria
|
|
- [ ] все дочерние issues закрыты
|
|
- [ ] проверяемое условие уровня фичи (например, e2e-сценарий работает)
|
|
```
|
|
|
|
## Template: `type/draft`
|
|
|
|
A parking spot for ideas that are not fleshed out yet. Minimal structure, no
|
|
acceptance criteria required. Before implementation starts, a draft MUST be
|
|
promoted: relabeled to a concrete type and rewritten into that type's template.
|
|
|
|
```markdown
|
|
## Summary
|
|
Идея одним-двумя предложениями.
|
|
|
|
## Spec
|
|
Ссылка или `none` (для драфтов обычно `none`).
|
|
|
|
## Notes
|
|
Свободные заметки: что известно, открытые вопросы, варианты.
|
|
```
|
|
|
|
## Containers beyond `type/feature`
|
|
|
|
- **Milestone** — a set of issues with an optional time bound. Locally it is
|
|
just the `milestone:` field; a tracker-side milestone must already exist for
|
|
a push to attach the issue to it.
|
|
- **Project** — a set of issues tracked by status columns (Backlog, ToDo,
|
|
InProgress, Ready, Done). Not represented in this format and not reachable
|
|
through the Gitea API — web UI only.
|