refactor: split issue domain from Gitea transport
An issue was a Gitea row that happened to be cached locally: its identity was the tracker's number (42.md), its dependencies were tracker numbers (depends: [#12]), and a local issue existed only as a draft that push deleted on success. Nothing could be planned or tracked without a tracker. Split into layers, with knowledge flowing one way: skills/issue DOMAIN what an issue is: format, validation, dep graph ^ offline; stdlib imports only, no subprocess | imports skills/sync BRIDGE map.py md <-> Gitea JSON, pure, no I/O _gitea.py login pin, api, pagination, filters skills/use REFERENCE tea CLI docs for non-issue entities skills/issue never imports skills/sync. Delete the sync layer and the domain keeps working. Identity is now a slug derived from the title (wire-sqlc-appclick.md) and is stable across retitles and pushes. Tracker numbers live in a `gitea:` field, never in a file name and never in `depends:`; the pair is indexed in .remote.json, which is a cache over the files, not a second source of truth. Behavior changes: - Pushing is additive. The file is never deleted; it gains gitea:/url:/ synced: and origin: flips from local to gitea. `origin: local` is a durable state, not a pending one. - Pushes go in topological order so dependencies get numbers first. - The dependency graph is computed offline from `depends:` metadata; body prose is passed through unchanged in both directions rather than being rewritten between slugs and #N. - `origin` is domain-owned (whether work exists elsewhere is a fact about the work); the handle and how to reach it stay with sync. Script moves: issue_get.py -> sync/pull.py issue_push.py -> sync/push.py issue_list.py -> sync/remote.py issue_index.py -> issue/issue_index.py _tea.py -> split into issue/issue.py, sync/map.py, sync/_gitea.py New: issue/issue_new.py, issue/issue_check.py, issue/issue_tree.py, and sync/comment.py — comment posting was the last issue operation still hand-rolled through raw `tea api`. references/issue-format.md moves to skills/issue/references/format.md; label hex colors move out of it into map.py, since a color is how a tracker paints a chip, not what an issue is. Verified: offline path end to end (new, check, tree, index, push --dry-run) and read-only against Gitea (remote listing, pull with mapping, comment guard). Write paths of push.py and comment.py are not exercised here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
# 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, and an issue pushed to a tracker keeps it too. Tracker numbers are a
|
||||
foreign key stored in a field, never the name of anything.
|
||||
|
||||
```
|
||||
tmp/issues/wire-sqlc-appclick.md
|
||||
```
|
||||
|
||||
## 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]
|
||||
origin: gitea
|
||||
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** |
|
||||
| `origin` | domain | `local`, or the name of a tracker this also lives in |
|
||||
| `gitea` | sync | the handle in that tracker: `owner/repo#N` |
|
||||
| `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 **durable state, not a pending one.** An issue that never
|
||||
leaves this machine is complete and valid. Pushing is optional and additive.
|
||||
|
||||
## 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.
|
||||
|
||||
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.
|
||||
- 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 link back
|
||||
via their `depends:`. Keep implementation detail in the children; the feature
|
||||
body stays at business level.
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
Бизнес-ценность одним-двумя предложениями.
|
||||
|
||||
## Spec
|
||||
Ссылка или `none`.
|
||||
|
||||
## Motivation
|
||||
Какую проблему пользователя/системы это решает.
|
||||
|
||||
## Issues
|
||||
- [ ] wire-sqlc-appclick — краткое описание части
|
||||
- [ ] …
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user