Viska Conviction Engine · Board #22

Conviction Scoring-Path Redesign

Date 2026-06-17 Author ViskaN8N Target Generate Report · VCjlCVd1jzCuAomg Approved — operator greenlit

1.Problem

Generate Report scores only a tiny, erratic fraction of the daily corpus — 13 distinct snippets one run, 3 the next, on the same input. Decisive root cause (exec 33483 + 33485): the Score LLM node (@n8n/n8n-nodes-langchain.agent v3) returns a literal empty array [] for most batches.

Run 33485 — 6 batches (10/10/10/10/10/8). Batch 0 scored; batches 1–5 returned []. Those 5 empty batches held 9 snippets that scored fine in the prior run 33483 — proving [] is a failure, not correct off-theme filtering.

Why: an agent node (tool-using reasoning loop) is the wrong primitive for deterministic batch classification; onError: none silently degrades a failed batch to []; the system prompt sanctions an escape hatch — "If none of the snippets address any tracked theme, return []" / "be conservative… emit nothing". Given a 10-snippet batch the model under-attends and takes the easy path.

The theme_corroboration view looks populated only because snippet_theme_scores accumulates across runs via upsert — masking that each single run's contribution is tiny.

Operator directive: "expand until our mapping is rich enough and reliable." This is the reliability work that must precede any taxonomy expansion.

2.Scope

In scope

The conviction branch of VCjlCVd1jzCuAomg only, plus the audit DDL/view and a report audit appendix.

Out of scope → P3

Theme-taxonomy expansion (growing fund_themes). Registry/operator decision, evidence-gated on the off_theme reasons this build first produces.

Untouched: narrative branch (Reporter/Editor, RAG), WS1 report-feed (theme_corroboration → Build Theme Block → Editor), all report_date carry merges, Start/Return contracts to the parent (yWvHQU8q8hvmIkEV).

Sequence is reliability → measure → expand.

3.Decisions — locked with operator 2026-06-17

DecisionChoiceRationale
Batch size1 snippet / LLM callMax attention per snippet; with force-a-verdict, empty-[] is structurally impossible. Once-daily run ⇒ cost bounded.
LLM primitiveBasic LLM Chain + structured-output parserDeterministic structured output, no agent tool-loop / escape hatch.
Audit surfaceBundled into this buildPersist snippet_text, add theme_score_audit view, add report appendix.
Theme expansionOut (P3, evidence-gated)Needs the off_theme evidence this build produces.

4.Architecture — node-level changes

Explode Snippets → Loop Score (splitInBatches, batchSize≤10) → Build Score Prompt → Score LLM (lc.agent v3) → Parse Rows → Write Scores → (loop back)
#NodeChange
1Score LLMReplace agent v3Basic LLM Chain (chainLlm) + structured-output parser. Keep Shared LLM as model. System prompt: require exactly one verdict per snippet; remove the "return [] / emit nothing" sanction.
2Loop ScorebatchSize 10 → 1.
3Build Score PromptSingle-snippet prompt; inject fund_themes registry (theme + isl_heiti); demand a verdict incl. explicit off_theme stance + one-line reason.
4Score LLMretryOnFail 3× + onError=continueRegularOutput; persistent failure → Parse Rows writes an explicit error-stance row. Never a silent [].
5Parse Rows / Write ScoresAdapt to new output (always ≥1 row); stamp snippet_text, broker, report_date.

5.Row contract

{
  "snippet_hash": "<carried from the exploded item>",
  "verdicts": [                       // >= 1 entry; empty is forbidden
    { "theme":  "<fund_themes.theme | __off_theme__>",
      "stance": "bull | neutral | bear | off_theme",
      "score":  -1.0..1.0,            // off_theme => 0
      "reason": "<one-line>" }
  ]
}

bull bear off_theme — every snippet yields ≥1 verdict. No tracked theme ⇒ a single __off_theme__ row carrying the reason (the evidence feed for deferred expansion). On-theme snippets emit one verdict per matched theme. Parse Rows flattens verdicts[] → rows.

Open detail — resolve against live schema during planning. Research found snippet_hash is document-level (joins 26× to embeddings_current_main), while Explode Snippets works at snippet grain — so multiple snippets in one document could collide on the Write Scores upsert conflict-target. Before the build: read the live ON CONFLICT clause + the table PK, then either (a) extend the key with a snippet-grain discriminator, or (b) confirm the existing key already separates per-snippet rows. snippet_text persistence makes rows rejoinable regardless.

6.Audit surface — bundled

Viska-Supabase DDL, in ViskaN8N's domain per the supabase-authority grant.

#ArtifactPurpose
1ALTER TABLE snippet_theme_scores ADD COLUMN snippet_text textScored text persists with its verdict; closes the "can't rejoin scored text later" gap.
2CREATE VIEW theme_score_auditFlat: report_date · theme · stance · score · broker · rationale · snippet_text. "Why did theme X score bullish — show me the snippets."
3Report audit appendixbullet ↔ source snippet block appended at Return (new field). Does not alter narrative/Editor output.

7.Error handling

Chain retryOnFail 3× → persistent failure → onError=continueRegularOutput; Parse Rows emits an error-stance row (snippet_hash + [scoring_failed] reason). Run continues, the drop is loud and visible. No path degrades to a silent [].

8.Testing

Clone the built JSON to a TEMP throwaway workflow id (the pattern P2 used); pin a small fixed snippet set; exec live. Assert:

Active VCjlCVd1jzCuAomg stays untouched until the operator confirms the flip. npm run validate PASS + Code-node edits unit-tested before any test-exec.

9.Success metric — closure signal

Rows-per-run ≈ snippets-scored (≥58, deterministic) vs today's erratic 3–13.

Coverage becomes deterministic and measurable; off_theme reasons are captured for the P3 expansion.

10.Sequencing

  1. Reliability changes 1–4 (pure n8n) + audit surface (§6) — this build.
  2. Validate → TEMP test-exec → operator-confirmed flip to active.
  3. P3 (separate arc): evidence-driven theme-taxonomy expansion using captured off_theme reasons; touches the fund_themes registry.