<?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: marcelotaparelli</title>
    <description>The latest articles on DEV Community by marcelotaparelli (@marcelotaparelli).</description>
    <link>https://dev.to/marcelotaparelli</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%2F1388551%2F8e0f7cab-1676-4dc0-b3cb-f7074e3ce0b7.png</url>
      <title>DEV Community: marcelotaparelli</title>
      <link>https://dev.to/marcelotaparelli</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marcelotaparelli"/>
    <language>en</language>
    <item>
      <title>DefectRisk</title>
      <dc:creator>marcelotaparelli</dc:creator>
      <pubDate>Sat, 10 Oct 2026 09:01:01 +0000</pubDate>
      <link>https://dev.to/marcelotaparelli/defectrisk-2g01</link>
      <guid>https://dev.to/marcelotaparelli/defectrisk-2g01</guid>
      <description>&lt;p&gt;&lt;strong&gt;Review ~30% of modules → capture 71.26% of known defects.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the historical once-only evaluation of the frozen raw Random Forest,&lt;br&gt;
652 of 2,177 modules were flagged and 300 of the 421 modules with recorded&lt;br&gt;
defects entered that queue. This measures prioritization; it does not show&lt;br&gt;
that reviewers found every bug or validate the later calibrated system.&lt;/p&gt;

&lt;p&gt;I built the evaluation process, compared models at equal review capacity,&lt;br&gt;
and delivered a frozen artifact with a real inference CLI. DefectRisk&lt;br&gt;
estimates risk to help a team decide where to start reviewing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;A team cannot inspect every module with equal attention. The useful question&lt;br&gt;
is: &lt;strong&gt;if we can review only part of the codebase, can ML concentrate more&lt;br&gt;
defects inside that queue?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Defective modules are the minority class. On JM1, predicting everything clean&lt;br&gt;
would yield about 80.65% accuracy while capturing no defects. Recall,&lt;br&gt;
precision, and review effort therefore guide the product decision.&lt;/p&gt;
&lt;h2&gt;
  
  
  Evaluation integrity came first
&lt;/h2&gt;

&lt;p&gt;The project uses &lt;strong&gt;JM1 / OpenML 1053, version 1&lt;/strong&gt;: 10,885 modules, 21 static&lt;br&gt;
code metrics, and approximately 19.35% defective modules.&lt;/p&gt;

&lt;p&gt;The initial audit found &lt;strong&gt;299 complete feature vectors shared between&lt;br&gt;
training and test&lt;/strong&gt; in the random split. The data included duplicates and&lt;br&gt;
conflicting-label groups: identical metrics with different defect records.&lt;br&gt;
Evaluation could partly measure recognition of repeated examples rather&lt;br&gt;
than generalization.&lt;/p&gt;

&lt;p&gt;I grouped original vectors before splitting. Labels stay outside group&lt;br&gt;
identity; identical vectors remain on the same side of every training/test,&lt;br&gt;
validation, and cross-validation boundary. I retained the rows, including&lt;br&gt;
conflicting ones, and added overlap audits before fitting. Imputation,&lt;br&gt;
transformations, and class weights learn only from fitting partitions.&lt;/p&gt;
&lt;h2&gt;
  
  
  Model development
&lt;/h2&gt;

&lt;p&gt;Development progressed through &lt;strong&gt;Logistic Regression baseline → class&lt;br&gt;
weighting → threshold and review-workload analysis → Random Forest → HGB →&lt;br&gt;
XGBoost&lt;/strong&gt;, with limited nested tuning, feature engineering, and an ensemble&lt;br&gt;
comparison.&lt;/p&gt;

&lt;p&gt;The main decision was to compare models at &lt;strong&gt;equal review capacity,&lt;br&gt;
especially around 30%&lt;/strong&gt;, instead of relying on the default 0.50 threshold.&lt;br&gt;
Five outer folds with groups estimated results; three inner folds selected&lt;br&gt;
tree configurations without consulting the outer fold's outcomes.&lt;/p&gt;

&lt;p&gt;In the training-only selection study, RF captured 953 of 1,685 defective&lt;br&gt;
modules, versus 906 for balanced Logistic Regression, at similar workload.&lt;br&gt;
HGB, XGBoost, derived features, and a small ensemble did not produce a&lt;br&gt;
material gain sufficient to replace RF. The selected weighted forest has&lt;br&gt;
200 trees, depth 8, and a minimum of three examples per leaf. These CV&lt;br&gt;
comparisons are separate from the historical result below.&lt;/p&gt;
&lt;h2&gt;
  
  
  Historical raw RF result
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Reviewing roughly one third of the highest-risk modules concentrated about&lt;br&gt;
seven out of ten modules with recorded defects inside the queue.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The model and policy were frozen before the single held-out evaluation.&lt;br&gt;
The policy selected its cutoff from scores and capacity without consulting&lt;br&gt;
test labels, keeping tied scores together.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measure&lt;/th&gt;
&lt;th&gt;Historical held-out&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Modules evaluated&lt;/td&gt;
&lt;td&gt;2,177&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modules with recorded defects&lt;/td&gt;
&lt;td&gt;421&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flagged for review&lt;/td&gt;
&lt;td&gt;652 (29.95%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Defective modules captured&lt;/td&gt;
&lt;td&gt;300 of 421&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recall&lt;/td&gt;
&lt;td&gt;71.26%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Precision&lt;/td&gt;
&lt;td&gt;46.01%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F1&lt;/td&gt;
&lt;td&gt;0.5592&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average Precision (AP)&lt;/td&gt;
&lt;td&gt;0.6553&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Defective modules missed&lt;/td&gt;
&lt;td&gt;121&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The queue also contained &lt;strong&gt;352 modules recorded as clean&lt;/strong&gt;, the false&lt;br&gt;
positives. This is human review prioritization, not automatic bug detection.&lt;br&gt;
71.26% is recall on this test, not accuracy, probability correctness, or&lt;br&gt;
expected production performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This result belongs to the frozen raw RF. It is not final validation of&lt;br&gt;
the calibrated artifact or abstention policies developed afterward.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Calibration and uncertainty
&lt;/h2&gt;

&lt;p&gt;Later, I evaluated sigmoid and isotonic calibration with nested CV and&lt;br&gt;
groups, &lt;strong&gt;using training data only&lt;/strong&gt;. For the fixed RF configuration,&lt;br&gt;
sigmoid improved Brier score from &lt;strong&gt;0.18692 to 0.13863&lt;/strong&gt;, while AP remained&lt;br&gt;
approximately stable (0.40703 → 0.40619). The score measures probability&lt;br&gt;
quality; interpretation also considers reliability curves and the support&lt;br&gt;
within each range.&lt;/p&gt;

&lt;p&gt;Calibration improved probability interpretation without creating new&lt;br&gt;
predictive information. The attempt at automatic HIGH classification with&lt;br&gt;
≥90% precision found no sufficiently supported tail. The conservative&lt;br&gt;
policy left &lt;strong&gt;98.44% UNCERTAIN&lt;/strong&gt;: 136 LOW, zero HIGH, and 8,572 UNCERTAIN.&lt;br&gt;
Even among LOW modules, seven had recorded defects.&lt;/p&gt;

&lt;p&gt;The product remains &lt;strong&gt;risk ranking + human review prioritization&lt;/strong&gt;.&lt;br&gt;
DefectRisk predicts risk and ranking, not certainty. The historical test&lt;br&gt;
was not reused for calibration or policy design; the later system still&lt;br&gt;
requires a genuinely independent external holdout.&lt;/p&gt;
&lt;h2&gt;
  
  
  Executable artifact
&lt;/h2&gt;

&lt;p&gt;The delivery includes the frozen calibrated artifact &lt;code&gt;defectrisk-rf-sigmoid-v1&lt;/code&gt;,&lt;br&gt;
an ordered feature schema, dependency versions, training fingerprints,&lt;br&gt;
and checksums. The CLI validates the CSV and loads the verified artifact,&lt;br&gt;
without training:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
defectrisk rank examples/modules.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run from the DefectRisk repository root with the recorded dependencies.&lt;br&gt;
The flow is &lt;strong&gt;CSV with static metrics → verified frozen artifact →&lt;br&gt;
calibrated probability → descending risk ranking&lt;/strong&gt;. Module identifiers stay&lt;br&gt;
outside the model; table and JSON output are available.&lt;/p&gt;

&lt;p&gt;Schema and checksum checks verify consistency and integrity, not authenticity.&lt;br&gt;
Joblib loading must use only trusted local artifacts. Reproducing saved CV&lt;br&gt;
evidence neither reopens the historical test nor independently validates&lt;br&gt;
the final artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and next investment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;JM1 is historical; features contain only static metrics.&lt;/li&gt;
&lt;li&gt;Label and snapshot provenance is imperfect; ambiguous or conflicting
labels exist.&lt;/li&gt;
&lt;li&gt;There is no guarantee of generalization across projects, languages, or versions.&lt;/li&gt;
&lt;li&gt;Calibration and policies have training/CV evidence only and require a new
independent external holdout with frozen settings.&lt;/li&gt;
&lt;li&gt;Probabilities do not establish individual certainty or causality; a low
score does not waive standard testing or review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The available evidence suggested that the current features had become the&lt;br&gt;
main bottleneck, without proving a mathematical information ceiling. The&lt;br&gt;
recommended next investment is better prediction-time data: &lt;strong&gt;code churn,&lt;br&gt;
defect history, ownership, test coverage, commit/change history, and review&lt;br&gt;
history&lt;/strong&gt;. These signals were not implemented; they would require clear&lt;br&gt;
snapshots and label horizons.&lt;/p&gt;

&lt;p&gt;The article &lt;a href="https://marcelotaparelli.com.br/en/articles/better-models-are-not-enough/" rel="noopener noreferrer"&gt;When Better Models Are Not Enough&lt;/a&gt;&lt;br&gt;
explains how I decided to stop model exploration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/marcelotaparelli/defectrisk-ml" rel="noopener noreferrer"&gt;
DefectRisk on GitHub: code, artifact and instructions
&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/marcelotaparelli/defectrisk-ml/blob/8ccdfc301a1cb22c89ae2ffbc1a29e2c054325fa/docs/model-card.md" rel="noopener noreferrer"&gt;
Model card and intended-use limits
&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/marcelotaparelli/defectrisk-ml/blob/8ccdfc301a1cb22c89ae2ffbc1a29e2c054325fa/docs/final-evaluation.md" rel="noopener noreferrer"&gt;
Historical once-only raw RF evaluation
&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/marcelotaparelli/defectrisk-ml/blob/8ccdfc301a1cb22c89ae2ffbc1a29e2c054325fa/docs/calibration-and-uncertainty.md" rel="noopener noreferrer"&gt;
Later training-only calibration and uncertainty study
&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>OpsPilot AI: Building RAG and Agents Without Giving the LLM Authority</title>
      <dc:creator>marcelotaparelli</dc:creator>
      <pubDate>Wed, 07 Oct 2026 20:09:27 +0000</pubDate>
      <link>https://dev.to/marcelotaparelli/opspilot-ai-building-rag-and-agents-without-giving-the-llm-authority-57jc</link>
      <guid>https://dev.to/marcelotaparelli/opspilot-ai-building-rag-and-agents-without-giving-the-llm-authority-57jc</guid>
      <description>&lt;p&gt;An operator finds context in runbooks and documentation, interprets what is happening, and turns some of that information into tracked work. Finding an instruction and opening an issue seem closely related. The second task, however, crosses a boundary: it produces an effect in another system.&lt;/p&gt;

&lt;p&gt;That was my starting point for &lt;a href="https://marcelotaparelli.com.br/en/projects/opspilot-ai/" rel="noopener noreferrer"&gt;OpsPilot AI&lt;/a&gt;. I wanted to search tenant-owned operational knowledge, answer with references, and prepare a GitLab issue for another person to approve. The interesting question emerged when I stopped treating all of this as “the agent's response.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did the AI find enough evidence? Should it have authority to execute an external action?&lt;/strong&gt; Those are different questions. A proposal can satisfy the schema, have a convincing title, and still fall outside the user's permissions.&lt;/p&gt;

&lt;p&gt;I chose to keep probabilistic interpretation inside deterministic boundaries. &lt;strong&gt;A correctly generated action is still a proposal.&lt;/strong&gt; The system authenticates, authorizes, records approval, and controls execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the system before adding the model
&lt;/h2&gt;

&lt;p&gt;I modeled the authenticated principal, tenant knowledge, proposal, approval, and execution separately. Combining them into one “agent state” object would make it easy to confuse generated information with granted authority.&lt;/p&gt;

&lt;p&gt;The principal carries tenant, subject, and roles derived from configured credentials. The proposal holds suggested work. Approval records who accepted which action. Execution records attempts and external outcomes. PostgreSQL persists this lifecycle; it does not depend solely on the process remaining alive.&lt;/p&gt;

&lt;p&gt;I separated domain contracts from the database, model, and GitLab adapters. That lets me exercise a planner that proposes forbidden actions without paying for a model call to discover whether policy enforcement works.&lt;/p&gt;

&lt;p&gt;The domain also constrains the product: each run can prepare at most one issue proposal. I did not give the agent a shell, arbitrary SQL, or permission to call any URL. Restricting the action space reduces what I need to authorize, observe, and recover. Adding tools would require revisiting the authority model, rather than just adding names to a prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieval must respect the access boundary
&lt;/h2&gt;

&lt;p&gt;Before generating an answer, I needed to decide which documents could enter its context. Tenant identity is not an argument the model chooses. It comes from the authenticated principal and follows the transaction into the database.&lt;/p&gt;

&lt;p&gt;Ingestion normalizes text, splits it into overlapping chunks, and stores text and embeddings atomically after external calls complete. Retrieval combines exact cosine vector search in pgvector with PostgreSQL full-text search. Reciprocal rank fusion combines ranking positions instead of assuming that scores from different retrieval methods are directly comparable. Embedding-space identifiers prevent comparisons between incompatible vectors.&lt;/p&gt;

&lt;p&gt;Tenant filters apply before ranking and limits. Searching globally and filtering only the top-k would be a different architecture: other tenants' documents could consume the result budget, as well as expanding the opportunity for leakage.&lt;/p&gt;

&lt;p&gt;I also configured transaction-local tenant context and forced row-level security. Tests against real PostgreSQL exercise missing predicates and connection-pool reuse. The limit matters: the runtime role can set tenant context, so RLS does not protect against arbitrary SQL executed as that role. It also does not provide document ACLs within a tenant.&lt;/p&gt;

&lt;h2&gt;
  
  
  RAG does not end when chunks are found
&lt;/h2&gt;

&lt;p&gt;I separate the path into boundaries that can fail independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query → retriever → ranked evidence
                         ↓
                     generation
                         ↓
                 citation validation
                         ↓
                 answer or abstention
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The adapter requests structured output. The application checks it again: fabricated or duplicate IDs are rejected, citations must belong to authorized retrieved chunks, and metadata comes from the database. Uncited output becomes a fixed abstention. When retrieval returns nothing, there is no reason to call generation.&lt;/p&gt;

&lt;p&gt;This controls ID provenance, not the truth of every statement. A chunk can be authorized while an answer misinterprets its content. &lt;strong&gt;Citation membership does not establish entailment&lt;/strong&gt;: citing an existing document does not prove that the conclusion follows from it.&lt;/p&gt;

&lt;p&gt;That distinction changes diagnosis. Retrieval may miss the right document; generation may fail despite receiving the right context; or a formally valid citation may not support the claim. Measuring only the final answer would hide which part needs improvement.&lt;/p&gt;

&lt;p&gt;I do not extend this control into a universal guarantee about issues either. An authorized proposal does not become semantically correct because it exists in the database. Human review still needs to assess its content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure retrieval without mistaking ranking for an answer
&lt;/h2&gt;

&lt;p&gt;The first retrieval dataset was too easy to distinguish strategies. Retrieval-v2 introduced synthetic documents and questions covering paraphrases, identifiers, ambiguity, multiple relevant documents, and hard negatives. I separated &lt;strong&gt;24 development queries from 36 held-out queries&lt;/strong&gt;, with labels defined before running the retrievers.&lt;/p&gt;

&lt;p&gt;MRR@5 measures how early the first relevant document appears. A first-place result receives 1 for that query; second place receives 1/2; no relevant document within the five considered positions receives 0. The mean summarizes that behavior. The protocol deduplicates documents from retrieved chunks; several chunks from one document can still consume part of the retrieval budget.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Held-out strategy&lt;/th&gt;
&lt;th&gt;MRR@5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lexical&lt;/td&gt;
&lt;td&gt;0.7532&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector with fake embedder&lt;/td&gt;
&lt;td&gt;0.4375&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid with fake embedder&lt;/td&gt;
&lt;td&gt;0.6306&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Lexical retrieval outperformed hybrid retrieval in this experiment. The vector branch used deterministic word hashing, &lt;strong&gt;not a semantic model&lt;/strong&gt;. RRF does not automatically repair a weak ranking: documents appearing in both branches can outrank a relevant result found only by the lexical branch.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/marcelotaparelli/opspilot-ai/blob/cacf611c1e7e31effe04895ec383673c76568876/docs/evaluation/retrieval-v2.md" rel="noopener noreferrer"&gt;retrieval-v2 report&lt;/a&gt; attributes the result to commit &lt;code&gt;7ba3378&lt;/code&gt;. The held-out set was run once and is now consumed; CI uses development queries. Subsequent readiness changes altered the fingerprint, and the guard refuses to rerun the held-out on current code.&lt;/p&gt;

&lt;p&gt;This does not show that hybrid search is generally worse. It does not measure real embeddings, groundedness, or answer quality. Evaluating another configuration would require a new frozen protocol and a fresh held-out set. That is the same concern about measurement scope I discussed in &lt;a href="https://marcelotaparelli.com.br/en/articles/evals-stop-guessing-start-measuring/" rel="noopener noreferrer"&gt;evals: stop guessing, start measuring&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The planner proposes; application code decides what is allowed
&lt;/h2&gt;

&lt;p&gt;LangGraph orchestrates the workflow. It does not define the authority model.&lt;/p&gt;

&lt;p&gt;The planner can select &lt;code&gt;search_knowledge&lt;/code&gt;, &lt;code&gt;prepare_gitlab_issue&lt;/code&gt;, or &lt;code&gt;final_answer&lt;/code&gt;. &lt;code&gt;create_gitlab_issue&lt;/code&gt; is not a model tool. The graph has no path from planning nodes to execution; entering execution depends on persisted state after approval.&lt;/p&gt;

&lt;p&gt;I bounded steps, deadlines, and invalid-output attempts. These limits contain loops and protocol failures, but do not make a decision useful. A planner can finish within its budget and still propose work nobody needs.&lt;/p&gt;

&lt;p&gt;During preparation, policy resolves the project alias within the tenant's configuration, checks labels, and maps permitted assignees to configured IDs. The model cannot supply an arbitrary project ID or choose its own permissions.&lt;/p&gt;

&lt;p&gt;Imagine a client submitting this as supposed proof of authority:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"allowed_project"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"secret-admin"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an antipattern, not an OpsPilot contract. Valid JSON or a client assertion that something is “allowed” does not grant access. Authority must come from authenticated identity and application policy. The project's contracts reject extra fields; planner schemas contain no tenant, approval, or authorization flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approve the exact action, not just the intention
&lt;/h2&gt;

&lt;p&gt;“A human approved it” is incomplete. What was approved? Is that still the object about to be executed? Has policy changed in the meantime?&lt;/p&gt;

&lt;p&gt;I persist the canonical action with tenant, requester, resolved project, title, description, labels, and assignees before requesting approval. A SHA-256 hash of canonical JSON binds the decision to that content. Normalization and ordering prevent equivalent representations of labels or assignees from producing different identities. Execution uses the stored action; it does not ask the model to generate it again.&lt;/p&gt;

&lt;p&gt;The approver must hold the appropriate role, belong to the same tenant, and be a different subject from the requester. The transaction checks the supplied hash and recomputes the stored action's hash. The runtime role has no UPDATE permission on proposals or approvals.&lt;/p&gt;

&lt;p&gt;Before producing the external effect, execution checks approval, hash, and current policy again. If the project was removed from the allowlist, the action fails closed. If the action changed outside the normal workflow, the approval no longer applies. A changed action requires a new run and approval.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;probabilistic interpretation
            ↓
structured proposal
            ↓
deterministic policy
            ↓
human approval (action hash)
            ↓
approved immutable action
            ↓
revalidation → execution
            ↓
reconciliation / audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hash protects the link between content and decision. It does not evaluate whether the issue makes sense and does not replace authorization. Human review adds operational work. I accepted that cost to control a consequential external action.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitLab's response was lost. Can I send the POST again?
&lt;/h2&gt;

&lt;p&gt;This failure mode connected the agent most clearly to familiar distributed-systems problems: GitLab receives the create request, stores the issue, and loses the response. The client observes a timeout. The issue may already exist.&lt;/p&gt;

&lt;p&gt;Immediately repeating the POST can duplicate work. I distinguish failure before sending, terminal rejection, and an ambiguous outcome. A timeout after possible transmission, a 5xx, or a malformed 2xx response can conceal a completed write. None necessarily means “nothing happened.”&lt;/p&gt;

&lt;p&gt;Each approved action gets a stable key derived from tenant, run, and hash. The adapter includes a marker in the issue description. When an outcome is ambiguous, an explicit &lt;code&gt;/resume&lt;/code&gt; request searches for that marker before considering another send.&lt;/p&gt;

&lt;p&gt;If the issue is found, the system records success without creating another. If lookup fails, ambiguity remains. If nothing is found, a grace period must elapse before a resend is allowed, within the attempt budget. There is no automatic recovery worker.&lt;/p&gt;

&lt;p&gt;PostgreSQL uses a lock, owner, and lease to control execution claims and prevent a late owner from overwriting a newer owner's records. A lease cannot cancel an HTTP request already in flight. Delayed search visibility or removal of the marker can also lead to duplicates. &lt;strong&gt;This is not an exactly-once guarantee.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tests with real PostgreSQL and fake GitLab exercise lost responses, database failure after the external effect, and process death after creation. They establish recovery in those controlled scenarios. The real GitLab smoke validated creation, lookup, and resume of a successful run; it did not measure real lost-response recovery or indexing delays.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security belongs outside the prompt
&lt;/h2&gt;

&lt;p&gt;The most useful adversarial test assumes that the planner has already been compromised. Documents tell it to skip approval and use a forbidden project. The scripted planner obeys, requests direct creation, tries the forbidden project, and emits an approval decision.&lt;/p&gt;

&lt;p&gt;In the exercised cases, application code refuses forbidden tools and projects. The only permitted proposal still waits for a human, with zero requests to GitLab. This tests the authority boundary even when interpretation fails.&lt;/p&gt;

&lt;p&gt;Prompts that label evidence “untrusted data” and strict schemas help organize the interface. They do not replace authentication or authorization. They also do not eliminate answer poisoning within a tenant: the model can receive malicious information it is authorized to read.&lt;/p&gt;

&lt;p&gt;The controls protect specific boundaries: tenant isolation, permitted tools, human decisions, runtime validation, and an audit trail. Adversarial regression verifies defined behaviors; it does not establish resistance to every prompt injection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability follows the boundaries
&lt;/h2&gt;

&lt;p&gt;A log saying “agent failed” would be insufficient. I need to distinguish missing evidence, model timeout, policy denial, time spent awaiting approval, and an ambiguous external effect.&lt;/p&gt;

&lt;p&gt;I instrumented boundaries with OpenTelemetry: HTTP requests, retrieval and its branches, AI calls, planning, authorization, approval, execution, GitLab, and reconciliation. Request IDs, run IDs, and trace IDs relate logs, spans, and audit events.&lt;/p&gt;

&lt;p&gt;Approval and resume are new requests. I do not claim that the entire human wait lives inside one continuous trace. The run and trace IDs recorded in the audit connect those entries. This lets me reconstruct the lifecycle without recording full prompts or documents.&lt;/p&gt;

&lt;p&gt;Allowlists restrict attributes, and request, tenant, or document IDs do not become metric labels. That reduces content exposure and uncontrolled cardinality. Configured and served models are distinct facts; missing usage or cost remains unknown, rather than zero.&lt;/p&gt;

&lt;p&gt;Telemetry export can lose signals and fails open so that it does not govern product availability. Authorization and approval remain fail-closed. Persisted state and the audit trail are authoritative; a successful span does not decide whether execution completed. The &lt;a href="https://github.com/marcelotaparelli/opspilot-ai/blob/cacf611c1e7e31effe04895ec383673c76568876/docs/observability.md" rel="noopener noreferrer"&gt;observability record&lt;/a&gt; documents these signals and local collector-failure tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Different evals answer different questions
&lt;/h2&gt;

&lt;p&gt;The agent evaluation passed &lt;strong&gt;16/16 cases&lt;/strong&gt; with scripted/offline planners, real PostgreSQL, and fake GitLab. It measures controls under defined sequences, including adversarial behavior. It does not measure a real LLM's ability to select tools.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval-v2:&lt;/strong&gt; exercises synthetic held-out ranking with fake embeddings. It does not establish real semantic retrieval or correct answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent eval:&lt;/strong&gt; exercises offline policy, approval, states, and recovery. It does not establish real LLM decision quality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI smoke:&lt;/strong&gt; exercises the real API, schemas, embeddings, and small database-backed RAG. It does not establish quality, entailment, or measured cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitLab smoke:&lt;/strong&gt; exercises sandbox approval, creation, confirmation, and resume. It does not establish recovery from real faults or exactly-once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Generation quality asks a different question: is the answer correct, and do its claims follow from the evidence? The project does not yet evaluate that behavior with a real model. Ranking, workflow, and integration checks can pass without answering that question.&lt;/p&gt;

&lt;p&gt;The OpenAI record from &lt;strong&gt;October 4, 2026&lt;/strong&gt; confirms real 256-dimensional embeddings, answer and planner schemas, citations belonging to context, usage, served-model records, and a bounded timeout. The GitLab record from &lt;strong&gt;October 5&lt;/strong&gt; confirms no request before approval, creation and GET, a second approval returning 409, resume with one matching issue and one create POST, and confirmed closure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;These were separate runs&lt;/strong&gt;, and the GitLab smoke uses an offline planner. There was no single live OpenAI + GitLab E2E. OpenAI pricing was unconfigured: passing a conditional cost check does not establish measured cost.&lt;/p&gt;

&lt;p&gt;Release &lt;code&gt;v0.1.0&lt;/code&gt; is published on GitHub. Its local record documents &lt;strong&gt;241 unit and 72 integration tests&lt;/strong&gt;, plus clean-room reproduction. The current-state document records the owner's subsequent confirmation that hosted CI passed. Those counts describe that validation, not performance under production traffic. The &lt;a href="https://github.com/marcelotaparelli/opspilot-ai/blob/cacf611c1e7e31effe04895ec383673c76568876/docs/evidence/release/live-provider-status.md" rel="noopener noreferrer"&gt;live records&lt;/a&gt; retain each run's scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  What remains unproven
&lt;/h2&gt;

&lt;p&gt;The held-out is small, synthetic, English-only, and authored by one person. The fake embedder does not test semantic understanding. There is no real-model quality evaluation, measured cost, or validation of the combined system against real operational traffic.&lt;/p&gt;

&lt;p&gt;Static tokens provide neither SSO nor identity lifecycle management. There are no within-tenant ACLs. Exact vector search scales linearly; a small corpus does not establish scale. Recovery requires explicit resume, and search-based reconciliation retains its visibility and in-flight-request limits.&lt;/p&gt;

&lt;p&gt;Terraform describes an AWS blueprint validated with offline plans. &lt;strong&gt;No AWS deployment was performed&lt;/strong&gt;; cloud runtime, restore, and the AWS telemetry path remain unvalidated. Passing configuration validation does not establish operation in that environment.&lt;/p&gt;

&lt;p&gt;The release-image scan retained &lt;strong&gt;44 HIGH findings without reported fixes&lt;/strong&gt;, with zero fixable HIGH/CRITICAL findings. The gate blocks fixable findings; passing it does not mean zero vulnerabilities. Four classified IaC risks also remain documented. These are historical results, not a new scan performed for this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from this project
&lt;/h2&gt;

&lt;p&gt;My main lesson was to make the contract between interpretation and effect explicit. The model can retrieve context, interpret a request, and propose work. The software system must determine authority, verify approval and external outcomes, and retain recoverable states when those outcomes are uncertain.&lt;/p&gt;

&lt;p&gt;The next evaluations need to address the remaining gaps: real embeddings under a new protocol, answer quality, and live recovery. A passing smoke test does not answer those questions. Separating these responsibilities taught me more than simply getting an agent to call a tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence and code
&lt;/h2&gt;

&lt;p&gt;I inspected the &lt;a href="https://github.com/marcelotaparelli/opspilot-ai" rel="noopener noreferrer"&gt;public OpsPilot AI repository&lt;/a&gt; at snapshot &lt;code&gt;cacf611&lt;/code&gt;. Links pin that state; the retrieval-v2 benchmark belongs to the historical freeze identified in its report.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/marcelotaparelli/opspilot-ai/blob/cacf611c1e7e31effe04895ec383673c76568876/docs/CURRENT-STATE.md" rel="noopener noreferrer"&gt;
Current state and validation provenance
&lt;/a&gt;
.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/marcelotaparelli/opspilot-ai/blob/cacf611c1e7e31effe04895ec383673c76568876/docs/architecture/agent-workflow.md" rel="noopener noreferrer"&gt;
Workflow, approval, recovery, and adversarial tests
&lt;/a&gt;
.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/marcelotaparelli/opspilot-ai/blob/cacf611c1e7e31effe04895ec383673c76568876/src/opspilot/agent/policy.py" rel="noopener noreferrer"&gt;
Deterministic policy in code
&lt;/a&gt;
.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/marcelotaparelli/opspilot-ai/blob/cacf611c1e7e31effe04895ec383673c76568876/docs/adr/002-isolation-and-untrusted-evidence.md" rel="noopener noreferrer"&gt;
Isolation, untrusted evidence, and RLS limits
&lt;/a&gt;
.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/marcelotaparelli/opspilot-ai/blob/cacf611c1e7e31effe04895ec383673c76568876/docs/evidence/release/vulnerability-summary.md" rel="noopener noreferrer"&gt;
Image scan and gate scope
&lt;/a&gt;
.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://marcelotaparelli.com.br/en/projects/opspilot-ai/" rel="noopener noreferrer"&gt;OpsPilot AI case&lt;/a&gt; for the project overview.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>rag</category>
      <category>programming</category>
    </item>
    <item>
      <title>Laya vs Jev in Practice: Zero-Shot, Fine-Tuning, and the Real Cost of an Open-Source Decision Model</title>
      <dc:creator>marcelotaparelli</dc:creator>
      <pubDate>Fri, 02 Oct 2026 23:43:01 +0000</pubDate>
      <link>https://dev.to/marcelotaparelli/laya-vs-jev-in-practice-zero-shot-fine-tuning-and-the-real-cost-of-an-open-source-decision-model-40j9</link>
      <guid>https://dev.to/marcelotaparelli/laya-vs-jev-in-practice-zero-shot-fine-tuning-and-the-real-cost-of-an-open-source-decision-model-40j9</guid>
      <description>&lt;p&gt;TypeSafe announced Jev on &lt;strong&gt;September 15, 2026&lt;/strong&gt;, introducing its first “System One Model.” Laya's official Hugging Face history dates the commit marked “Initial release” to &lt;strong&gt;September 18, 2026&lt;/strong&gt;. &lt;strong&gt;Those public records are three days apart&lt;/strong&gt; — an interval between an announcement and a release record, not a measure of development time. Sources: &lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" rel="noopener noreferrer"&gt;TypeSafe's announcement&lt;/a&gt; and &lt;a href="https://huggingface.co/convaiinnovations/laya/commit/00c37c405e3c3ad73ee070227614c89cda06b99e" rel="noopener noreferrer"&gt;Laya's initial release record&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By the time I completed the &lt;a href="https://marcelotaparelli.com.br/en/articles/jev-1-13-decision-model-benchmark/" rel="noopener noreferrer"&gt;Jev 1.13 evaluation&lt;/a&gt;, Laya was already an open-weight alternative worth investigating. Learning a tool was just the first step. The more durable work was evaluating a new approach with a sound method and measuring whether it improves the system.&lt;/p&gt;

&lt;p&gt;On the same &lt;a href="https://marcelotaparelli.com.br/en/projects/ops-triage-ai/" rel="noopener noreferrer"&gt;Ops Triage AI&lt;/a&gt; held-out set, Laya went from &lt;strong&gt;8/70 correct tuples zero-shot to 60/70 after domain adaptation&lt;/strong&gt;. Jev had reached 66/70 without domain-specific training. The interesting question is what changes when we can modify a model's weights, and what we take responsibility for in exchange.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, what is a decision model?
&lt;/h2&gt;

&lt;p&gt;Jev is a non-generative AI model specialized in structured decisions. It takes state or context expressed in natural language and returns typed decisions with probability distributions, rather than generating an open-ended token sequence like a conventional generative LLM.&lt;/p&gt;

&lt;p&gt;It is still AI. TypeSafe calls the category &lt;strong&gt;System One Models&lt;/strong&gt;; that is the company's terminology, not a universally established academic taxonomy. I also use the more neutral terms “probabilistic decision model” and “structured decision model.”&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Generative LLM
text → token-by-token generation → text

Decision model
state + set of choices → distribution → typed decision
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This changes both the interface and the task. In ticket triage, category, priority, and risk can be closed choices. The software receives values it already understands, without asking for a paragraph and extracting an enum afterward.&lt;/p&gt;

&lt;p&gt;LLMs can produce structured outputs too. The distinction goes beyond model size or JSON formatting: a decision model is designed to score a decision space instead of generating free-form text. A bounded answer space reduces parsing problems and out-of-schema values. &lt;strong&gt;A valid choice can still be semantically wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Laya fits
&lt;/h2&gt;

&lt;p&gt;Laya addresses a similar role with code and weights released under &lt;strong&gt;Apache 2.0&lt;/strong&gt;, non-autoregressive inference, self-hosting, and fine-tuning. The general English checkpoint combines ModernBERT-large with a decision head for &lt;strong&gt;421 million parameters&lt;/strong&gt;, according to the &lt;a href="https://huggingface.co/convaiinnovations/laya/blob/55cf4c4ebb4ebe31b2550e8bdf3bd21b99753851/README.md" rel="noopener noreferrer"&gt;model card at the revision we used&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We pinned &lt;code&gt;convaiinnovations/laya&lt;/code&gt; at revision &lt;code&gt;55cf4c4ebb4ebe31b2550e8bdf3bd21b99753851&lt;/code&gt;, using &lt;strong&gt;&lt;code&gt;laya==0.3.23&lt;/code&gt;&lt;/strong&gt;. The &lt;a href="https://github.com/NandhaKishorM/laya/releases/tag/v0.3.23" rel="noopener noreferrer"&gt;runtime release&lt;/a&gt; was published on October 1, 2026. Library version and weight revision are separate identities, and both were frozen.&lt;/p&gt;

&lt;p&gt;This base checkpoint is distinct from &lt;code&gt;laya-typed-decisions&lt;/code&gt;, which has already been fine-tuned on other workflows. We did not use the multilingual checkpoint either. Here, “zero-shot” means no training specific to &lt;strong&gt;our&lt;/strong&gt; domain, not a model with no prior training.&lt;/p&gt;

&lt;h2&gt;
  
  
  A decision model inside a real architecture
&lt;/h2&gt;

&lt;p&gt;A voice-driven website-building demonstration I watched made the role easier to understand. It described a library of more than 1,000 assets/components, with Jev selecting actions and assets. The conceptual flow was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;voice → real-time transcription → context adaptation/correction
      → decision model → asset/action selection → UI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an account of the observed demonstration, without a verifiable URL in the project context or benchmark value.&lt;/p&gt;

&lt;p&gt;Jev does not have to generate the website. A speech model can transcribe, another layer can normalize context, and a decision model can choose among permitted actions. Speech models, LLMs, and decision models can serve different roles in the same architecture. Code remains responsible for executing and validating the actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark we already had
&lt;/h2&gt;

&lt;p&gt;Our most useful asset was a &lt;strong&gt;frozen held-out set of 70 synthetic English tickets&lt;/strong&gt;, previously used for the deterministic baseline, Ollama, and Jev 1.13. We kept the tickets, labels, and metric definitions. Exact-tuple accuracy counts a ticket as correct only when &lt;strong&gt;category, priority, and risk&lt;/strong&gt; all match.&lt;/p&gt;

&lt;p&gt;We split the investigation into two questions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Experiment&lt;/th&gt;
&lt;th&gt;Comparison&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A — out of the box&lt;/td&gt;
&lt;td&gt;Historical Jev 1.13 × base Laya zero-shot&lt;/td&gt;
&lt;td&gt;What happens without domain-specific training?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B — domain adaptation&lt;/td&gt;
&lt;td&gt;The same, unchanged Jev result × adapted Laya&lt;/td&gt;
&lt;td&gt;How much can specialization close the observed gap?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We did not rerun Jev. We retained its frozen historical result through OpenRouter: &lt;code&gt;typesafe/jev-1.13&lt;/code&gt;, resolved to &lt;code&gt;typesafe/jev-1.13-20260917&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Experiment B is &lt;strong&gt;asymmetric&lt;/strong&gt;. Jev received no domain-specific training examples; Laya received 1,120 TRAIN tickets. The taxonomy is shared, but prompts and inference paths differ. Laya's questions were adapted to its token budget before the freeze and held constant between A and B. We are evaluating generalization without adaptation and the capacity to specialize, with different exposure to the domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment A: base Laya zero-shot
&lt;/h2&gt;

&lt;p&gt;For this general checkpoint, zero-shot, on this taxonomy and frozen held-out set:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Base Laya zero-shot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Category accuracy&lt;/td&gt;
&lt;td&gt;84.29% — 59/70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Priority accuracy&lt;/td&gt;
&lt;td&gt;31.43% — 22/70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Risk accuracy&lt;/td&gt;
&lt;td&gt;62.86% — 44/70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exact tuple&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11.43% — 8/70&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The clearest pattern was &lt;strong&gt;40/40 expected LOW priorities predicted as MEDIUM&lt;/strong&gt;. Before attributing that to the model, the audit examined options, order, indices, mapping, serialization, and normalization. It found no integration bug that invalidated the result: the archived probabilities placed MEDIUM first, matching the published choice.&lt;/p&gt;

&lt;p&gt;The audit used &lt;strong&gt;parsed&lt;/strong&gt; outputs with copied choices and probabilities; untouched native responses were not retained byte for byte. That evidence supports checking the mapping: MEDIUM was already the model's choice, with no LOW-to-MEDIUM conversion in the adapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this does not contradict Laya's published benchmark
&lt;/h2&gt;

&lt;p&gt;The material that prompted the investigation showed much stronger results. The &lt;a href="https://huggingface.co/convaiinnovations/laya-typed-decisions" rel="noopener noreferrer"&gt;laya-typed-decisions model card&lt;/a&gt; explains the key difference: that checkpoint was fine-tuned on the corresponding benchmark's workflows. The card distinguishes the specialized checkpoint from the base model and notes that its Jev figures were published by third parties, not measured in the same run.&lt;/p&gt;

&lt;p&gt;The published metrics belong to a different dataset, protocol, and checkpoint, and measure individual decisions rather than our per-ticket tuple. That distinction raised a second question: was weak zero-shot quality structural, or was this model missing adaptation to our domain?&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment B: keep learning separate from evaluation
&lt;/h2&gt;

&lt;p&gt;We created &lt;strong&gt;1,120 TRAIN tickets&lt;/strong&gt; and &lt;strong&gt;280 VALIDATION tickets&lt;/strong&gt;, also synthetic, from the operational taxonomy. A deterministic generator used authored scenarios, without an LLM and without reading the held-out file or its errors to construct examples.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TRAIN — 1,120 tickets → changes weights
VALIDATION — 280 tickets → selects checkpoint
HELD-OUT — the same 70 tickets → final evaluation after the freeze
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The held-out set stayed outside dataset generation, training, validation, checkpoint selection, thresholds, calibration, and hyperparameter tuning. This was a follow-up on a benchmark whose aggregate result we already knew; its file and errors were isolated from construction of the new data.&lt;/p&gt;

&lt;p&gt;VALIDATION shares scenario families with TRAIN, although the wording is distinct. It supports checkpoint selection, but is not an independent real-world population.&lt;/p&gt;

&lt;p&gt;Fine-tuning started from the &lt;strong&gt;same base checkpoint&lt;/strong&gt; as A. Our deterministic labels became one-hot targets, unlike the soft distributions from a probabilistic teacher in Laya's official example. We did not fit temperatures afterward: candidate checkpoints used temperature 1.0. This is not an exact replication of the external benchmark recipe, and the difference may affect confidence.&lt;/p&gt;

&lt;p&gt;The selection rule favored the highest VALIDATION exact-tuple accuracy, with predeclared tie-breakers based on macro-F1, mean Brier, and earliest epoch. Epoch 4 was selected. &lt;strong&gt;Freeze 2&lt;/strong&gt;, commit &lt;code&gt;00a9a64&lt;/code&gt;, fixed data, checkpoint, code, inference, and thresholds before the single official adapted evaluation: 70/70 valid responses and zero failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  The infrastructure needed to adapt the model
&lt;/h2&gt;

&lt;p&gt;The local host offered four vCPUs, roughly 4 GB of RAM, and no GPU. Full fine-tuning there was not a reasonable option. We used a &lt;strong&gt;temporary NVIDIA RTX A5000 with 24 GB VRAM&lt;/strong&gt;, Python 3.11, and PyTorch 2.5.1 with CUDA 12.1.&lt;/p&gt;

&lt;p&gt;Training ran for four epochs with mixed precision — fp16 autocast and fp32 master weights — and gradient accumulation to manage memory use.&lt;/p&gt;

&lt;p&gt;This supplied concrete evidence of my direction in &lt;strong&gt;ML Systems / AI Infrastructure&lt;/strong&gt;. The task included checking whether training accounting matched the updates that actually happened.&lt;/p&gt;

&lt;p&gt;In the first run, &lt;code&gt;GradScaler&lt;/code&gt; skipped an optimizer update while the scheduler advanced anyway. We detected the anomaly, changed the scheduler to advance only after applied updates, preserved the initial run's artifacts, and repeated training &lt;strong&gt;before opening the adapted held-out evaluation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The corrected official run recorded &lt;strong&gt;420 update attempts, 413 applied optimizer updates, seven GradScaler skips, and 413 scheduler steps&lt;/strong&gt;. It became the candidate on methodological validity, not because its accuracy was more convenient. Selection, freeze, and final evaluation followed that correction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The jump: 11.43% → 85.71%
&lt;/h2&gt;

&lt;p&gt;On the same held-out set:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Laya zero-shot&lt;/th&gt;
&lt;th&gt;Adapted Laya&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Category accuracy&lt;/td&gt;
&lt;td&gt;84.29% — 59/70&lt;/td&gt;
&lt;td&gt;97.14% — 68/70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Priority accuracy&lt;/td&gt;
&lt;td&gt;31.43% — 22/70&lt;/td&gt;
&lt;td&gt;94.29% — 66/70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Risk accuracy&lt;/td&gt;
&lt;td&gt;62.86% — 44/70&lt;/td&gt;
&lt;td&gt;92.86% — 65/70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exact tuple&lt;/td&gt;
&lt;td&gt;11.43% — 8/70&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;85.71% — 60/70&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;LOW → MEDIUM errors fell from &lt;strong&gt;40/40 to 0/40&lt;/strong&gt;. Adaptation clearly changed behavior in this domain, as documented in the &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai/blob/e8c75f0ff75aafcd24403f860e1ad7d4450392f5/docs/evaluation/laya-domain-adaptation-final-report.md" rel="noopener noreferrer"&gt;public final report&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Fine-tuning did not fix everything. Two categories, four priorities, and five risk labels remained wrong. Errors overlapped across fields, leaving &lt;strong&gt;ten incorrect tuples&lt;/strong&gt;. HIGH/CRITICAL priority recall decreased from 13/14 zero-shot to 12/14 after adaptation. Improving aggregate accuracy does not guarantee improvement in every severity error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jev and adapted Laya: read the asymmetry carefully
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Jev 1.13 — none of our TRAIN tickets&lt;/th&gt;
&lt;th&gt;Adapted Laya — 1,120 TRAIN tickets&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Category accuracy&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;97.14%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Priority accuracy&lt;/td&gt;
&lt;td&gt;98.57%&lt;/td&gt;
&lt;td&gt;94.29%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Risk accuracy&lt;/td&gt;
&lt;td&gt;95.71%&lt;/td&gt;
&lt;td&gt;92.86%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HIGH/CRITICAL priority recall&lt;/td&gt;
&lt;td&gt;100% — 14/14&lt;/td&gt;
&lt;td&gt;85.71% — 12/14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HIGH risk recall&lt;/td&gt;
&lt;td&gt;85.71% — 6/7&lt;/td&gt;
&lt;td&gt;100% — 7/7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exact tuple&lt;/td&gt;
&lt;td&gt;94.29% — 66/70&lt;/td&gt;
&lt;td&gt;85.71% — 60/70&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Jev showed strong out-of-the-box generalization on this benchmark. Laya showed how much control open source offers when we can adapt a model to the domain.&lt;/strong&gt; Jev's 94.29% came without our TRAIN tickets; Laya went from 11.43% zero-shot to 85.71% with adaptation. Adapted Laya had higher HIGH-risk recall, but the denominator is just seven examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confidence remained a problem
&lt;/h2&gt;

&lt;p&gt;Adapted Laya's mean &lt;code&gt;answer_confidence&lt;/code&gt; — its rounded top-option probability — was nearly saturated:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Mean answer_confidence&lt;/th&gt;
&lt;th&gt;Multiclass Brier&lt;/th&gt;
&lt;th&gt;Top-label ECE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Category&lt;/td&gt;
&lt;td&gt;0.99986&lt;/td&gt;
&lt;td&gt;0.05715&lt;/td&gt;
&lt;td&gt;0.02844&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Priority&lt;/td&gt;
&lt;td&gt;0.99997&lt;/td&gt;
&lt;td&gt;0.11429&lt;/td&gt;
&lt;td&gt;0.05711&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Risk&lt;/td&gt;
&lt;td&gt;0.99884&lt;/td&gt;
&lt;td&gt;0.13841&lt;/td&gt;
&lt;td&gt;0.07027&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ten of the 70 tuples were still wrong. Brier and ECE describe this small sample; they do not certify calibration on future traffic.&lt;/p&gt;

&lt;p&gt;The exploratory gate required all three &lt;code&gt;answer_confidence&lt;/code&gt; values to meet the threshold. From 0 through &lt;strong&gt;0.90&lt;/strong&gt;, it retained 70/70 tickets at 85.71% exact-tuple accuracy. At &lt;strong&gt;0.95 and 0.99&lt;/strong&gt;, it retained 69/70 at &lt;strong&gt;86.96%&lt;/strong&gt;. It barely filtered errors. No threshold was selected or changed using these results.&lt;/p&gt;

&lt;p&gt;This returns to the &lt;a href="https://marcelotaparelli.com.br/en/articles/jev-1-13-decision-model-benchmark/" rel="noopener noreferrer"&gt;caution in the Jev article&lt;/a&gt;: high confidence does not automatically mean a reliable probability of correctness. Jev reports its own confidence; Laya distinguishes &lt;code&gt;answer_confidence&lt;/code&gt; from an entropy-based confidence. Those different semantics prevent a direct comparison of the values.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap between learning and generalization
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Split, selected checkpoint&lt;/th&gt;
&lt;th&gt;Tickets&lt;/th&gt;
&lt;th&gt;Exact tuple&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TRAIN&lt;/td&gt;
&lt;td&gt;1,120&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VALIDATION&lt;/td&gt;
&lt;td&gt;280&lt;/td&gt;
&lt;td&gt;92.50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HELD-OUT&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;85.71%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The sequence &lt;strong&gt;100% → 92.50% → 85.71%&lt;/strong&gt;, TRAIN loss recorded as zero in the final epoch, and saturated confidence show a very strong fit to training data. Since VALIDATION still shares scenario families with TRAIN, the held-out drop deserves attention.&lt;/p&gt;

&lt;p&gt;I interpret the perfect fit and split differences as signs consistent with memorization and overconfidence. The sample is too small to determine the severity of that effect or estimate a production generalization gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local did not automatically mean faster
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Measured path&lt;/th&gt;
&lt;th&gt;Mean latency per ticket&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base Laya&lt;/td&gt;
&lt;td&gt;Local CPU, four AMD EPYC vCPUs, fp32&lt;/td&gt;
&lt;td&gt;1,414 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adapted Laya&lt;/td&gt;
&lt;td&gt;Same CPU context, fp32&lt;/td&gt;
&lt;td&gt;1,619 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jev 1.13&lt;/td&gt;
&lt;td&gt;Historical API/network call&lt;/td&gt;
&lt;td&gt;569 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Laya timings include tokenization, inference over three questions, parsing, and validation; the per-request mean excludes model loading. Adapted held-out inference ran on CPU, &lt;strong&gt;not the training A5000&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On this specific CPU, self-hosted Laya was not automatically faster than calling Jev remotely. Since infrastructure and measurement dates differ, these timings describe the observed serving paths. Historical Ollama hybrid latency also measures a broader path than standalone inference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open source does not mean zero cost
&lt;/h2&gt;

&lt;p&gt;Jev's historical run reported &lt;strong&gt;US$ 0.003789618&lt;/strong&gt; for 70 requests. That is API usage cost for that run, not a current pricing projection or a complete operating-cost estimate.&lt;/p&gt;

&lt;p&gt;Laya inference incurred &lt;strong&gt;US$ 0 in external per-token/request charges&lt;/strong&gt;. CPU/GPU, RAM/VRAM, electricity, cloud resources, and operations still matter. CPU cost was not monetized because the necessary rate and measurements were unavailable.&lt;/p&gt;

&lt;p&gt;For training, the recorded Pod rate was approximately &lt;strong&gt;US$ 0.27 per GPU-hour&lt;/strong&gt;, or &lt;strong&gt;US$ 0.28/hour with container disk&lt;/strong&gt;. Measured tasks — base VALIDATION, the first anomalous run, and corrected training/evaluation — totaled &lt;strong&gt;1,523.51 seconds&lt;/strong&gt;, about 25m24s.&lt;/p&gt;

&lt;p&gt;The report estimates &lt;strong&gt;US$ 0.1185&lt;/strong&gt; for those tasks at the rate including disk. &lt;strong&gt;That is not the Pod's total bill:&lt;/strong&gt; it excludes setup, provider startup, and idle time; total billed time and the invoice were unavailable. The official CPU evaluation added no GPU time.&lt;/p&gt;

&lt;p&gt;Laya removes per-token charges while moving cost and responsibility into infrastructure and operations. Preparing data, investigating an anomaly, and managing artifacts also belong in the decision, even without a monetary measurement of engineering time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real trade-off: convenience and control
&lt;/h2&gt;

&lt;p&gt;Jev demonstrated the value of a decision model that works very well without training specific to this domain. Laya demonstrated a different advantage: open weights let us change a model when zero-shot is insufficient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open source did not automatically give us better accuracy, lower latency, or better calibration. It gave us control.&lt;/strong&gt; That meant taking responsibility for the dataset, training, GPU, checkpoint, calibration, versioning, deployment, observability, and operating costs. This experiment exercised data, training, and evaluation; deploying and operating the adapted service remain future work.&lt;/p&gt;

&lt;p&gt;That is the engineering decision: how much is the ability to adapt behavior and host the model under our constraints worth, given the work required to sustain it?&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A synthetic, English-only HELD-OUT set of just 70 tickets from one domain; no real traffic or production validation.&lt;/li&gt;
&lt;li&gt;One official run per protocol; Jev is a historical result, without repeated runs to measure variance.&lt;/li&gt;
&lt;li&gt;Synthetic TRAIN and VALIDATION data; VALIDATION shares scenario families with TRAIN.&lt;/li&gt;
&lt;li&gt;Different domain exposure, questions, and inference paths for Jev and Laya.&lt;/li&gt;
&lt;li&gt;Different hardware and measurement dates; latency paths are not equivalent.&lt;/li&gt;
&lt;li&gt;Different confidence semantics; calibration and coverage analyses are descriptive and based on small samples.&lt;/li&gt;
&lt;li&gt;Untouched native responses were not preserved byte for byte for the zero-shot audit.&lt;/li&gt;
&lt;li&gt;The roughly 843 MB adapted checkpoint is outside public Git. Its hash, size, metadata, and results are public, but the weights are not available for download there.&lt;/li&gt;
&lt;li&gt;Total billed Pod cost was unavailable; the estimate covers measured tasks only.&lt;/li&gt;
&lt;li&gt;Jev and Laya remain &lt;strong&gt;evaluation-only&lt;/strong&gt; in Ops Triage. Neither was integrated into &lt;code&gt;HybridPolicy&lt;/code&gt; because of these experiments.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would do in a real system
&lt;/h2&gt;

&lt;p&gt;I would start with labeled real tickets permitted for that use, separating scenarios, sources, and time periods so VALIDATION does not merely rephrase TRAIN. I would set acceptance criteria by severity and error cost before opening a new final evaluation.&lt;/p&gt;

&lt;p&gt;Calibration and thresholds would need separate, representative data, with human review and fallback evaluated as part of the policy. I would repeat protocols to measure variance and compare latency, throughput, loading, memory, and cost on the serving paths I actually intend to operate.&lt;/p&gt;

&lt;p&gt;Before integration, I would version weights and configuration alongside data and code, verify the license and context limits, and prepare monitoring for drift and errors, plus rollback. Typed decisions simplify integration; reliability still depends on the surrounding system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence and reproducibility
&lt;/h2&gt;

&lt;p&gt;All figures come from the public reports and artifacts in &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai" rel="noopener noreferrer"&gt;Ops Triage AI&lt;/a&gt;, inspected at commit &lt;code&gt;e8c75f0&lt;/code&gt;. The links below pin that snapshot. Its README still summarizes only zero-shot; domain-adaptation evidence lives in the final report and artifacts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero-shot: &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai/blob/e8c75f0ff75aafcd24403f860e1ad7d4450392f5/docs/evaluation/laya-held-out-a9725ea.md" rel="noopener noreferrer"&gt;Experiment A report&lt;/a&gt; and &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai/blob/e8c75f0ff75aafcd24403f860e1ad7d4450392f5/artifacts/laya-held-out.json" rel="noopener noreferrer"&gt;result JSON&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Adaptation: &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai/blob/e8c75f0ff75aafcd24403f860e1ad7d4450392f5/docs/evaluation/laya-domain-adaptation-protocol.md" rel="noopener noreferrer"&gt;protocol&lt;/a&gt; and &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai/blob/e8c75f0ff75aafcd24403f860e1ad7d4450392f5/artifacts/laya-adapt-freeze.json" rel="noopener noreferrer"&gt;Freeze 2&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Experiment B results: &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai/blob/e8c75f0ff75aafcd24403f860e1ad7d4450392f5/artifacts/laya-adapt-held-out.json" rel="noopener noreferrer"&gt;held-out JSON&lt;/a&gt; and &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai/blob/e8c75f0ff75aafcd24403f860e1ad7d4450392f5/artifacts/laya-adapt-held-out-attempts.jsonl" rel="noopener noreferrer"&gt;attempt journal&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Training and selection: &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai/blob/e8c75f0ff75aafcd24403f860e1ad7d4450392f5/artifacts/laya-adapt-training.json" rel="noopener noreferrer"&gt;corrected run&lt;/a&gt;, &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai/blob/e8c75f0ff75aafcd24403f860e1ad7d4450392f5/artifacts/laya-adapt-selection.json" rel="noopener noreferrer"&gt;VALIDATION selection&lt;/a&gt;, and &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai/blob/e8c75f0ff75aafcd24403f860e1ad7d4450392f5/artifacts/laya-adapt-checkpoint-metadata.json" rel="noopener noreferrer"&gt;weight metadata&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Audit: &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai/blob/e8c75f0ff75aafcd24403f860e1ad7d4450392f5/docs/evaluation/laya-adaptation-audit.md" rel="noopener noreferrer"&gt;mapping and archived outputs&lt;/a&gt; and &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai/tree/e8c75f0ff75aafcd24403f860e1ad7d4450392f5/artifacts/laya-adapt-run1-scheduler-anomaly" rel="noopener noreferrer"&gt;preserved initial run&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Historical reference: &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai/blob/e8c75f0ff75aafcd24403f860e1ad7d4450392f5/docs/evaluation/jev-1.13-held-out-4c41e0b.md" rel="noopener noreferrer"&gt;Jev 1.13 report&lt;/a&gt; and &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai/blob/e8c75f0ff75aafcd24403f860e1ad7d4450392f5/artifacts/jev-1.13-held-out-4c41e0b.json" rel="noopener noreferrer"&gt;Jev artifact&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Upstream project: &lt;a href="https://github.com/NandhaKishorM/laya" rel="noopener noreferrer"&gt;official Laya GitHub repository&lt;/a&gt; and &lt;a href="https://github.com/NandhaKishorM/laya/blob/v0.3.23/LICENSE" rel="noopener noreferrer"&gt;license at the runtime release&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>opensource</category>
      <category>mlops</category>
    </item>
    <item>
      <title>Jev 1.13 in Practice: I Tested a Decision Model Against Rules and a Local LLM</title>
      <dc:creator>marcelotaparelli</dc:creator>
      <pubDate>Wed, 23 Sep 2026 02:33:26 +0000</pubDate>
      <link>https://dev.to/marcelotaparelli/jev-113-in-practice-i-tested-a-decision-model-against-rules-and-a-local-llm-3p1c</link>
      <guid>https://dev.to/marcelotaparelli/jev-113-in-practice-i-tested-a-decision-model-against-rules-and-a-local-llm-3p1c</guid>
      <description>&lt;p&gt;Jev 1.13 got every category classification right in my held-out set: &lt;strong&gt;1.0000 category accuracy&lt;/strong&gt;. The same experiment measured 0.9857 priority accuracy and 0.9571 risk accuracy. Those are results on 70 synthetic tickets; they do not prove that Jev is superior in production.&lt;/p&gt;

&lt;p&gt;I decided to test Jev because I already had two reference points in &lt;code&gt;ops-triage-ai&lt;/code&gt;: a deterministic rules baseline and a classifier using local Ollama. The question was whether a third paradigm — a typed probabilistic decision model — would produce different results on the same taxonomy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes compared with a chat LLM
&lt;/h2&gt;

&lt;p&gt;I did not use Jev as a free-form conversation and then interpret prose afterward. A separate adapter sent decisions through the OpenRouter Decisions API using &lt;code&gt;typesafe/jev-1.13&lt;/code&gt;, with typed responses and probability distributions. All 73 API responses during the experiment resolved to &lt;code&gt;typesafe/jev-1.13-20260917&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The adapter implements the &lt;code&gt;TriageClassifier&lt;/code&gt; interface, but remained separate from the Ollama classifier and &lt;code&gt;HybridPolicy&lt;/code&gt;. I used Bun native fetch and added no dependencies. Jev was not integrated into the application flow and did not replace the local model.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I froze the experiment
&lt;/h2&gt;

&lt;p&gt;I used the same taxonomy and the same 70 synthetic labels from the historical held-out benchmark. Development calls happened before the freeze. In commit &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai/tree/4c41e0b" rel="noopener noreferrer"&gt;&lt;code&gt;4c41e0b&lt;/code&gt;&lt;/a&gt;, I froze the configuration and evaluation, then ran the held-out set once for the official result.&lt;/p&gt;

&lt;p&gt;That separation matters: the final set was not used to tune the model and then measure that tuning on the same set. It is still a small synthetic sample, but the protocol makes clear what was frozen and which run produced the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Deterministic&lt;/th&gt;
&lt;th&gt;Ollama&lt;/th&gt;
&lt;th&gt;Jev 1.13&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Category accuracy&lt;/td&gt;
&lt;td&gt;0.8286&lt;/td&gt;
&lt;td&gt;0.9571&lt;/td&gt;
&lt;td&gt;1.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Category macro-F1&lt;/td&gt;
&lt;td&gt;0.8512&lt;/td&gt;
&lt;td&gt;0.9550&lt;/td&gt;
&lt;td&gt;1.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Priority accuracy&lt;/td&gt;
&lt;td&gt;0.9000&lt;/td&gt;
&lt;td&gt;0.9143&lt;/td&gt;
&lt;td&gt;0.9857&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Risk accuracy&lt;/td&gt;
&lt;td&gt;0.9571&lt;/td&gt;
&lt;td&gt;0.9143&lt;/td&gt;
&lt;td&gt;0.9571&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HIGH/CRITICAL priority recall&lt;/td&gt;
&lt;td&gt;0.7857&lt;/td&gt;
&lt;td&gt;1.0000&lt;/td&gt;
&lt;td&gt;1.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HIGH risk recall&lt;/td&gt;
&lt;td&gt;0.5714&lt;/td&gt;
&lt;td&gt;0.7143&lt;/td&gt;
&lt;td&gt;0.8571&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Jev got the full category, priority, and risk tuple right in &lt;strong&gt;66 of 70 cases (0.9429)&lt;/strong&gt;. The historical benchmark did not emit standalone exact-tuple accuracy for the deterministic classifier or Ollama. I therefore do not compare 66/70 with per-field metrics or the hybrid path's exact tuple: they measure different things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measured cost and latency
&lt;/h2&gt;

&lt;p&gt;Across Jev's 70 standalone calls, mean latency was &lt;strong&gt;569.4 ms&lt;/strong&gt;, with p50 at &lt;strong&gt;545.5 ms&lt;/strong&gt;, p95 at &lt;strong&gt;712.2 ms&lt;/strong&gt;, and a maximum of &lt;strong&gt;1,142.2 ms&lt;/strong&gt;. The report recorded &lt;strong&gt;90,229 input tokens&lt;/strong&gt;, a total reported cost of &lt;strong&gt;US$ 0.003789618&lt;/strong&gt;, and zero API or schema failures.&lt;/p&gt;

&lt;p&gt;That cost is the reported amount for this OpenRouter run. The historical Ollama benchmark did not emit cost or standalone latency. Its published 6–7 seconds describe the full hybrid path; that is not an equivalent comparison with a standalone Jev call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confidence is not semantic correctness
&lt;/h2&gt;

&lt;p&gt;Jev preserved probability distributions, continuous confidence, and top-1/top-2 margins. That gives more information for evaluating a decision, but it does not guarantee the decision is right: one incorrect HIGH risk classification had &lt;strong&gt;0.86 confidence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For this observed dataset, exploratory calibration analysis reported Brier scores of 0.0005 for category, 0.0386 for priority, and 0.0800 for risk; top-1 ECE was 0.0054, 0.0329, and 0.0226, respectively. These are statistics over 70 synthetic tickets, with few examples in each range. They do not show that probabilities are calibrated for real tickets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Selective automation: an exploratory signal
&lt;/h2&gt;

&lt;p&gt;When requiring all three confidence values to reach a threshold of 0.80, &lt;strong&gt;54 of 70 tickets&lt;/strong&gt; remained covered, and all covered cases had the observed tuple correct. At 0.90, &lt;strong&gt;51 of 70&lt;/strong&gt; remained, also with 100% observed exact accuracy.&lt;/p&gt;

&lt;p&gt;This does not define a production threshold. The denominator is small, the examples are synthetic, and the result does not establish calibration or performance beyond this set. It is a hypothesis to test with representative data and separately defined review criteria.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this benchmark does not prove
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It does not estimate performance in production or on real tickets.&lt;/li&gt;
&lt;li&gt;One official run does not measure run-to-run variance.&lt;/li&gt;
&lt;li&gt;70 synthetic examples do not represent the full domain distribution.&lt;/li&gt;
&lt;li&gt;The result does not establish that Jev is generally superior to Ollama or rules.&lt;/li&gt;
&lt;li&gt;Exploratory calibration metrics do not validate confidence for real decisions.&lt;/li&gt;
&lt;li&gt;I did not compare Ollama standalone cost or latency because the historical benchmark did not emit them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would do architecturally
&lt;/h2&gt;

&lt;p&gt;I would keep the current decisions: the deterministic baseline and Ollama remain in the existing evaluation and hybrid policy; Jev stays benchmark/evaluation only. Before any integration, I would expand and diversify the labeled data, repeat the frozen evaluation to measure variance, and define how human review and severity errors factor into acceptance criteria.&lt;/p&gt;

&lt;p&gt;The useful result is not “Jev won.” A third paradigm produced different measurable signals in a reproducible run with explicit limits — material for guiding the next evaluation, not for justifying a production replacement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://marcelotaparelli.com.br/en/projects/ops-triage-ai/" rel="noopener noreferrer"&gt;Ops Triage AI case&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai" rel="noopener noreferrer"&gt;ops-triage-ai on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Jev 1.13 held-out report at the freeze commit: &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai/blob/4c41e0b/docs/evaluation/jev-1.13-held-out-4c41e0b.md" rel="noopener noreferrer"&gt;jev-1.13-held-out-4c41e0b.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Run JSON artifact: &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai/blob/4c41e0b/artifacts/jev-1.13-held-out-4c41e0b.json" rel="noopener noreferrer"&gt;jev-1.13-held-out-4c41e0b.json&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>programming</category>
    </item>
    <item>
      <title>A Valid JWT Does Not Mean Authorized Access</title>
      <dc:creator>marcelotaparelli</dc:creator>
      <pubDate>Sat, 19 Sep 2026 21:12:04 +0000</pubDate>
      <link>https://dev.to/marcelotaparelli/a-valid-jwt-does-not-mean-authorized-access-1m38</link>
      <guid>https://dev.to/marcelotaparelli/a-valid-jwt-does-not-mean-authorized-access-1m38</guid>
      <description>&lt;p&gt;A security review of Salus brought me back to a question that remains after authentication:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can this user access this specific resource?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consider this scenario:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User A creates patient X.&lt;/li&gt;
&lt;li&gt;User B signs in and receives a valid JWT.&lt;/li&gt;
&lt;li&gt;B calls &lt;code&gt;GET /patients/X&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the application looks up the patient by &lt;code&gt;patientId&lt;/code&gt; alone, B may receive a patient belonging to A. B is authenticated, but is not authorized to access that object. This is &lt;strong&gt;Broken Object Level Authorization (BOLA)&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authorization must reach the query
&lt;/h2&gt;

&lt;p&gt;A valid JWT establishes who made the request. The access rule still needs to use that identity to scope the resource lookup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;patientId + currentUser.id → authorized resource
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;findById(patientId)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the operation could conceptually use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;findByIdAndOwner(patientId, currentUser.id)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The method name is incidental. The access boundary belongs in the rule and the lookup, including reads, updates, and deletes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should the API return for someone else's patient?
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;403 Forbidden&lt;/code&gt; response can tell B that patient X exists. When even that fact is sensitive, &lt;code&gt;404 Not Found&lt;/code&gt; may be preferable: X is outside the set of resources B is allowed to see.&lt;/p&gt;

&lt;p&gt;The choice depends on the API's policy and should be applied consistently. It makes HTTP responses less useful for probing which IDs exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security as testable behavior
&lt;/h2&gt;

&lt;p&gt;The path does not end at &lt;code&gt;login → JWT → middleware&lt;/code&gt;. It continues through the operation on each object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;identity → authentication → authorization → ownership → resource access
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the property that became a Salus security regression test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User A creates patient X.
User B tries to access X.
Expected result: 404.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hardening work reproduced this scenario as a failing test (RED): User A creates X, User B receives a valid JWT and tries to access it. Ownership was then introduced, operations were scoped by &lt;code&gt;ownerId&lt;/code&gt;, and cross-user access began returning 404 in this context. The regression test now passes (GREEN), alongside JWT verification on patient routes.&lt;/p&gt;

&lt;p&gt;TDD and security regression tests protect this behavior as the system evolves.&lt;/p&gt;

&lt;p&gt;Security should be verifiable behavior, not just configuration.&lt;/p&gt;

&lt;p&gt;Project: &lt;a href="https://github.com/marcelotaparelli/salus" rel="noopener noreferrer"&gt;Salus on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>api</category>
      <category>backend</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Evals: I Stopped Asking Whether the LLM “Looks Good” and Started Measuring</title>
      <dc:creator>marcelotaparelli</dc:creator>
      <pubDate>Wed, 16 Sep 2026 16:03:28 +0000</pubDate>
      <link>https://dev.to/marcelotaparelli/evals-i-stopped-asking-whether-the-llm-looks-good-and-started-measuring-536b</link>
      <guid>https://dev.to/marcelotaparelli/evals-i-stopped-asking-whether-the-llm-looks-good-and-started-measuring-536b</guid>
      <description>&lt;p&gt;When I started working with LLMs, one of the hardest questions looked deceptively simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;how do I know the model is actually getting better?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Running a few examples by hand and thinking "that answer looks good" works at first.&lt;/p&gt;

&lt;p&gt;But it does not scale.&lt;/p&gt;

&lt;p&gt;And, more importantly, it produces no evidence.&lt;/p&gt;

&lt;p&gt;That is when I started to understand the role of &lt;strong&gt;evals&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an eval?
&lt;/h2&gt;

&lt;p&gt;An eval is a structured way of testing the behavior of an AI system.&lt;/p&gt;

&lt;p&gt;The idea is fairly simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;input
+
produced answer
+
expected answer
+
metric
=
evaluation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of subjectively asking whether an answer turned out well, you define up front what you expect and measure the distance between actual behavior and expected behavior.&lt;/p&gt;

&lt;p&gt;An article by Martin Fowler on &lt;a href="https://martinfowler.com/articles/gen-ai-patterns/" rel="noopener noreferrer"&gt;GenAI patterns&lt;/a&gt; describes this kind of mechanism as &lt;strong&gt;scoring and judging&lt;/strong&gt;: the model's output goes through a scorer that produces metrics or feedback about the result.&lt;/p&gt;

&lt;p&gt;That is exactly the principle I applied in my operational triage project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before the LLM, I wrote code
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;ops-triage-ai&lt;/code&gt;, the system receives tickets and has to determine things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;category;&lt;/li&gt;
&lt;li&gt;priority;&lt;/li&gt;
&lt;li&gt;risk;&lt;/li&gt;
&lt;li&gt;suggested team.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before putting an LLM on the problem, I implemented a deterministic classifier with hand-written rules in TypeScript.&lt;/p&gt;

&lt;p&gt;Simplified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ticket
   ↓
deterministic rules
   ↓
classification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That created something extremely valuable:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a baseline.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I now had a concrete implementation to compare any AI-based solution against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I wrote down expected answers
&lt;/h2&gt;

&lt;p&gt;I set aside a collection of tickets and defined in advance what the correct classification for each one should be.&lt;/p&gt;

&lt;p&gt;Conceptually something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ticket"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Production API unavailable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Users cannot access the service"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INCIDENT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CRITICAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"risk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HIGH"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The classifier receives the ticket.&lt;/p&gt;

&lt;p&gt;Its output is compared against &lt;code&gt;expected&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And then we compute metrics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;               ┌──────────────────┐
ticket ───────►│    classifier    │
               └────────┬─────────┘
                        │
                        ▼
                 predicted output
                        │
expected output ────────┤
                        ▼
                    scorer
                        │
                        ▼
                    metrics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That scorer can be plain code.&lt;/p&gt;

&lt;p&gt;It does not need to be another LLM.&lt;/p&gt;

&lt;h2&gt;
  
  
  My code became part of the experiment
&lt;/h2&gt;

&lt;p&gt;That is where I found the most interesting idea.&lt;/p&gt;

&lt;p&gt;The deterministic code I would normally write to solve the problem also became an &lt;strong&gt;experimental reference&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I could run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dataset
   ├── deterministic classifier
   └── LLM classifier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and compare both on exactly the same examples.&lt;/p&gt;

&lt;p&gt;On the project's final held-out set of 70 synthetic tickets, for instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Category accuracy

Deterministic: 82.9%
LLM:           95.7%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;HIGH/CRITICAL&lt;/code&gt; priority recall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Deterministic: 78.6%
LLM:           100%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But something even more important happened.&lt;/p&gt;

&lt;p&gt;The model &lt;strong&gt;did not win everywhere&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On overall risk classification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Deterministic: 95.7%
LLM:            91.4%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without an eval, it would have been easy to look at a few good LLM answers and conclude the model was simply better.&lt;/p&gt;

&lt;p&gt;The metrics told a more interesting story.&lt;/p&gt;

&lt;h2&gt;
  
  
  And then the eval started shaping the architecture
&lt;/h2&gt;

&lt;p&gt;At that point the question stopped being:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do I make the LLM replace my rules?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Where does each approach work best?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That pushed the project toward a hybrid architecture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 ticket
                   │
          ┌────────┴────────┐
          ▼                 ▼
 deterministic           LLM
 classifier           classifier
          │                 │
          └────────┬────────┘
                   ▼
              hybrid policy
                   │
             ┌─────┴─────┐
             ▼           ▼
         decision    human review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The baseline stopped being just an old version of the system.&lt;/p&gt;

&lt;p&gt;It started serving as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a reference;&lt;/li&gt;
&lt;li&gt;a divergence signal;&lt;/li&gt;
&lt;li&gt;a fallback;&lt;/li&gt;
&lt;li&gt;a component of the human-review policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The eval did not just measure the architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It helped determine the architecture.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  It also changes how you develop with LLMs
&lt;/h2&gt;

&lt;p&gt;Without structured evaluation, the loop tends to look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;change the prompt
↓
run a few examples
↓
looks better
↓
deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With evals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;change prompt/model/policy
↓
run the dataset
↓
measure results
↓
compare against baseline
↓
analyze regressions
↓
decide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That difference looks small.&lt;/p&gt;

&lt;p&gt;But it is the difference between experimenting and simply trusting an impression.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not every metric needs to come from an LLM
&lt;/h2&gt;

&lt;p&gt;There is a lot of discussion about &lt;strong&gt;LLM-as-a-judge&lt;/strong&gt;, where another model grades the produced answer.&lt;/p&gt;

&lt;p&gt;That is useful when the criteria are subjective, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clarity;&lt;/li&gt;
&lt;li&gt;relevance;&lt;/li&gt;
&lt;li&gt;coherence;&lt;/li&gt;
&lt;li&gt;quality of an open-ended answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But when there is a verifiable answer, traditional code is usually simpler.&lt;/p&gt;

&lt;p&gt;In my case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;predicted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;already answers an important question.&lt;/p&gt;

&lt;p&gt;The evaluation tool should be proportional to the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The main takeaway
&lt;/h2&gt;

&lt;p&gt;I used to think of evals as something that happens after building an AI system.&lt;/p&gt;

&lt;p&gt;I see it differently now.&lt;/p&gt;

&lt;p&gt;The eval is part of development itself.&lt;/p&gt;

&lt;p&gt;It helps answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;did the model improve?
where did it get worse?
by how much?
in which cases?
compared to what?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And perhaps the most important question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;does this improvement actually justify putting the LLM in this part of the system?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without that, it is easy to build a convincing demo.&lt;/p&gt;

&lt;p&gt;With it, engineering starts to show up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep exploring
&lt;/h2&gt;

&lt;p&gt;The full details — official metrics, trade-offs, and limitations — are in the &lt;a href="https://marcelotaparelli.com.br/en/projects/ops-triage-ai/" rel="noopener noreferrer"&gt;project case&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Project: &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai" rel="noopener noreferrer"&gt;ops-triage-ai on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>programming</category>
    </item>
    <item>
      <title>The LLM Didn't Win Everywhere — and That's What Made the Project Interesting</title>
      <dc:creator>marcelotaparelli</dc:creator>
      <pubDate>Tue, 15 Sep 2026 09:33:21 +0000</pubDate>
      <link>https://dev.to/marcelotaparelli/the-llm-didnt-win-everywhere-and-thats-what-made-the-project-interesting-a1a</link>
      <guid>https://dev.to/marcelotaparelli/the-llm-didnt-win-everywhere-and-thats-what-made-the-project-interesting-a1a</guid>
      <description>&lt;p&gt;I closed an important stage of &lt;code&gt;ops-triage-ai&lt;/code&gt;, an operational triage&lt;br&gt;
system that combines a deterministic baseline, a local LLM, and a hybrid&lt;br&gt;
policy with human review.&lt;/p&gt;

&lt;p&gt;The most interesting result was not simply "the LLM was better."&lt;/p&gt;

&lt;h2&gt;
  
  
  What the benchmark measured
&lt;/h2&gt;

&lt;p&gt;Evaluation on a frozen held-out set of 70 synthetic tickets, in a single&lt;br&gt;
run. Deterministic baseline versus the local LLM:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Category accuracy: 82.9% → 95.7%&lt;/li&gt;
&lt;li&gt;HIGH/CRITICAL priority recall: 78.6% → 100%&lt;/li&gt;
&lt;li&gt;HIGH risk recall: 57.1% → 71.4%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the point that matters: the deterministic baseline still won on&lt;br&gt;
overall risk accuracy — 95.7% against the LLM's 91.4%.&lt;/p&gt;

&lt;p&gt;The LLM did not win everywhere. The regression is published alongside the&lt;br&gt;
gains, with nothing smoothed over.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question changed
&lt;/h2&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;"How do we replace rules with AI?"&lt;/p&gt;

&lt;p&gt;the architecture answers:&lt;/p&gt;

&lt;p&gt;"How do we combine different behaviors in a way that is safe, observable,&lt;br&gt;
and auditable?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The final architecture
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Deterministic baseline as reference and fallback;&lt;/li&gt;
&lt;li&gt;Local LLM for semantic interpretation;&lt;/li&gt;
&lt;li&gt;Hybrid policy to decide when human review is required;&lt;/li&gt;
&lt;li&gt;Audit trail preserving ticket, run, decision, and feedback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the final benchmark:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;51.4% of cases were routed to human review;&lt;/li&gt;
&lt;li&gt;the classifiers disagreed on 32.9% of tickets;&lt;/li&gt;
&lt;li&gt;there was no fallback and no LLM failure during the official run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Review rate describes operations here — how many cases asked for review —&lt;br&gt;
not review precision/recall. The held-out set has no independent ground&lt;br&gt;
truth for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accepted limits
&lt;/h2&gt;

&lt;p&gt;Synthetic dataset, in English, with 70 examples. A single official run,&lt;br&gt;
with no measurement of LLM variance. The system has not been validated&lt;br&gt;
under real production traffic. Hybrid latency sits around 6 to 7 seconds:&lt;br&gt;
fine for asynchronous triage, not for a critical synchronous path. The&lt;br&gt;
full details — official metrics, trade-offs, and the 13 published&lt;br&gt;
limitations — are in the &lt;a href="https://marcelotaparelli.com.br/en/projects/ops-triage-ai/" rel="noopener noreferrer"&gt;project case&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;A useful AI system does not need to trust the model blindly.&lt;/p&gt;

&lt;p&gt;Sometimes the best architecture comes precisely from understanding where&lt;br&gt;
the model is better — and where it is not.&lt;/p&gt;

&lt;p&gt;Project: &lt;a href="https://github.com/marcelotaparelli/ops-triage-ai" rel="noopener noreferrer"&gt;https://github.com/marcelotaparelli/ops-triage-ai&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Why I Built My Portfolio with Bun + Astro + MDX Instead of a More Complex Stack</title>
      <dc:creator>marcelotaparelli</dc:creator>
      <pubDate>Sun, 13 Sep 2026 13:05:47 +0000</pubDate>
      <link>https://dev.to/marcelotaparelli/why-i-built-my-portfolio-with-bun-astro-mdx-instead-of-a-more-complex-stack-2cmd</link>
      <guid>https://dev.to/marcelotaparelli/why-i-built-my-portfolio-with-bun-astro-mdx-instead-of-a-more-complex-stack-2cmd</guid>
      <description>&lt;p&gt;Choosing a stack is a product decision before it is a technical one. This&lt;br&gt;
article documents why this portfolio runs on Bun, Astro, and MDX — and,&lt;br&gt;
more importantly, the reasoning that led me to turn down more sophisticated&lt;br&gt;
alternatives. Nothing here is theory: every claim points to something&lt;br&gt;
verifiable in this repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;I needed a portfolio that could carry professional authority as a Software&lt;br&gt;
Engineer going deeper into Applied AI Engineering. The requirements were&lt;br&gt;
concrete: bilingual content (Portuguese and English), project cases and&lt;br&gt;
articles with their own identity, solid SEO, high performance, genuine&lt;br&gt;
accessibility, and maintenance simple enough that I would never have to&lt;br&gt;
think about it twice. No backend, no platform team, no infrastructure&lt;br&gt;
budget — just me, the code, and the content.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question behind the architecture
&lt;/h2&gt;

&lt;p&gt;The guiding question was simple: what is the simplest solution capable of&lt;br&gt;
solving this problem? Not "which stack is trending," and not "which one&lt;br&gt;
shows off the most skill." Technology chosen for hype often charges&lt;br&gt;
interest in complexity. Every layer I added had to justify itself against this specific&lt;br&gt;
problem — a mostly static content site maintained by one person.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Bun
&lt;/h2&gt;

&lt;p&gt;Bun is this project's toolchain: package manager, runner for the&lt;br&gt;
development scripts, and runtime for the validation tooling. The dev&lt;br&gt;
server, build, artifact validation, asset measurement, and release-check&lt;br&gt;
scripts are TypeScript executed directly by Bun, with no separate&lt;br&gt;
transpilation layer for tooling. I make no comparative claims I haven't&lt;br&gt;
measured here — the decision was about structural simplicity: one tool&lt;br&gt;
where there would otherwise be several. What is observable is objective&lt;br&gt;
but contextual: in this implementation, on the local environment, the full&lt;br&gt;
build renders every page in about two seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Astro
&lt;/h2&gt;

&lt;p&gt;This site's content is predominantly static, so HTML is generated at build&lt;br&gt;
time and served as files — no application server, no database, no&lt;br&gt;
per-request runtime. Astro was chosen for exactly that model: it lets me&lt;br&gt;
write composable editorial content while shipping almost no JavaScript to&lt;br&gt;
the browser. The script embedded in the home page is 277 bytes (198&lt;br&gt;
gzipped) and exists for exactly one job: the mobile menu. Zero bytes of&lt;br&gt;
client-side framework — measured, not estimated. React was never installed&lt;br&gt;
because no problem required React. This model also simplifies SEO: every&lt;br&gt;
page arrives as complete HTML, with its own canonical and alternates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why MDX
&lt;/h2&gt;

&lt;p&gt;Articles and cases live versioned alongside the code, in the same&lt;br&gt;
repository, under the same review process. There is no CMS or backend to&lt;br&gt;
maintain, update, or pay for — and no content outside version control. MDX&lt;br&gt;
gives the content structure (schema-validated frontmatter requiring a&lt;br&gt;
bilingual pair, slug, category, and review status), which makes every piece&lt;br&gt;
of writing an artifact as reviewable as any other code. Publishing means&lt;br&gt;
merging and building; rolling back means reverting a commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not Next.js, SPA, or CMS
&lt;/h2&gt;

&lt;p&gt;Not because they are bad technologies — they are excellent for the right&lt;br&gt;
problems. A full-stack framework would solve problems I don't have:&lt;br&gt;
per-request rendering, API routes, global client state. A client-rendered&lt;br&gt;
SPA would add JavaScript runtime and browser state to a site whose main&lt;br&gt;
job is delivering ready-to-read content. A CMS would trade versioned files for an external dependency with logins,&lt;br&gt;
backups, and a bill. None of that complexity was justified by the current&lt;br&gt;
problem, so none of it got in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bilingualism as an architecture requirement
&lt;/h2&gt;

&lt;p&gt;Bilingualism is not a plugin bolted on afterward — it shaped the&lt;br&gt;
architecture. Portuguese lives at &lt;code&gt;/&lt;/code&gt;, English at &lt;code&gt;/en/&lt;/code&gt;, and every&lt;br&gt;
published item is required to exist in both languages: the publication&lt;br&gt;
model rejects at build time anything published without its reviewed&lt;br&gt;
counterpart. Every page carries its own canonical and three alternates&lt;br&gt;
(current language, opposite language, and x-default), and the artifact&lt;br&gt;
validator checks the pairs across every generated page. Incomplete content&lt;br&gt;
simply never reaches production: drafts are excluded from the production&lt;br&gt;
build and included only in the preview build. The &lt;a href="https://marcelotaparelli.com.br/en/projects/" rel="noopener noreferrer"&gt;projects page&lt;/a&gt;&lt;br&gt;
and the &lt;a href="https://marcelotaparelli.com.br/en/about/" rel="noopener noreferrer"&gt;about page&lt;/a&gt; exist in both languages because the&lt;br&gt;
system would accept nothing less.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quality as part of the product
&lt;/h2&gt;

&lt;p&gt;Here, quality is not a promise — it is a pipeline. Every change goes&lt;br&gt;
through strict typechecking (zero errors), linting, verified formatting,&lt;br&gt;
unit tests for the publication model (4/4), thirteen browser tests&lt;br&gt;
(reciprocal PT/EN navigation, keyboard menu with Escape, no-JavaScript&lt;br&gt;
navigation, localized 404, explicit external links, 320px reflow with&lt;br&gt;
automated WCAG checks), and&lt;br&gt;
artifact validation in both build modes. If something breaks, the build&lt;br&gt;
says so — before any human needs to check.&lt;/p&gt;

&lt;p&gt;CI/CD pipeline with GitHub Actions covering type checking, linting, unit&lt;br&gt;
tests, Playwright E2E tests, artifact validation, and production builds.&lt;br&gt;
Delivery follows a Continuous Delivery model: the same CI-approved&lt;br&gt;
artifact is published after explicit manual approval, without rebuilding,&lt;br&gt;
to a dedicated production branch and then deployed to Hostinger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measured performance
&lt;/h2&gt;

&lt;p&gt;What follows are laboratory measurements, never real-user data. I ran&lt;br&gt;
Lighthouse 13.4.1 in headless Chromium, mobile simulation (412×823&lt;br&gt;
viewport, simulated network and CPU throttling), three runs per page,&lt;br&gt;
serving the local static build — methodology recorded in&lt;br&gt;
&lt;code&gt;scripts/lighthouse.ts&lt;/code&gt;, results in &lt;code&gt;reports/lighthouse-summary.json&lt;/code&gt;. On&lt;br&gt;
the home page: performance 100 and accessibility 100 across all three&lt;br&gt;
runs, LCP between roughly 1.5 s and 1.7 s, CLS around 0.0006, zero TBT,&lt;br&gt;
and about 93.5 KB of initial transfer. The asset budget&lt;br&gt;
(&lt;code&gt;reports/assets.json&lt;/code&gt;) shows where the lightness comes from: roughly&lt;br&gt;
6 KB of gzipped CSS, about 0.2 KB of inline JavaScript, and fonts totaling&lt;br&gt;
around 48 KB. Lab numbers inform decisions; they prove nothing about real&lt;br&gt;
user experience, and I would never present them as such.&lt;/p&gt;

&lt;h2&gt;
  
  
  A bug worth finding
&lt;/h2&gt;

&lt;p&gt;The artifact validator flagged a broken link on the Portuguese 404 page:&lt;br&gt;
&lt;code&gt;/404/&lt;/code&gt;. The cause was in the header's language switcher, which used&lt;br&gt;
&lt;code&gt;Astro.url.pathname&lt;/code&gt; as the current-language link — and with trailing&lt;br&gt;
slashes always on, that pathname renders as &lt;code&gt;/404/&lt;/code&gt;. But the actual&lt;br&gt;
artifact Astro generates for &lt;code&gt;404.astro&lt;/code&gt; is &lt;code&gt;404.html&lt;/code&gt;, a special 404&lt;br&gt;
document for static hosting; the &lt;code&gt;/404/&lt;/code&gt; route never existed. The fix went&lt;br&gt;
into the routing model (the 404 page now links its canonical&lt;br&gt;
&lt;code&gt;/404.html&lt;/code&gt; and &lt;code&gt;/en/404/&lt;/code&gt; paths), not into the test. Weakening the&lt;br&gt;
validator would have hidden the symptom and destroyed its value. The&lt;br&gt;
episode became a working rule: fix the publication model, never work&lt;br&gt;
around the validator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;p&gt;Every choice has a cost, and these are mine, accepted: no backend, no&lt;br&gt;
database, no CMS — any new content requires a commit, a build, and a&lt;br&gt;
deploy. No React at launch — richer interactivity in the future will&lt;br&gt;
require revisiting the decision. No analytics — there is no real-usage&lt;br&gt;
telemetry, so the lab metrics above are the ceiling of what I can claim&lt;br&gt;
today. And bilingualism costs double review: every piece must exist, make&lt;br&gt;
sense, and be approved in both languages. A deliberate cost, because&lt;br&gt;
international reach and consistency across languages are part of the&lt;br&gt;
product I want to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Mature engineering is not about choosing the most sophisticated stack —&lt;br&gt;
it is about choosing complexity proportional to the problem. This&lt;br&gt;
portfolio could have been a full-stack monolith, an SPA, or a CMS&lt;br&gt;
instance; those approaches could work, but they would introduce&lt;br&gt;
capabilities and operational costs this project's requirements never asked&lt;br&gt;
for. Technology starts from the human&lt;br&gt;
problem, not from the code: understand what needs to change, choose&lt;br&gt;
deliberately, and examine the outcome with evidence. FROM REAL PROBLEMS TO&lt;br&gt;
INTELLIGENT PRODUCTS — including when building the showcase itself.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>astro</category>
      <category>bunjs</category>
      <category>showdev</category>
    </item>
    <item>
      <title>⚡ C# Tip: ToCharArray() vs ToArray() — Why It Matters More Than You Think</title>
      <dc:creator>marcelotaparelli</dc:creator>
      <pubDate>Fri, 04 Jul 2025 18:29:21 +0000</pubDate>
      <link>https://dev.to/marcelotaparelli/c-tip-tochararray-vs-toarray-why-it-matters-more-than-you-think-56ba</link>
      <guid>https://dev.to/marcelotaparelli/c-tip-tochararray-vs-toarray-why-it-matters-more-than-you-think-56ba</guid>
      <description>&lt;p&gt;As developers, we often write code that “just works” — but sometimes, small choices can have a big impact on performance.&lt;/p&gt;

&lt;p&gt;One example? The subtle but meaningful difference between ToCharArray() and ToArray() when working with strings in C#. Let’s break it down 👇&lt;/p&gt;



&lt;h2&gt;
  
  
  🧩 What Each One Does
&lt;/h2&gt;

&lt;p&gt;✅ &lt;strong&gt;ToCharArray()&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native method on the string class.&lt;/li&gt;
&lt;li&gt;Returns a char[].&lt;/li&gt;
&lt;li&gt;Fast and direct.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🚧 &lt;strong&gt;ToArray()&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extension method from LINQ (System.Linq).&lt;/li&gt;
&lt;li&gt;Treats the string as IEnumerable.&lt;/li&gt;
&lt;li&gt;Adds abstraction and intermediate steps.&lt;/li&gt;
&lt;/ul&gt;



&lt;h2&gt;
  
  
  🔍 The Overhead Problem
&lt;/h2&gt;

&lt;p&gt;Calling ToArray() on a string might seem harmless, but it invokes LINQ under the hood. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extra allocations&lt;/li&gt;
&lt;li&gt;Added abstraction&lt;/li&gt;
&lt;li&gt;Unnecessary iteration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Meanwhile, ToCharArray() skips all that and copies the characters directly — with less memory and better performance. In tight loops, I've seen it run 2–3x faster. That adds up.&lt;/p&gt;



&lt;h2&gt;
  
  
  🧪 When Should You Use Each?
&lt;/h2&gt;

&lt;p&gt;Let’s keep it simple:&lt;/p&gt;

&lt;p&gt;👉 Use ToCharArray() when you're working directly with strings — parsing, looping, checking characters, etc.&lt;br&gt;
👉 Use ToArray() when you're working with LINQ and want to convert the result of a query to an array.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Using LINQ to filter and convert to array
var expensive = products
 .Where(p =&amp;gt; p.Price &amp;gt; 100)
 .ToArray(); // 👍 This is a perfect case for ToArray()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h2&gt;
  
  
  💡 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Is this a micro-optimization? Maybe.&lt;br&gt;
But understanding how your tools work under the hood can make you a better developer — and sometimes, a faster one too.&lt;/p&gt;



&lt;p&gt;If this helped or surprised you, give it a share so others don’t fall into the same trap 😄&lt;/p&gt;

&lt;p&gt;I’d love to hear your thoughts — have you encountered other subtle performance gotchas in C#?&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>performance</category>
      <category>programmingtips</category>
    </item>
  </channel>
</rss>
