← Back to The System

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.

  1. A note is created

    POST /notes takes a title and content. It returns 201 immediately and schedules the enrichment as a background task, so tags is 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."
    }
  2. The classifier is forced to answer in schema

    The background task POSTs the text to the classifier's /classify endpoint. One claude-sonnet-5 call with tool_choice pinned to the classify_article tool 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"
      }
    }
  3. Labels become namespaced tags

    notes-api owns the writeback. It maps the fields it knows to prefixed tags (note the operational_domaindomain: 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 equivalent PUT /notes/{id}/tags exists for external callers and carries the same replace semantics.

    Note what happens to region here: 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. Wiring region: 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 region that 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"] }
  4. The note is now enriched

    Reading it back shows the labels in its flat tags list. There is no status field anywhere: "enriched" is simply the presence of category: / 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": "..."
    }
  5. The agent can find it

    The kb-agent's search_notes tool 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"
    }