SYS-020: A repo earns tags when it publishes a number or exposes a contract¶
Status: Accepted Date: 2026-07-19 Deciders: San Lee
Related: SYS-004 (the /classify contract),
SYS-018 (provider-owned contract
artifacts), SYS-017 (evals as CI).
Context¶
A status sweep on 2026-07-19 checked every repo's tags and releases against what its prose
claimed had shipped. The result: defense-news-classifier carries 7 tags and 5 GitHub
releases; twelve other repos carry no releases at all, and six carry no tags either.
That is not automatically wrong — most of those repos genuinely do not need versions. The problem is that the absence is indistinguishable from an oversight. The classifier has a documented semver policy, a CHANGELOG, and a tag per milestone. Every other repo has nothing and says nothing about why. A reader — including a future session running a sweep — cannot tell "deliberately continuous" from "nobody got around to it," so the sweep flags it every time and the flag is noise.
Two of those untagged repos carry a real cost, not just ambiguity:
Published numbers with no anchor. faithfulness-judge states κ = 0.751 (Opus) and 0.716
(Sonnet) at n=189. kb-agent publishes retrieval eval numbers. Neither repo can answer
"which commit produced that?" This is the same failure the published-metrics guard chain
exists to prevent, one layer down: assertions are checked against a metrics file, but nothing
anchors which state of the repo produced the metrics file.
Contracts with nothing to pin. SYS-018
records what happens when a contract moves without coordination — the classifier shipped
v3.0.0 with a new region field and kb-agent read a two-field response for the rest of
the day, no build red on either side. A consumer that wants to pin a known-good provider
state needs the provider to have states. Tags are the cheapest form of that.
Decision¶
A repo gets tags when it publishes a number or exposes a contract another repo depends on. Every other repo is deliberately untagged, and that decision is recorded here so its absence of tags reads as intent rather than neglect.
Tagged¶
| Repo | Why |
|---|---|
defense-news-classifier |
Both: publishes eval numbers, provides the /classify contract. Already compliant — semver + CHANGELOG + release per milestone. This repo's existing practice is the reference implementation. |
faithfulness-judge |
Publishes κ figures that appear in portfolio copy. A cited number needs a commit behind it. |
kb-agent |
Both: publishes retrieval eval numbers, and consumes/exposes the SYS-004 contract. |
notes-api |
Exposes the notes read contract (SYS-006) that kb-agent's search_notes calls. |
Deliberately untagged¶
| Repos | Why |
|---|---|
learning-notes, learning-notes-site, portfolio, sanlee-ys |
Continuously published content. Nobody pins a version of a website; a tag would encode nothing a reader could use. |
dotfiles, agent-ops, architecture |
Always-latest by design. Tagging an operating layer implies a stable release you could deliberately hold back on, which is the opposite of what agent-ops ADR-002 makes these repos — public-first and canonical now. |
desk, job-tracker, github-follow-tracker |
Personal tooling with no external consumer and no published figures. Nothing to reproduce, nothing to pin. |
finance, training |
Retired 2026-08-16. Archived. Still untagged: no external consumer, no published figures. |
The rule, stated for future repos¶
When a new repo appears, ask two questions:
- Does anything outside this repo quote a number it produces? If yes, tag — the number needs a commit behind it.
- Does another repo call it, import it, or assert against its shape? If yes, tag — the consumer needs something to pin.
If both answers are no, leave it untagged and do not treat that as debt.
Downstream surfaces¶
Per SYS-009, the surfaces this decision touches:
| Surface | Change owed |
|---|---|
architecture/README.md decision log |
New SYS-020 row — done in this PR. |
faithfulness-judge |
First tag, anchoring the published κ figures. Follow-on. |
kb-agent |
First tag, anchoring the retrieval eval numbers. Follow-on. |
notes-api |
Post-port tag. Open question: the existing v1-rest-baseline is a milestone marker, not semver, so adopting semver here means two schemes in one repo's tag list. Deliberately not resolved unilaterally. |
dotfiles claude/skills/status-map/SKILL.md |
Must consult this ADR before flagging a missing tag, or it re-raises deliberately untagged repos every run — handled in dotfiles#41. |
| Repos named "deliberately untagged" | None. The decision is that nothing changes there; recording it here is the entire point. |
defense-news-classifier |
None. Already compliant; this ADR ratifies its practice rather than asking anything of it. |
Consequences¶
What this makes easier
- A missing tag becomes readable. Sweeps and fresh sessions can consult this table instead of
re-flagging eleven repos as gaps every run.
- Published figures become reproducible. "κ = 0.751" gains an answer to "from what?"
- Consumers gain something to pin, which is the precondition for the coordinated-update
discipline SYS-018 asks for.
What it costs
- Two versioning schemes now coexist in the house. The classifier's semver is a genuine
contract promise; a tag on faithfulness-judge is closer to a bookmark. That is acceptable
— they are answering different questions — but the distinction should not blur into
"everything must be semver."
- Tagging is a manual step that will be forgotten. This ADR does not add automation, and a
tag that lags reality is a smaller problem than the ambiguity it replaces, but it is not
zero.
What it forecloses / revisit triggers
Revisit if a content repo ever gains a consumer — e.g. if something starts importing
learning-notes content programmatically rather than reading it — or if a personal-tooling
repo starts publishing figures. The rule is about consumers and claims, not about repo size
or effort invested.
Alternatives Considered¶
| Option | Reason not chosen |
|---|---|
| Tag every repo, uniformly | Manufactures ceremony for repos where a version number means nothing. A tag on a notes site is a lie about how it is consumed. |
| Tag nothing but the classifier; leave the rest undocumented | The status quo. It works until something asks "which commit produced this number", and it makes every sweep re-litigate the same eleven repos. |
| Date-stamp releases instead of semver everywhere | Loses the compatibility promise where it genuinely matters (/classify, notes read). Semver is doing real work in exactly the places this ADR keeps it. |