<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: GriffinHayes3461</title>
    <description>The latest articles on DEV Community by GriffinHayes3461 (@griffinhayes3461).</description>
    <link>https://dev.to/griffinhayes3461</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4064955%2F4964157e-82db-4dff-9f23-f956c3b09c3a.png</url>
      <title>DEV Community: GriffinHayes3461</title>
      <link>https://dev.to/griffinhayes3461</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/griffinhayes3461"/>
    <language>en</language>
    <item>
      <title>Failure-Bounded Node.js Topic Classification with Semantic Search and LLM Evidence</title>
      <dc:creator>GriffinHayes3461</dc:creator>
      <pubDate>Fri, 07 Aug 2026 00:40:52 +0000</pubDate>
      <link>https://dev.to/griffinhayes3461/failure-bounded-nodejs-topic-classification-with-semantic-search-and-llm-evidence-i7k</link>
      <guid>https://dev.to/griffinhayes3461/failure-bounded-nodejs-topic-classification-with-semantic-search-and-llm-evidence-i7k</guid>
      <description>&lt;p&gt;The operational constraint that changes this design is the consequence of a wrong topic: a removable search facet and a compliance-routing decision do not deserve the same pipeline. &lt;strong&gt;Short answer:&lt;/strong&gt; for reviewable document tagging, use Node.js to coordinate immutable source revisions, versioned embeddings, broad semantic retrieval, a separate rerank step, and an LLM classifier constrained by a topic catalog; publish only validated decisions, and preserve enough evidence to replay each stage.&lt;/p&gt;

&lt;p&gt;Do not collapse those stages into one call. Semantic search answers which evidence looks related, reranking changes the order of that evidence, and classification applies a labeling policy. A system that stores only the final tag cannot tell which claim failed.&lt;/p&gt;

&lt;p&gt;Keep that boundary.&lt;/p&gt;

&lt;p&gt;This architecture decision record assumes that topics change, source documents are revised, remote calls can be retried, and ambiguous documents may go to review. It does not assume that a similarity score is a probability or that one threshold travels safely between corpora. The right threshold isn't knowable from the interface alone; labeled evaluation data from the actual corpus is what would resolve it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should a Node.js semantic search, embeddings, rerank, and LLM classifier store?
&lt;/h2&gt;

&lt;p&gt;Store the source revision and the evidence chain, not merely &lt;code&gt;topic = "security"&lt;/code&gt;. The durable record needs a digest of normalized content, a normalization revision, the embedding revision, the topic-catalog revision, retrieved candidate identifiers and scores, the reranker revision and new ordering, the classifier-policy revision, the validated response, and the final disposition. The source object stays canonical. Every derived item points back to the exact source revision that produced it.&lt;/p&gt;

&lt;p&gt;Four invariants carry most of the load. A source revision has exactly one normalized-content digest under a named normalization revision. An embedding belongs to that digest and its embedding revision; mixing vectors created under incompatible revisions is a data error, not model nuance. A published label must exist in the catalog revision supplied to the classifier. Reprocessing writes another decision record and moves an active pointer only after validation, rather than overwriting the prior evidence.&lt;/p&gt;

&lt;p&gt;That last rule matters during taxonomy edits. Suppose &lt;code&gt;account-security&lt;/code&gt; is split into &lt;code&gt;authentication&lt;/code&gt; and &lt;code&gt;authorization&lt;/code&gt;. Re-embedding may be unnecessary when document content has not changed, but retrieval exemplars, reranking behavior, and classifier policy may all need a new revision. Keeping those identities separate allows targeted recomputation. A single &lt;code&gt;pipeline_version&lt;/code&gt; string can still provide a convenient deployment boundary, but it shouldn't erase the component revisions needed to explain a transition.&lt;/p&gt;

&lt;p&gt;The publication key should be derived from stable inputs such as source revision, normalized digest, catalog revision, and pipeline revision. Then two workers processing the same revision converge on one logical result. If publication is exposed through HTTP, RFC 9110's method semantics are relevant, but they don't make an application-specific operation safe by wishful thinking: the server-side conditional write and operation key must enforce the intended replay behavior.&lt;/p&gt;

&lt;p&gt;Keep raw content out of routine logs. Traces can carry the document revision, stage revision, candidate IDs, duration, and disposition while sensitive text remains in its governed store. Evidence snippets, if retained, need the same deletion and access policy as the source because a small excerpt can still contain the material the storage policy was meant to protect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Name the failure boundaries before choosing a classifier
&lt;/h2&gt;

&lt;p&gt;The useful boundaries are source read, extraction, normalization, chunking, embedding, candidate retrieval, reranking, classification, schema validation, and publication. &lt;code&gt;AI failed&lt;/code&gt; is not an actionable status. &lt;code&gt;retrieval_empty&lt;/code&gt;, &lt;code&gt;catalog_violation&lt;/code&gt;, and &lt;code&gt;publication_conflict&lt;/code&gt; point to different owners and different recovery paths.&lt;/p&gt;

&lt;p&gt;Extraction is an especially quiet failure. A loader can emit an empty string for a valid document ID, flatten a table in the wrong reading order, or retain navigation text that overwhelms the body. Imagine a 12,000-document import in which 317 records use &lt;code&gt;content&lt;/code&gt; while the adapter reads &lt;code&gt;body&lt;/code&gt;. The embedding stage may still accept an empty or boilerplate-only value unless the boundary rejects it. Counters can look healthy while recall decays. Validate the envelope before normalization, record empty-text counts by connector and revision, and retain a content digest so a corrected adapter triggers deliberate recomputation. Retrieval has a different failure shape. pgvector supports exact and approximate nearest-neighbor search and multiple distance functions, including cosine distance; that makes it a suitable implementation example, not a guarantee that the first neighbor represents the correct policy label. Index choice affects the candidate-generation boundary, while the classifier's job remains downstream. Preserve the initial candidate set before reranking so an evaluation can distinguish “the relevant exemplar was never retrieved” from “the reranker demoted it.” Then there is drift. Document length, language, template boilerplate, new terminology, and topic prevalence can change independently. Monitor acceptance, abstention, and review outcomes by those slices rather than trusting one aggregate accuracy number. A frozen evaluation set is useful for regression checks, but live sampling catches data shapes the frozen set doesn't contain. Don't log a confidence number without its scoring method and revision — &lt;code&gt;0.82&lt;/code&gt; has no stable meaning by itself.&lt;/p&gt;

&lt;p&gt;Network uncertainty also crosses the storage boundary. A client may lose the response after a successful write and retry. A second classification call can produce another valid ordering or label, even with identical input, so blindly repeating the whole pipeline turns transport uncertainty into state ambiguity. Persist stage completion under the operation key, resume from the last validated artifact, and make the final publication conditional. A duplicate attempt should receive a deterministic existing result or a clearly handled &lt;code&gt;409&lt;/code&gt; conflict, never create an untraceable second canonical decision.&lt;/p&gt;

&lt;p&gt;Small failures compound.&lt;/p&gt;

&lt;p&gt;Observability should therefore answer three questions without opening the document: which revision ran, where it stopped, and whether any externally visible pointer changed. Track per-stage latency and outcome counts, candidate-set size, schema rejection, unknown-topic rejection, abstention, conditional-write conflict, and review resolution. Alerting on a rising empty-candidate rate is generally more diagnostic than alerting on a generic exception total.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare the paths by consequence, evidence, and operating cost
&lt;/h2&gt;

&lt;p&gt;The decision is not “which model is smartest?” It is how much state and review the consequence justifies. This table forces the failure boundary into the choice.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Appropriate use&lt;/th&gt;
&lt;th&gt;Evidence to retain&lt;/th&gt;
&lt;th&gt;Main limitation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deterministic rules&lt;/td&gt;
&lt;td&gt;Small, stable taxonomy with explicit language&lt;/td&gt;
&lt;td&gt;Rule revision, matched terms, precedence&lt;/td&gt;
&lt;td&gt;Paraphrase and overlapping rules become brittle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embedding nearest label&lt;/td&gt;
&lt;td&gt;Reversible suggestions and exploratory facets&lt;/td&gt;
&lt;td&gt;Vector revision, metric, neighbors, scores&lt;/td&gt;
&lt;td&gt;Proximity is not a policy decision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieve, rerank, constrained LLM&lt;/td&gt;
&lt;td&gt;Nuanced, changing topics with reviewable mistakes&lt;/td&gt;
&lt;td&gt;Candidates before and after rerank, catalog, validated decision&lt;/td&gt;
&lt;td&gt;Adds latency, retained state, and evaluation work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supervised classifier&lt;/td&gt;
&lt;td&gt;Stable classes with representative labeled data&lt;/td&gt;
&lt;td&gt;Dataset revision, model revision, evaluation slices&lt;/td&gt;
&lt;td&gt;Taxonomy and distribution changes require retraining and revalidation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human adjudication&lt;/td&gt;
&lt;td&gt;Rare ambiguity or high-consequence routing&lt;/td&gt;
&lt;td&gt;Assignment, evidence shown, decision, reviewer policy&lt;/td&gt;
&lt;td&gt;Throughput and consistency depend on process capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a changing documentation corpus where topic tags help discovery, the staged retrieve-rerank-classify path is defensible because each claim can be evaluated independently and uncertain cases can abstain. &lt;strong&gt;The catch is operational ownership:&lt;/strong&gt; it is not suitable when nobody maintains the catalog, reviews sampled errors, or governs retained evidence. In that environment, deterministic rules with an explicit unmatched state are more honest.&lt;/p&gt;

&lt;p&gt;Stick with rules when vocabulary is controlled and precedence can be tested exhaustively. Prefer a supervised classifier when classes are stable and representative labeled examples actually exist. Send documents directly to human adjudication when a wrong label controls access, deletion, billing, or another hard-to-reverse action and the automated path has not been validated for that consequence. No component choice removes the need to name who accepts residual error.&lt;/p&gt;

&lt;p&gt;Model operating cost as work units rather than transient prices: source bytes extracted, chunks embedded, candidate vectors examined, query-document pairs reranked, classifier input and output, review minutes, and retained evidence bytes. Digest-based caching avoids recomputing unchanged content. Revision keys prevent that cache from silently serving an artifact produced under an obsolete contract. This is dull accounting — and exactly what makes capacity reviews survive an implementation change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the critical path behind replayable contracts
&lt;/h2&gt;

&lt;p&gt;The deployed coordinator can be Node.js even though the contract example is Python. Language is secondary here; the important properties are injected stage boundaries, immutable inputs, explicit revisions, schema checks, and one conditional publication point. No external route is assumed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Candidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;evidence_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;retrieval_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;rerank_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;operation_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;source_revision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;
    &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Candidate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;
    &lt;span class="n"&gt;disposition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decide_topics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;source_revision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;catalog_revision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pipeline_revision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;allowed_topics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
    &lt;span class="n"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Candidate&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
    &lt;span class="n"&gt;rerank&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Candidate&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Candidate&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
    &lt;span class="n"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Candidate&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document text is empty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;operation_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;source_revision&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;catalog_revision&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pipeline_revision&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;retrieved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rerank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retrieved&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromkeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="n"&gt;unknown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;allowed_topics&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown topics: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;disposition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;operation_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;operation_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;source_revision&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;source_revision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;disposition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;disposition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function intentionally does not publish. Its caller persists the retrieved set before reranking, persists the validated decision under &lt;code&gt;operation_key&lt;/code&gt;, and conditionally advances the active pointer. Retry policy belongs at the boundary that knows whether an operation is idempotent and whether a prior result can be read back. RFC 9110 defines HTTP semantics for methods and retries; the domain still has to supply its own deduplication key and conditional state transition.&lt;/p&gt;

&lt;p&gt;Deployment should run a candidate revision against held-out documents and a shadow sample, compare transitions by topic and data slice, inspect abstentions, and require an accountable approval before moving the active pointer. Rollback moves that pointer to a prior validated revision. It does not delete the newer evidence, because deletion would make the deployment impossible to audit. Retention limits can remove expired artifacts later under an explicit policy that also covers derived vectors and snippets.&lt;/p&gt;

&lt;p&gt;Tests should target boundaries rather than only happy-path labels. Feed empty extracted text, duplicated workers, an unknown classifier label, a zero-candidate retrieval, reordered rerank results, and a retry after a simulated lost response. Property tests can assert that every published label belongs to its catalog and that identical stable inputs produce the same operation key. Evaluation tests, separately, measure whether the retrieved and final labels are useful; mixing data-integrity assertions with quality metrics makes both harder to interpret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record the rejected shortcut and its valid use case
&lt;/h2&gt;

&lt;p&gt;The rejected option is to embed a document, select the nearest topic name, and overwrite the canonical record immediately. Topic names are thin policy descriptions, similarity is not calibrated confidence, and overwrite destroys the evidence needed to explain later drift. Adding an LLM after that lookup without recording candidates preserves the same architectural problem behind a more complicated call graph.&lt;/p&gt;

&lt;p&gt;There is a valid narrow use case. The shortcut can generate removable, non-authoritative suggestions in an internal discovery interface when users can correct them, the original document remains canonical, and no access, retention, compliance, or financial action consumes the tag. Mark the output as a suggestion, attach its revision, and measure correction behavior. For everything with a harder consequence, keep the stages separate and preserve the decision trail.&lt;/p&gt;

&lt;p&gt;The final criterion is deliberately plain: semantic retrieval finds candidate evidence, reranking prioritizes it, and an LLM classifier applies a catalog policy. Store and test them as three claims. If the system cannot show which claim produced a wrong topic, it is not ready to publish that topic as durable state.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;RFC 9110, HTTP Semantics: &lt;a href="https://www.rfc-editor.org/rfc/rfc9110" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc9110&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;pgvector, vector similarity search for PostgreSQL: &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;https://github.com/pgvector/pgvector&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>node</category>
      <category>ai</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
