One Note, End to End
The loop from the system page, carrying a real note through real schemas — including the field a consumer quietly never mapped.
"How it flows" in words is one thing. Here is the same loop carrying real data. Every request and response field below is the actual schema from the repos, and the classification is a real model prediction, not a mock-up.
The article text and all three predicted labels are real: gold row
g002 from the classifier's eval set, a prediction that matches
both the hand label and the LLM judge on every axis. The note's title,
id, and timestamps are illustrative placeholders.
-
A note is created
POST /notestakes a title and content. It returns201immediately and schedules the enrichment as a background task, sotagsis still empty at this point.POST /notes { "title": "USS Ronald Reagan first ordnance drop", "content": "F/A-18E Hornets assigned to the \"Eagles\" of Strike Fighter Attack Squadron 115 ... in support of Operation Iraqi Freedom." } -
The classifier is forced to answer in schema
The background task POSTs the text to the classifier's
/classifyendpoint. Oneclaude-sonnet-5call withtool_choicepinned to theclassify_articletool forces the model to answer only as an enum-validated tool call, which the endpoint returns as{category, operational_domain, region}. No confidence score, by design.// forced tool_use emitted by the model { "name": "classify_article", "input": { "category": "operations", "operational_domain": "air", "region": "middle-east" } } -
Labels become namespaced tags
notes-api owns the writeback. It maps the fields it knows to prefixed tags (note the
operational_domain→domain:rename) and applies them with replace semantics, so reprocessing converges instead of piling up duplicates. The user's own tags are preserved. The background task writes through the ORM in-process rather than calling the service's own HTTP endpoint; the equivalentPUT /notes/{id}/tagsexists for external callers and carries the same replace semantics.Note what happens to
regionhere: nothing. The classifier grew a third field in v3; this consumer still reads two and drops the rest on the floor. It doesn't break, because the writeback pulls fields out by name instead of assuming the response shape. Wiringregion:through is pending work, and until it lands this page shows two tags because two tags is what actually gets written.I originally wrote that this was the contract doing its job. That was too generous to me. Tolerating an unknown field is good consumer hygiene, but it isn't a guard, and the guard I implied turned out not to exist. The frozen contract for this seam requires a provider change to land with a coordinated consumer update, and when the classifier shipped
regionthat update didn't happen and no build went red — because each repo's “contract test” asserts against its own copy of the shape rather than a shared one. Nothing broke here, and nothing would have noticed if it had. That's written up honestly in the SYS-004 amendment rather than quietly fixed.classifier_tags({ "category": "operations", "operational_domain": "air", "region": "middle-east", # not mapped yet — ignored, not an error }) # -> ["category:operations", "domain:air"] # written in-process; note.tags = merge_tags(note.tags, new_tags) { "tags": ["category:operations", "domain:air"] } -
The note is now enriched
Reading it back shows the labels in its flat
tagslist. There is nostatusfield anywhere: "enriched" is simply the presence ofcategory:/domain:tags, which keeps the schema small and the state honest.GET /notes/1 { "id": 1, "title": "USS Ronald Reagan first ordnance drop", "content": "F/A-18E Hornets ... Operation Iraqi Freedom.", "tags": ["category:operations", "domain:air"], "created_at": "...", "updated_at": "..." } -
The agent can find it
The kb-agent's
search_notestool filters by tag over the same HTTP seam, so the freshly-tagged note is now retrievable for grounding. The result follows the system-wide observation contract (SYS-003): a status, a summary, the payload, and a cited source.search_notes(tag="domain:air") -> GET /notes?tag=domain:air { "status": "success", "summary": "1 matching note(s).", "payload": [ { "id": 1, "title": "USS Ronald Reagan first ordnance drop", "tags": ["category:operations", "domain:air"] } ], "source": "notes-api service, http://127.0.0.1:8000/notes" }