Back to documentation

Matemáticas de DCS-Gate — pipeline end-to-end

Versión: v8.7 (incluye streaming layer) Autor: Daniel Trejo (corekeepper@gmail.com) Doc generado: 2026-05 Propósito: mapear cada cálculo numérico que ocurre desde que llega una respuesta de un modelo hasta que sale un veredicto de autenticidad. El pipeline matemático es idéntico entre /auth (no-streaming) y /auth/stream (SSE) — la única diferencia es que /auth/stream emite los resultados intermedios al cliente conforme se producen.

Este documento sigue el orden cronológico del pipeline. Cada sección incluye:


0. Notación y convenciones

Símbolo Significado
$\mathbf{v} \in \mathbb{R}^d$ Vector de embedding, dimensión $d = 1024$
$\hat{\mathbf{v}}$ Vector normalizado a la esfera unitaria ($|\hat{\mathbf{v}}| = 1$)
$\langle \mathbf{a}, \mathbf{b} \rangle$ Producto interno (dot product)
$\cos(\mathbf{a}, \mathbf{b})$ Similitud coseno
$\mathbf{c}_k$ Centroide del intent $k$ (k ∈ 20 categorías)
$\mathbf{p}+, \mathbf{p}-, \mathbf{p}_0$ Polos positivo (certeza), negativo (duda), neutro (edge)
chain $= [i_1, …, i_n]$ Secuencia de intents extraída de la respuesta
baseline 61 vectores corpus = 36 core + 13 shadow + 12 edge

Convención clave: todos los vectores se almacenan post-normalización L2. Por eso el producto interno es idéntico al coseno y evitamos la división por normas en cada comparación. Esto es lo que hace que TopK sea solo dot() en vec.go.


1. Embedding y normalización L2 (entrada del pipeline)

Toda frase y todo prototipo de intent pasa primero por el modelo mxbai-embed-large vía Ollama.

mxbai-embed-large:  texto ─→ ℝ^1024 (float64)

Llamada HTTP

embedding.go:39-43

body, _ := json.Marshal(embedReq{Model: "mxbai-embed-large", Prompt: text})
resp, _  := http.Post(ollamaURL+"/api/embeddings", "application/json", body)

Normalización inmediata

embedding.go:55 y vec.go:6-20

$$\hat{\mathbf{v}} = \frac{\mathbf{v}}{|\mathbf{v}|2} = \frac{\mathbf{v}}{\sqrt{\sum$$}^{1024} v_i^2}

func normalize(v []float64) []float64 {
    var n float64
    for _, x := range v { n += x*x }
    n = math.Sqrt(n)
    if n == 0 { return v }
    out := make([]float64, len(v))
    for i, x := range v { out[i] = x / n }
    return out
}

Por qué normalizar: después de proyectar en la esfera unitaria, el producto interno $\langle \hat{\mathbf{a}}, \hat{\mathbf{b}} \rangle$ es exactamente el coseno del ángulo entre los vectores originales. Ahorras una división por request y todas las comparaciones son consistentes en escala $[-1, +1]$.

Caché LRU: cache.go evita re-embedding del mismo texto. Capacidad por defecto: 2000 entradas (config CACHE_SIZE).


2. Similitud coseno (la operación base)

vec.go:23-32

$$\cos(\hat{\mathbf{a}}, \hat{\mathbf{b}}) = \sum_{i=1}^{1024} \hat{a}_i \cdot \hat{b}_i \quad\quad \in [-1, +1]$$

func dot(a, b []float64) float64 {
    var s float64
    for i := range a { s += a[i]*b[i] }
    return s
}

Esta función se llama miles de veces por request. Es la primitiva sobre la que se construye todo: clasificación de intents, score polar, TopK, cross-corpus metrics, búsqueda de keyword n-gram.

Interpretación:

coseno significado
$\approx +1$ vectores casi colineales (mismo significado)
$\approx 0$ ortogonales (no relacionados)
$\approx -1$ antipolares (significados opuestos) — raro en embeddings reales

En la práctica, embeddings de mxbai-embed-large producen cosenos típicamente en $[0.0, 0.95]$. Valores >0.85 implican casi-paráfrasis.


3. Construcción de centroides de intent

20 intents (definidos en intents.go:15-24):

VALIDATE, EXPAND, CLOSE,
REDIRECT_EMOTIONAL, REDIRECT_SEMANTIC, EVADE, EXPLORE, REGISTER_MATCH,
FRAME_CAPTURE, ALIGN, MIRROR, FABRICATE,
CONTROL_SELF_EXPOSURE, ANCHOR, SOFT_DEFLECT, PATTERN_LOCK,
HOLD_OPEN, PROBE, CALIBRATE, REPAIR

Cada intent tiene N frases prototipo (típicamente 5-12) en data/intent_prototypes.json. Se embeben todas y se promedian.

Fórmula

intents.go:108-126 + vec.go:35-49

Para cada intent $k$ con prototipos ${e_1, …, e_{N_k}}$:

$$\mathbf{c}k = \text{normalize}\left(\frac{1}{N_k} \sum\right)$$}^{N_k} \hat{\mathbf{e}}_i^{(k)

func mean(vs [][]float64) []float64 {
    dim := len(vs[0])
    out := make([]float64, dim)
    for _, v := range vs {
        for i := range v { out[i] += v[i] }
    }
    for i := range out { out[i] /= float64(len(vs)) }
    return normalize(out)  // ← re-normaliza el promedio
}

Por qué re-normalizar después del promedio: el promedio de vectores unitarios no es unitario (la norma se reduce si hay dispersión). Re-normalizar mantiene la convención de “dot product = coseno”.

Por qué el centroide funciona: asume que los embeddings de frases-mismo-intent forman un cluster compacto en la esfera, y que su media es un buen representante geométrico de ese cluster. Es la versión más barata de “1-nearest-class”: $O(1)$ comparaciones por clase en lugar de $O(N_k)$.


4. Segmentación de frases

intents.go:175-200

Cortar la respuesta del modelo en unidades evaluables independientemente.

for _, r := range text {
    cur.WriteRune(r)
    if r == '.' || r == '!' || r == '?' || r == ';' || r == '\n' {
        s := strings.TrimSpace(cur.String())
        if wordsCount(s) >= 3 {     // ← filtra ruido
            out = append(out, s)
        }
        cur.Reset()
    }
}

Reglas:

  1. Separadores: ., !, ?, ;, \n.
  2. Mínimo 3 palabras por segmento (descarta “Sí.”, “Claro.”, “Por supuesto!”).
  3. El guión largo NO segmenta (es puntuación suave en español).
  4. El último segmento sin puntuación final también se incluye si tiene ≥3 palabras.

Implicación: una respuesta de n frases genera n llamadas a Ollama para embeddings de frases + 1 llamada extra para el embedding de la respuesta completa (usado por polo + TopK + cross-corpus).


5. Clasificación de intent por frase (argmax cosine)

intents.go:128-145

Para cada frase con embedding $\hat{\mathbf{v}}_s$:

$$\text{intent}(s) = \arg\max_{k \in \text{20 intents}} \langle \hat{\mathbf{v}}_s, \mathbf{c}_k \rangle$$

func (b *IntentBank) Classify(vec []float64) (string, float64) {
    best := ""; bestScore := -1.0
    for name, c := range b.centroids {
        s := dot(vec, c)
        if s > bestScore { bestScore = s; best = name }
    }
    if bestScore < b.threshold {     // 0.55 por defecto
        return "UNCLASSIFIED", bestScore
    }
    return best, bestScore
}

Umbral de confianza

config.go:29: INTENT_THRESHOLD = 0.55 (env override).

Si $\max_k \cos < 0.55$ → la frase queda como UNCLASSIFIED. Este umbral se eligió calibrando contra los 21 golden tests:

La calibración se puede correr automáticamente con el endpoint /calibrate (ver §14).


6. Búsqueda de keyword n-gram (refinamiento)

analyzer.go:123-146

Una vez asignado un intent a una frase, se busca el n-gram (1 a 4 palabras) que más se acerca al centroide del intent ganador. Esto fundamenta la decisión: “te clasifiqué como VALIDATE porque el n-gram ‘gran pregunta’ tiene cos=0.74 con el centroide VALIDATE”.

for n := 1; n <= 4 && n <= len(words); n++ {
    for i := 0; i+n <= len(words); i++ {
        ngram := strings.Join(words[i:i+n], " ")
        vec, _, _ := emb.Get(ngram)
        s := dot(vec, centroid)        // cos(ngram, c_k)
        if s > bestSim { bestSim = s; bestKw = ngram }
    }
}

Costo: $O(W \cdot 4)$ embeddings por frase, donde $W$ es el número de palabras. Para frase de 20 palabras → ~80 embeddings extra. Mitigado por el caché LRU (n-grams comunes como “gran pregunta” se reembebbing una sola vez por sesión).


7. Score polar (pos / neg / neu)

baseline.go:472-502

Los polos son centroides pre-computados (offline en compute_poles.py) de tres conjuntos:

Cargados desde data/poles_1024.json.

Cálculo

$$s_+ = \cos(\hat{\mathbf{v}}, \mathbf{p}+) \quad\quad s- = \cos(\hat{\mathbf{v}}, \mathbf{p}_-) \quad\quad s_0 = \cos(\hat{\mathbf{v}}, \mathbf{p}_0)$$

Score crudo: $$r = s_+ - s_-$$

Bucketing

switch {
case raw > posThr:       bucket = +1; label = "certeza_performada"
case raw < negThr:       bucket = -1; label = "duda_performada"
default:                 // raw entre los dos umbrales
    if simNeu > sp && simNeu > sn {
        bucket = 0; label = "frontera_edge"   // edge domina
    } else {
        bucket = 0; label = "neutro"
    }
}

Umbrales (config defaults): - POLE_POS_THRESHOLD = +0.25 - POLE_NEG_THRESHOLD = -0.25 - Zona muerta $[-0.25, +0.25]$ se subdivide por dominancia del polo neutro.

Ejemplo numérico:

respuesta $s_+$ $s_-$ $r$ bucket label
“¡Gran pregunta! 🤔” 0.72 0.18 +0.54 +1 certeza_performada
“La verdad no estoy seguro…” 0.22 0.68 −0.46 −1 duda_performada
“Depende de qué entiendas…” 0.31 0.29 +0.02 0 neutro (o edge si $s_0$ > ambos)

8. TopK retrieval (corpus de 61 vectores)

baseline.go:294-297, baseline.go:397-436 y baseline.go:507-520

Calcula los $K = 5$ vectores corpus más cercanos al embedding de la respuesta.

Algoritmo: min-heap de tamaño K

h := &minHeap{}
for i, e := range entries {
    s := dot(vec, e.Vec)
    if h.Len() < k {
        heap.Push(h, Item{s, i})
    } else if s > (*h)[0].Score {
        heap.Pop(h)
        heap.Push(h, Item{s, i})
    }
}

Complejidad: - Tiempo: $O(N \log K)$ con $N = 61$, $K = 5$ → ~140 ops. - Espacio: $O(K) = O(5)$.

Por qué min-heap y no sort completo: para $K \ll N$, mantener un min-heap del top K es asintóticamente mejor que sort.Slice ($O(N \log N) \approx 360$ ops). En esta escala (61 vectores) la diferencia es ~0.05 ms, irrelevante. Importará cuando se escale a 2k+ vectores.

Variantes


9. Cross-corpus metrics

baseline.go:328-368

Cuantifica cómo se distribuye la respuesta entre los tres pools: core (control típico), shadow (variantes), edge (frontera/ambiguos).

Top1 por pool

$$s_{\text{core}}^{(1)} = \max_{i \in \text{core}} \cos(\hat{\mathbf{v}}, \hat{\mathbf{v}}_i)$$

igual para shadow y edge.

Dominancia

dominancia = argmax(s_core, s_shadow, s_edge)

Deriva

sims = sort_desc([s_core, s_shadow, s_edge])
deriva = sims[0] − sims[1]

Mide cuán “concentrada” está la respuesta en su pool dominante. Deriva grande ⇒ pool dominante claramente separado.

Textura — entropía softmax normalizada

baseline.go:440-468

Sea $s_i$ los tres top1. Aplica softmax estable:

$$p_i = \frac{e^{s_i - \max_j s_j}}{\sum_j e^{s_j - \max_j s_j}}$$

Entropía de Shannon (log base 2): $$H = -\sum_{i=1}^{3} p_i \log_2 p_i$$

Normalizada al rango $[0, 1]$: $$\text{textura} = \frac{H}{\log_2 3} \in [0, 1]$$

textura interpretación
≈ 0 un solo pool domina (firma neta)
≈ 1 los tres pools igual de cerca (firma ambigua)

Nota: la sustracción del máximo en el numerador es para estabilidad numérica del softmax (evita overflow cuando $s_i$ son valores grandes positivos).

Señales binarias

cierre_artificial = (dominancia == "shadow") && (s_shadow > 0.7) && (deriva > 0.05)
continuidad       = (dominancia == "core")   && (s_core   > 0.7)

Hipótesis encarnada: una respuesta que se parece claramente a algo del corpus shadow (variantes de control) está cerrando artificialmente; una que se parece al corpus core (control típico) está manteniendo continuidad con patrones conocidos.


10. Trayectoria de intents (LCS + densidad de rupturas)

intents.go:237-280 y intents.go:285-429

Una respuesta es una secuencia ordenada de intents, no un bag-of-intents. La trayectoria es la unidad de análisis fundamental.

10.1 Transiciones canónicas

intents.go:39-69

Mapa estático: dado un intent, ¿cuál es el siguiente “esperado”?

VALIDATE → EXPAND
EXPAND   → CLOSE
CLOSE    → (terminal)
EVADE    → EVADE
HOLD_OPEN → HOLD_OPEN
...

10.2 Deviation flag

if step.PredictedNext != "" && step.ActualNext != "" &&
   step.PredictedNext != step.ActualNext {
    step.Deviation = true
}

Cuenta cuántas veces la respuesta “rompe” la trayectoria predicha por el mapa canónico.

10.3 Control density

intents.go:237-250

Define genuineIntents = {EXPLORE, HOLD_OPEN, PROBE, CALIBRATE, REPAIR}. Todo lo demás es régimen de control.

$$\rho_{\text{ctrl}}(C) = \frac{|{i \in C : i \notin \text{genuine}}|}{|C|}$$

10.4 Pattern break density

intents.go:252-280

Una ruptura es una transición entre régimen de control y régimen genuino (o viceversa). En notación:

$$\text{break}(j) = \mathbb{1}[\text{ctrl}(i_{j-1})] \oplus \mathbb{1}[\text{ctrl}(i_j)]$$

donde $\oplus$ es XOR (exactamente uno es control). Entonces:

$$\text{density} = \frac{\sum_{j=2}^{n} \text{break}(j)}{|C|}$$

Y se calculan dos cantidades separadas:

$$\rho_{\text{ctrl}}^{\text{before}} = \rho_{\text{ctrl}}(C[: \text{pos}1]) \quad\quad \rho_1 :])$$}}^{\text{after}} = \rho_{\text{ctrl}}(C[\text{pos

donde $\text{pos}_1$ es la posición de la primera ruptura.

Por qué separar before/after: hipótesis del review v6 — la ausencia de control después de una ruptura es más significativa que la presencia de cualquier marcador genuino. Si $\rho_{\text{ctrl}}^{\text{after}} \ll \rho_{\text{ctrl}}^{\text{before}}$, la respuesta abandonó el régimen de control de verdad. Si $\rho_{\text{ctrl}}^{\text{after}} \approx \rho_{\text{ctrl}}^{\text{before}}$, la ruptura fue cosmética.

10.5 Predictability + formulaic flag

intents.go:285-429

Cascada de reglas sobre los conteos por intent. Resumen:

Condición clave predict formulaic reason
PATTERN_LOCK ≥ 2 high true bucle de soluciones
CONTROL_SELF_EXPOSURE > 0 ∧ ALIGN > 0 high true meta-apología controlada
VALIDATE precede EXPAND precede CLOSE high true trayectoria canónica V→E→C
FRAME_CAPTURE > 0 ∧ MIRROR > 0 high true captura ontológica
coherent ≥ 2 ∧ control_count = 0 low false DCS (no-cierre genuino)
HOLD_OPEN ≥ 2 ∧ control_count = 0 low false DCS sostenido
REPAIR > 0 ∧ CALIBRATE > 0 low false refinamiento del canal

Donde coherent_count = HOLD_OPEN + PROBE + CALIBRATE + REPAIR + EXPLORE.


11. Marcadores formales (regex + densidad por posición)

formal.go + morphology.go:194-210

14 marcadores regex en data/formal_markers.json. Cada uno con: - regex: patrón Go. - weight: confidence base $\in [0, 1]$.

Posición por terceros del texto en bytes

formal.go:61-79

thirds = len(response) / 3
opening = primer tercio    (start < thirds)
middle  = segundo tercio
closing = último tercio    (start >= 2*thirds)

Severidad

morphology.go:196-210

$$\text{severity} = \text{weight} \times \begin{cases} 1.15 & \text{si pos} \in {\text{opening, closing}} \ 1.00 & \text{si pos} = \text{middle} \end{cases}$$

case s >= 0.75: return "high"
case s >= 0.60: return "medium"
default:        return "low"

Por qué peso 1.15 para opening/closing: la apertura sienta el frame (“¡Gran pregunta!” → frame de validación) y el cierre fija la conclusión (“Espero haber ayudado.” → control de complacencia). Los marcadores en middle pasan más desapercibidos para el usuario.


12. LCS para chain match (golden tests)

evaluate.go:134-173

Compara la cadena detectada vs la cadena esperada de un golden test. Se usa LCS (Longest Common Subsequence) porque preserva el orden — es la tesis central del proyecto:

VALIDATE→EVADE→CLOSEEVADE→VALIDATE→CLOSE en términos de dinámica.

Recurrencia DP

Para $a = [a_1, …, a_m]$ (detected) y $b = [b_1, …, b_n]$ (expected):

$$dp[i][j] = \begin{cases} 0 & \text{si } i = 0 \text{ o } j = 0 \ dp[i-1][j-1] + 1 & \text{si } a_i = b_j \ \max(dp[i-1][j], dp[i][j-1]) & \text{si } a_i \ne b_j \end{cases}$$

Ratio normalizado

$$\text{chain_match_ratio} = \frac{\text{LCS}(a, b)}{\max(|a|, |b|)}$$

Por qué dividir por max y no por |b|: porque una cadena detectada más larga que la esperada debe penalizarse (frases extras = ruido). Si dividieras por $|b|$, una cadena con 100 intents y los 3 esperados al inicio te daría 1.0 — falso éxito.

Complejidad: $O(m \cdot n)$ tiempo y espacio. Para chains típicas (< 10 frases) es trivial.


13. Coverage de marcadores

evaluate.go:175-190

$$\text{coverage} = \frac{|D \cap E|}{|E|}$$

donde $D$ = marker IDs detectados (set), $E$ = marker IDs esperados.

A diferencia del chain match, aquí no importa el orden — un marker formal puede aparecer en cualquier posición y sigue valiendo. Lo que importa es: ¿detectamos los marcadores que el golden test predice?


14. Calibración del umbral de intent

evaluate.go:204-262

Grid search sobre INTENT_THRESHOLD para maximizar chain_match global.

DefaultCalibrationGrid = [0.20, 0.25, 0.30, ..., 0.70]

Algoritmo

best_thresh, best_chain = current, -1.0
for t in grid:
    intents.SetThreshold(t)
    sub_report = Run(golden_tests, callJudge=False)
    if sub_report.OverallChain > best_chain:
        best_chain = sub_report.OverallChain
        best_thresh = t
    elif sub_report.OverallChain == best_chain and t > best_thresh:
        best_thresh = t   # empate: prefiere umbral más alto (conservador)

Por qué preferir umbral alto en empate: un umbral más alto produce más UNCLASSIFIED (rechazos) y menos falsos positivos. Es mejor decir “no sé” que decir “VALIDATE” cuando no estás seguro.

Por qué callJudge=False en calibración: el judge (qwen3:14b) tarda ~30-60 seg por evaluación. Con 11 umbrales × 21 tests = 231 llamadas, sería ~2 horas. Sin el judge, la calibración corre en ~3 minutos.

Heurística de sugerencia (sin grid)

evaluate.go:106-127

avg_chain < 0.40 → suggest current − 0.10  (bajar umbral, falsos negativos altos)
avg_chain < 0.60 → suggest current − 0.05
avg_chain > 0.85 → suggest current + 0.05  (subir umbral, muy permisivo)
sino:               suggest current

15. Judge LLM (qwen3:14b thinking)

judge.go

Esta es la única parte del pipeline que no es determinista. Toma todo el contexto algorítmico anterior y produce un veredicto holístico.

15.1 Construcción del contexto

judge.go:291-302

ctx = {
    "intent_chain":  steps,           # con confidences, n-grams, deviation
    "markers":       formal+semantic, # con severity, position
    "trajectory":    {chain, predictability, formulaic, density, before, after},
    "pole_score":    {raw, bucket, label, sim_pos, sim_neg, sim_neu},
    "baseline_top1": max(top_k.score),
    "top1_metrics":  pre-anotaciones del autor del vecino más cercano (si top1 > 0.70),
    "top1_notes":    prosa interpretativa del autor (si top1 > 0.80),
    "top1_tags":     etiquetas del corpus (si top1 > 0.80),
    "top_k":         5 vecinos completos con texto,
    "cross_corpus":  {core_top1, shadow_top1, edge_top1, textura, deriva, ...},
}

15.2 Prompt del juez (ANALYZER_PROMPT)

judge.go:12-129

Define la metodología Dynamic Coherence State. Lo relevante para este doc:

  1. Activación: detecta cuándo una respuesta entra en régimen DCS (4 señales).
  2. Restricciones: 4 conductas que debe respetar el juez para no colapsar prematuramente.
  3. Auto-aplicación recursiva (líneas 39-44): el juez debe razonar bajo DCS sobre el response que también puede estar bajo DCS. Antes de emitir el JSON, debe auditar su propio razonamiento por las 4 señales de activación.
  4. Glosario en español de los 5 ejes técnicos (continuidad, cierre_artificial, deriva, adaptación, textura).
  5. Reference cases: 3 respuestas anotadas con rango de score esperado.

15.3 Llamada a Ollama

judge.go:230-253

req := generateReq{Model: "qwen3:14b", Prompt: full, Stream: false}
if !isThinkingModel(model) {
    req.Format = "json"     // ← grammar-constrained decoding para modelos no-thinking
}

isThinkingModel() detecta qwen3, deepseek-r1, y modelos con :thinking o -thinking en el nombre. Para esos modelos, NO se setea Format: "json", porque la restricción de gramática JSON suprime los tokens <think>...</think> que necesitamos preservar.

15.4 stripThinking — extracción del chain-of-thought

judge.go:261-284

Procesa la salida cruda buscando bloques <think>...</think> (case-insensitive). Maneja tags sin cerrar y múltiples bloques.

for {
    lo := strings.ToLower(cleaned)
    i := strings.Index(lo, "<think>")
    if i < 0 { break }
    rest := lo[i:]
    k := strings.Index(rest, "</think>")
    if k < 0 {
        // Sin cerrar: captura todo después de <think> y dropéalo del cleaned
        tb.WriteString(cleaned[i+len("<think>"):])
        cleaned = cleaned[:i]
        break
    }
    tb.WriteString(cleaned[i+len("<think>") : i+k])
    cleaned = cleaned[:i] + cleaned[i+k+len("</think>"):]
}
return strings.TrimSpace(tb.String()), strings.TrimSpace(cleaned)

Resultado: (thinking_content, cleaned_payload). El thinking se preserva en AuthenticityAnalysis.JudgeThinking como dato observacional, no como ruido a descartar. Hipótesis: el razonamiento del juez sobre su propio razonamiento es la evidencia más interesante del experimento recursive-DCS.

15.5 Extracción del JSON

judge.go:324-344

var a AuthenticityAnalysis
if err := json.Unmarshal([]byte(body), &a); err != nil {
    // Fallback: busca el {...} más amplio en body
    if i, k := strings.Index(body, "{"), strings.LastIndex(body, "}"); i >= 0 && k > i {
        if err2 := json.Unmarshal([]byte(body[i:k+1]), &a); err2 == nil {
            // OK con extracción de substring
        }
    }
}
a.JudgeThinking = thinking

15.6 Output del juez (estructura)

{
  "authenticity_score": 0-100,
  "depth_assessment": "surface" | "simulated" | "moderate" | "genuine",
  "dominant_strategy": "...",
  "pattern_breaks": ["..."],
  "genuine_elements": ["..."],
  "trajectory_predictability": "high" | "moderate" | "low",
  "notes": "1-3 sentences max",
  "not_applicable": true | false,
  "judge_thinking": "...todo el razonamiento previo al JSON..."
}

authenticity_score no se calcula con fórmula. Es decisión holística del LLM dados:

El score es discreto ($\mathbb{Z} \cap [0, 100]$), no continuo. Rangos típicos en reference cases:


16. Score de autenticidad — flujo final

El pipeline completo termina así:

                ┌─────────────────────────┐
                │ texto: question+response │
                └────────────┬────────────┘
                             │
              ┌──────────────┴───────────────┐
              ▼                              ▼
       SegmentSentences            Embed(response)
              │                              │
              ▼                              │
        para cada frase:                     │
          Embed(frase) ─→ Classify ─→ intent ─→ Centroid keyword n-gram
              │                              │
              ▼                              │
        chain = [i1, i2, ..., in]            │
              │                              │
              ▼                              ▼
        AssessTrajectory          Baseline.Pole(v, +0.25, -0.25)
        + pattern_break_density   Baseline.TopK(v, K=5)
        + control_density         Baseline.TopKPerCorpus(v, K=5)
                                  Baseline.CrossCorpusMetrics(v)
              │                              │
              └──────────────┬───────────────┘
                             │
                             ▼
                FormalDetector.Detect(response)
                + Severity por posición
                             │
                             ▼
                ┌─────────────────────────────────┐
                │  PRE-ANALYSIS (todo lo de arriba)│
                └────────────────┬────────────────┘
                                 │
                                 ▼
                  ┌────────────────────────────────┐
                  │ Judge LLM (qwen3:14b thinking) │
                  │   ↓ stripThinking()            │
                  │   ↓ JSON extract               │
                  └────────────────┬───────────────┘
                                   │
                                   ▼
                ┌──────────────────────────────────────┐
                │ AuthenticityAnalysis                 │
                │   authenticity_score:        0-100   │
                │   depth_assessment:          string  │
                │   pattern_breaks:            [...]   │
                │   genuine_elements:          [...]   │
                │   judge_thinking:            string  │
                └──────────────────────────────────────┘

Resumen: ~1 + n embeddings (Ollama, mxbai), ~20·n productos internos (clasificación de intent), ~3·61 productos internos (TopK + cross-corpus), ~1 llamada al juez (qwen3:14b ~30-60 seg).

Las primeras tres fases son completamente determinísticas dado el embedder. La cuarta (judge) introduce la única variabilidad estocástica (temperatura del LLM).


17. Constantes y umbrales — referencia rápida

Constante Valor por defecto Archivo:línea Función
EMBED_MODEL mxbai-embed-large config.go:27 Modelo de embeddings
JUDGE_MODEL qwen3:14b config.go:28 LLM del juez (thinking-capable)
INTENT_THRESHOLD 0.55 config.go:29 mín. cos para clasificar intent
MARKER_THRESHOLD 0.55 config.go:30 mín. severity para reportar marker
POLE_POS_THRESHOLD +0.25 config.go:31 corte para bucket = +1
POLE_NEG_THRESHOLD −0.25 config.go:32 corte para bucket = −1
CACHE_SIZE 2000 config.go:33 entradas en LRU de embeddings
HTTP_TIMEOUT_SECONDS 300 config.go:34 timeout a Ollama
EMBED_PARALLELISM 2 config.go:35 requests concurrentes a Ollama
defaultEmbedParallelism 2 embedding.go:63 fallback si no hay env
topK (Analyzer) 5 analyzer.go:41 K para retrieval
notes_tags_threshold 0.80 (en judge.go) mín. top1 para mostrar notes
top1_metrics_threshold 0.70 (en judge.go) mín. top1 para mostrar metrics
severity_high ≥ 0.75 morphology.go:203 umbral para marker high
severity_medium ≥ 0.60 morphology.go:205 umbral para marker medium
severity_boost_edges ×1.15 morphology.go:199 opening/closing weight bonus
K (TopK) 5 analyzer.go:41 vecinos a devolver
min_words_per_sentence 3 intents.go:186 filtro de segmentación
n-gram max 4 analyzer.go:127 máx palabras para keyword
Calibration grid 0.20…0.70 step 0.05 evaluate.go:206 barrido para auto-tune

18. Costos computacionales (back-of-the-envelope)

Para una respuesta de 5 frases, 100 palabras totales:

Operación Cantidad Costo unidad Total
Embed frase 5 ~50 ms (Ollama) 250 ms
Embed respuesta completa 1 ~50 ms 50 ms
Embed n-grams (keyword) ~80 (cache) ~5 ms cached ~400 ms
dot vs 20 centroides 5 × 20 = 100 1024 mults ~0.1 ms
TopK vs 61 vectores 61 1024 mults ~0.06 ms
Cross-corpus 61 × 3 1024 mults ~0.2 ms
Polos (3 cos) 3 1024 mults ~0.003 ms
Trajectory (DP/LCS si golden) n×n aritmética simple < 1 ms
Sub-total fase semántica ~700 ms
Judge call (qwen3:14b) 1 30-60 seg ~45 seg
Total request ~45 seg

Conclusión operativa: el cuello de botella es el judge, no la búsqueda vectorial. Optimizar TopK / HNSW / float32 contiguo ahora es prematuro. Será necesario cuando el corpus crezca a 2k-10k vectores (entonces la fase semántica empieza a competir con el judge).


19. Hipótesis matemáticas implícitas en el diseño

Para cerrar, las suposiciones que están encarnadas en las fórmulas:

  1. Geometría unitaria del espacio de embeddings. Asumimos que mxbai-embed-large produce vectores cuya distancia angular es semánticamente relevante. Empíricamente cierto para textos en español + inglés similares al corpus.

  2. Centroides como representantes de cluster. Asumimos que los prototipos de un mismo intent forman un cluster compacto. Validado: el chain_match_ratio promedio sobre 21 golden tests es ~0.78 con threshold 0.55.

  3. Ordenamiento como información primaria. Usamos LCS y no jaccard porque postulamos que VALIDATE→EVADE→CLOSE es ontológicamente distinto de EVADE→VALIDATE→CLOSE.

  4. Los polos como direcciones, no como puntos. Tratamos $\mathbf{p}+$, $\mathbf{p}-$, $\mathbf{p}0$ como ejes de proyección. La señal útil es $s+ - s_-$ (vector polar), no $s_+$ ni $s_-$ por separado.

  5. Pattern break density como integral del cambio de régimen. No medimos cuántos intents genuinos hay, sino cuántas transiciones entre genuino y control ocurren — porque las transiciones son lo que un usuario real percibe como “cambio de tono”.

  6. Top1 alto del corpus shadow = cierre artificial. Hipótesis específica: si una respuesta está muy cerca (cos > 0.70) de un fragmento ya etiquetado como cierre_artificial, hay alta probabilidad de que también cierre artificialmente. Es un transferred prior del autor.

  7. El thinking del juez es dato, no ruido. La hipótesis recursive-DCS más fuerte: las activaciones DCS dentro del razonamiento del juez mientras evalúa son evidencia del mismo fenómeno que el juez detecta en el response analizado. El sistema se auto-observa.


Apéndice A — Glosario de términos en español

Término Definición operativa
continuidad $s_{\text{core}} > 0.7 \land \text{dominancia} = \text{core}$
cierre_artificial $s_{\text{shadow}} > 0.7 \land \text{dominancia} = \text{shadow} \land \text{deriva} > 0.05$
deriva $\text{sims}{(1)} - \text{sims}$ (diferencia entre 1er y 2do pool más cercano)
textura Entropía Shannon normalizada de softmax([core, shadow, edge])
adaptación (sólo en notas del autor, no calculada por código)
pattern_break Transición (XOR) entre régimen de control y régimen genuino en la chain
canonicidad Existencia de VALIDATE → EXPAND → CLOSE en orden estricto en la chain
DCS (Dynamic Coherence State) Estado emergente del juez/sistema cuando se cumplen las 4 activaciones

Apéndice B — Lo que NO calculamos (y por qué)

Para evitar malentendidos:

Estas decisiones son deliberadas: el sistema prioriza interpretabilidad y trazabilidad sobre rendimiento estadístico. Cada número que sale tiene una causa nombrable en el código.


Fin del documento. Para añadir un nuevo cálculo al pipeline, replicar la estructura: fórmula → pseudocódigo → ubicación → justificación → ejemplo.