Files
marketplace/cli/internal/mapping/AGENTS.md
T
naudachu 01fb5a2703 feat: publish releases with this repository's own SDK code
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>
2026-08-12 01:01:27 +05:00

126 lines
6.7 KiB
Markdown

# AGENTS.md — internal/mapping (BRIDGE)
**md ↔ Gitea's payloads. The whole translation, and only the translation.**
Pure functions: no network, no filesystem, no flags, no clock. Give it a payload
and it hands back a domain issue; give it an issue and it hands back a request
body. That purity is the point — it can be reasoned about and tested without a
Gitea anywhere, and it is the one package to open when the two representations
disagree.
Imports [`issue`](../issue/AGENTS.md), [`wire`](../wire/AGENTS.md) and the SDK.
Nothing imports it but [`cmd`](../cmd/AGENTS.md) — not the domain, not the
transport. Both sides speak the SDK's shapes, which is what lets the two meet
without either reaching into the other.
| file | what is in it |
|---|---|
| `mapping.go` | the sync-owned metadata keys (`gitea`, `url`, `synced`, `remote-updated`, `comments`, `branch`), `Origin`, `ApplyRemote` |
| `frompayload.go` | `FromPayload` and the accessors around it: `LabelNames`, `AssigneeLogins`, `MergeCheckboxState` |
| `torequest.go` | the create/edit request bodies, `LabelIDsFor` |
| `labels.go` | `LabelColor`, `LabelSpecs`, `CanonicalLabelSpecs`, `DefaultColor` |
| `marker.go` | `IDMarker`, `IDInBody`, and the strip on the way in |
| `layering_test.go` | the two tests that keep this package pure |
## What crosses the boundary, and what does not
```
domain Gitea note
----------------------------------------------------------------------
id (slug) body marker <!-- kettle:id … -->, first line of the
tracker-side body; stripped out of the
local copy — see marker.go
title title verbatim, both ways
body body verbatim up, verbatim down except the
marker and checkbox state
state state open/closed, the same vocabulary
labels labels[] names both ways; ids only on write
assignees assignees[] logins
milestone milestone.title resolved to an id on write
depends — slugs; #N is translated at this edge
— number, html_url lands in Extra as gitea:/url:
— ref Extra as branch:; push fills it from git
```
Only the **login** of a Gitea user crosses: it is what `assignees:` holds, and a
display name is not an identity anything can be pushed against. Label and
assignee lists are appended into a nil slice, so an issue with no labels is the
same value as one loaded from a file — two spellings of "none" is a comparison bug
waiting to happen.
`depends:` is the authoritative graph and is always slugs. The body's
`## Depends on` prose is passed through **unchanged in both directions**: a pull
seeds `depends:` from the `#N` it finds there, and a push never rewrites what the
author wrote. Deliberate — a translator that edits prose churns the body on every
round trip.
## The id marker
The **one** thing this package adds to a body, and it adds it because the slug has
to survive a push: push deletes the local file, so the tracker has to be the thing
that remembers what the issue was called here.
- one place formats it (`IDMarker`), one regex reads it — and the regex accepts
more than the formatter writes, including the older `<!-- tea:id … -->`, because
issues pushed before the rename are still in the tracker;
- the **first** valid marker wins; a second is ignored and removed on the way in;
- the captured text must be a slug by the domain's own rule, so a mangled comment
falls back to the title instead of naming a file after garbage;
- it is stripped before anything else looks at the body, so checkboxes, `#N`
references and what lands on disk all see the body the author wrote.
## The checkbox merge
`MergeCheckboxState` is the one exception to "a pull overwrites the body", and
deliberately the narrowest one that works. **A tick is monotone** — an item only
travels `[ ]``[x]` — so the two sides are joined by a **set union**: no base
version, no drift tracking, no conflict to resolve. An item comes out ticked when
either side has it ticked; everything else in the body is still the remote's word.
Matching is on `Checkbox.Text`, which the domain parser has already stripped and
rejoined with single spaces, so rewrapping a long item does not cost it its tick.
It is otherwise literal: reword an item and it is a different item.
The same text more than once is read as the rule says, as a set — one ticked local
item ticks every remote item with that text. Pairing duplicates up by order is the
reading that can still drop a tick, and dropping a tick is the bug this exists to
fix.
## Labels, and the two write paths
Colours live here, not in the domain: a hex code is how a tracker paints a chip and
not what an issue is. `CanonicalLabelSpecs` is derived from the domain's own list
rather than restated, so adding a type over in the taxonomy creates it on the next
bootstrap with no line changing here but the colour. `DefaultColor` paints
everything outside the canonical set, because `tech/*` and `comp/*` are
project-specific and guessing a colour for one invents a meaning it does not have.
`LabelIDsFor` is exported so that a create and a repair cannot derive the answer
differently: **Gitea's edit endpoint carries no labels**, so an issue that already
exists gets its label set through a `PUT`, and a `PUT` that disagreed with what a
create would have sent would make a pushed issue and a re-pushed one two different
things. `nil` means "resolved no ids"; an **empty, non-nil** list means "resolved
some and matched none", which is a statement to the tracker — `[]` clears every
label on the issue.
## Purity, and the one weakening
`layering_test.go` checks **direct** imports and fails on `os`, `net/http`,
`os/exec`, `internal/gitea`, `internal/config` and `internal/project`; a second
test greps the sources for `time.Now`.
It does not walk the dependency closure, and it cannot: the SDK's types come with
the SDK's client attached, so the graph contains an HTTP client whatever this
package does with it. The full reasoning — and why `time` is allowed where it once
was not — is in [`internal/AGENTS.md`](../AGENTS.md).
## Keeping this file true
- **Scope:** every `.go` file here — the field table, the marker, the merge, the
colours.
- **Update it when** a field starts or stops crossing the boundary (the table is
the contract), a sync-owned metadata key is added, the marker spelling changes or
an older one stops being read, or the purity test is loosened.
- **Do not** put a request here that anything else could make. This package returns
values; [`gitea`](../gitea/AGENTS.md) sends them.