01fb5a2703
There is no CI: the instance has no act_runner and none is planned, so releases are cut by hand. That makes `make check` the only thing standing between a mistake and the tracker, and it is one command: gofmt, vet, the suite with the cache defeated, `go mod verify`, a vendored build, and `kettle gen skills --check`. The last one is the invariant worth having — the plugin's SKILL.md command reference is generated from the binary's registry, so a flag that changed cannot ship with documentation that recommends the old one. `cli/cmd/release` publishes to Gitea using the same SDK the binary already vendors, which is a pleasing thing to be able to say: nothing third-party handles the artifacts. It is a second binary rather than a `kettle` subcommand on purpose — `kettle`'s command tree is what generates the plugin's skills, so a verb there ships to every operator, and publishing a release is build infrastructure. It is idempotent end to end: an existing release for the tag is reused, an asset of the same name is replaced rather than doubled, and a retried run converges instead of duplicating. `make release` refuses three things, each with its own message: a dirty working tree, a TAG that is not what `git describe` reports, and a tag the remote does not have. A release built from uncommitted code is unreproducible and nobody finds out until they need to reproduce it. `kettle version` reports the stamp, the toolchain and the VCS revision. The default is `dev`, and a hand build says so and means it — a binary out of somebody's working tree is not a release and must not claim to be one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
174 lines
8.0 KiB
Markdown
174 lines
8.0 KiB
Markdown
# 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 `<slug>.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 `<id>.<something>` 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 lives in the plugin
|
|
(`plugins/kettle/skills/issue/references/format.md`) — when the taxonomy moves,
|
|
both change.
|
|
- **Do not** document how any of this reaches a tracker.
|