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.
VCjlCVd1jzCuAomg only, plus the audit DDL/view and a
report audit appendix.
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.
| Decision | Choice | Rationale |
|---|---|---|
| Batch size | 1 snippet / LLM call | Max attention per snippet; with force-a-verdict, empty-[] is structurally impossible. Once-daily run ⇒ cost bounded. |
| LLM primitive | Basic LLM Chain + structured-output parser | Deterministic structured output, no agent tool-loop / escape hatch. |
| Audit surface | Bundled into this build | Persist snippet_text, add theme_score_audit view, add report appendix. |
| Theme expansion | Out (P3, evidence-gated) | Needs the off_theme evidence this build produces. |
| # | Node | Change |
|---|---|---|
| 1 | Score LLM | Replace agent v3 → Basic 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. |
| 2 | Loop Score | batchSize 10 → 1. |
| 3 | Build Score Prompt | Single-snippet prompt; inject fund_themes registry (theme + isl_heiti); demand a verdict incl. explicit off_theme stance + one-line reason. |
| 4 | Score LLM | retryOnFail 3× + onError=continueRegularOutput; persistent failure → Parse Rows writes an explicit error-stance row. Never a silent []. |
| 5 | Parse Rows / Write Scores | Adapt to new output (always ≥1 row); stamp snippet_text, broker, report_date. |
{
"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.
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.
Viska-Supabase DDL, in ViskaN8N's domain per the supabase-authority grant.
| # | Artifact | Purpose |
|---|---|---|
| 1 | ALTER TABLE snippet_theme_scores ADD COLUMN snippet_text text | Scored text persists with its verdict; closes the "can't rejoin scored text later" gap. |
| 2 | CREATE VIEW theme_score_audit | Flat: report_date · theme · stance · score · broker · rationale · snippet_text. "Why did theme X score bullish — show me the snippets." |
| 3 | Report audit appendix | bullet ↔ source snippet block appended at Return (new field). Does not alter narrative/Editor output. |
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 [].
Clone the built JSON to a TEMP throwaway workflow id (the pattern P2 used); pin a small fixed snippet set; exec live. Assert:
off_theme rows presenttheme_score_audit populatedActive VCjlCVd1jzCuAomg stays untouched until the operator confirms the flip.
npm run validate PASS + Code-node edits unit-tested before any test-exec.
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.
off_theme reasons; touches the fund_themes registry.