# AGENTS.md — internal/issue (DOMAIN) **What an issue IS.** The canonical markdown format, the label taxonomy, validation, checkboxes, the dependency graph, the store, and eviction. It knows **nothing** about any tracker: no Gitea, no logins, no HTTP, no issue numbers. Delete the transport entirely and this layer keeps working — issues that live only on this machine are first-class, not drafts on their way somewhere. Imports [`project`](../project/AGENTS.md) and the standard library, and nothing else; two tests hold that, see [`internal/AGENTS.md`](../AGENTS.md). | file | what is in it | |---|---| | `issue.go` | the `Issue` type, `FromText`/`Text`, `Slugify`, `IsSlug`, `UniqueID`, `DomainKeys` — and the package comment with the annotated file format | | `meta.go` | `ParseMeta`/`RenderMeta`: the metadata block, one field per line | | `taxonomy.go` | `Types`, `Severities`, the section headers, `RequiredSections`, `ExpectedSections`, `CanonicalLabels`, `SectionBody` | | `template.go` | `Template`: the prefilled body per type | | `validate.go` | `Validate`: errors mean malformed, warnings mean it deviates from its template | | `checkbox.go` | `Checkboxes`, `SetCheckbox`, `CheckboxProgress` — pure functions over a string | | `graph.go` | `Graph`, `Dependents`, `TopoOrder`, `FindCycles` over `depends:` | | `depsection.go` | `BodyDepRefs`: references written in `## Depends on` / `## Issues` prose | | `store.go` | `Root`, `AllIDs`, `SlugFiles`, `Load`/`LoadAll`/`Save`, `RequireStore`, `CreateStore`, `StoreError` | | `index.go` | `BuildIndex`: INDEX.md, a view of the directory | | `evict.go` | `Classify`, `Evict`, `Remove`, and the report types | | `layering_test.go` | the two tests that keep a tracker out of this package | ## Identity A slug derived from the title, and **the file name is the id**: ``` .kettle/issues/wire-sqlc-appclick.md ``` ``` --- id: wire-sqlc-appclick state: open labels: [type/task, tech/sql] assignees: [naudachu] milestone: v0.2 depends: [migrate-schema] origin: gitea gitea: owner/repo#42 synced: 2026-08-07T18:40:00Z --- # Wire sqlc into the appclick repo layer ## Summary … ``` Keys down to `origin` are owned here. **Everything below is foreign**: written by the sync layer, carried through load and save verbatim in `Issue.Extra`, never read. That passthrough is what lets one file represent both a local issue and a synced one without the domain learning a second vocabulary. Every metadata field is one line and lists are inline, so plain grep works without a parser: ```bash grep -l 'labels:.*type/bug' .kettle/issues/*.md grep -ln 'depends:.*migrate-schema' .kettle/issues/*.md # who depends on it ``` `FromText` takes an id that **overrides** the one in the block, which is how the store makes the file name authoritative. ## Origin is the safety argument `origin: local` means **this file IS the issue** — there is no other copy, and deleting it deletes the work. It is a complete state, not a pending one. Anything with a tracker origin can be fetched again, which is what makes it safe to remove. Every dangerous operation in this package turns on that one field: - `Classify` splits the store into evictable, protected and still-open. It is **pure** — it reads loaded issues and decides, touching no disk — and a protected issue comes back as protected **even when it was named explicitly**: naming a file does not make deleting it safe. - `Evict` classifies, removes, and rebuilds the index. One implementation, called both by the offline command and by the sync layer — which does nothing to this decision except hand over issues whose `state:` it has just refreshed. - `Remove` is deliberately dumb: it takes an id, not a decision. Whether an issue may go is settled by `Classify` before this is reached, so the dangerous half of the operation has no branches in it at all. The store is a **working set, not an archive**: a closed issue with a tracker origin is evicted, and eviction is not a one-off migration — a pull by number fetches an issue in any state, so a closed issue pulled after an eviction lands on disk again. ## The store, and the three ways it can be missing `AllIDs` reads `.md` and nothing else. **A slug has no dot in it**, so `wire-sqlc.comments.md` is not an issue; without that rule a bare push tries to file a comment thread as a unit of work. `SlugFiles` is the same rule read the other way round — everything named `.` belongs to that issue and goes when it goes, which is how the domain removes an issue completely without learning what a comment thread is. Three failures, three messages, because they are three different things to do next: | answer | means | |---|---| | `project.NotFoundError` | no project at all — run `kettle init` | | `store … does not exist` | a project whose store was never created | | `store … exists but is empty` | a store with nothing filed in it yet | `ErrStoreMissing` marks the first two. Conflating "empty" with "not there" is exactly what once made a missed directory look like an empty backlog. **Nothing creates a store as a side effect of a write** — only `new` and `pull` call `CreateStore`, and both announce it. ## Sections, and what a checkbox is Section headers are fixed English literals in a fixed order; **only body prose is Russian**. `RequiredSections` (`## Summary`, `## Spec`) must be present in every type; `ExpectedSections` are the per-type ones and their absence is a warning. `DepSections` — `## Depends on` and `## Issues` — both name what an issue depends on, so both are edge sources pointing the same way. In a `type/feature` that reads container → child: "the container is closed when its children are closed" *is* a dependency, while "a child belongs to a feature" is membership, and membership has no place in a dependency graph. Which is why a child never names its container back. **`depends:` is the authoritative edge list; body prose is never walked by `Graph`.** `BodyDepRefs` exists so a command can *report* what the prose claims, and never so the graph can be built from it. A checkbox is the one part of a body that is **state** and not prose. `SetCheckbox` is surgical: exactly one byte of the input changes, and everything else — trailing whitespace, the item's own wording, an existing `[X]`'s capital — comes back byte for byte. Ticking a box must not produce a diff wider than the state that changed. Fenced code blocks are skipped whole: `- [ ]` inside a fence is an example of the markup, not a box anybody may tick. `CheckboxProgress` is computed on the fly. Progress is not a metadata field — a second copy of that state would be wrong by the next edit. ## Usage ```go root := issue.Root(out) // out overrides; "" resolves the project if err := issue.RequireStore(root); err != nil { return err } issues, err := issue.LoadAll(root) order := issue.TopoOrder(ids, issue.Graph(issues)) // dependencies first errs, warns := issue.Validate(issues[id], knownIDs) ``` `TopoOrder` breaks cycles deterministically rather than raising: a cycle is a data problem for the caller to report (`FindCycles` finds them), not a reason to refuse to order the rest. ## What does not belong here An issue number, a login, an HTTP call, a label colour, a hex code, a JSON tag, a yaml tag. If one appears in this package it is in the wrong place — colours are [`mapping`](../mapping/AGENTS.md)'s, because a hex code is how a tracker paints a chip and not what an issue is. ## Keeping this file true - **Scope:** every `.go` file in this directory — the format, the taxonomy, the store, the graph, checkboxes, eviction. - **Update it when** a metadata field is added to `DomainKeys`, a type or severity is added to the taxonomy, a required or expected section changes, a file appears or goes in the table above, or any rule about what may be deleted changes. The format's operator-facing statement of intent is embedded in the binary (`cli/internal/scaffold/assets/skills/kettle-issue/references/format.md`) — when the taxonomy moves, both change. - **Do not** document how any of this reaches a tracker.