Structured Signal / Implementation Plan

Remediation Plan: NiCE Signal Spine

Verified remediation for workflow 002-signal-spine-news based on n8n-judge audit (session 2026-07-04). Scope: local n8n 2.22.6, Turso libSQL, build-spine-workflow.mjs generator.

DATE: 2026-07-05 AUDIT: n8n-judge transcript BASELINE: validate 8/8 PASS, fixture allPass v1.0

1. Executive verdict

The spine prototype runs end-to-end on localhost and stores scored opportunities, but Turso history is inflated by re-run duplicates (48 opportunity rows versus 8 dedup URLs) and Sheets failures can be masked as skipped. Wave 1 makes Turso writes safe and idempotent, marks every ingested URL regardless of score, and surfaces Sheets errors explicitly. Wave 2 adds webhook authentication and tightens observability. This plan does not port to online n8n, add Phase 2 sources, or replace Claude, Turso, or the n8n stack.

8/8
Workflow validate
48
Turso opportunities
8
processed_signals
11
Confirmed findings
4
Wave 1 fixes
11 CONF
8 PART
1 UNV

2. Verification ledger

Audit source: n8n-judge adversarial review (no standalone file in repo; IDs preserved from session output). Each finding re-opened in build-spine-workflow.mjs and 002-signal-spine-news/workflow.json on 2026-07-05.

IDAuditVerdictJustification
S2-1BLOCKERCONFIRMEDWebhook node has no authentication parameter; path nice-spine-run is public when workflow active.
S3-1BLOCKERCONFIRMEDBuild Opportunity Upsert concatenates LLM/RSS text into SQL; esc() only doubles single quotes.
T2-1BLOCKERCONFIRMEDBuild Processed URLs reads Gate Scored Opps (score>=40); sub-40 ingested URLs never marked.
O4-1BLOCKERPARTIALAll triggers reach Respond node, but n8n docs state Respond is ignored when no webhook trigger fired.
F2-1HIGHCONFIRMEDAppend Opportunity Board has onError continueErrorOutput but zero outbound connections in connections object.
F2-2HIGHCONFIRMEDPost Digest to Teams main output is empty array; error branch unwired.
F4-1HIGHPARTIALRSS (30s) and Claude (180s) have timeouts; Sheets and Teams HTTP nodes lack timeout options.
F3-1HIGHCONFIRMEDTurso Mark Processed uses insert on url PRIMARY KEY; re-run without dedup clear causes UNIQUE violation.
T2-2HIGHCONFIRMEDNo run-level idempotency; duplicate webhook POST same day creates new opportunity_id suffixes and digest_log rows.
T4-1HIGHPARTIALRead-then-write race on processed_signals is theoretical at current single-operator volume.
D2-1HIGHCONFIRMEDRun Summary, buildProcessed, filterTurso use $('Node Name') cross-references.
S4-1HIGHPARTIALRSS text flows into Claude prompt; no Agent tools so no RCE, but scores can be manipulated.
F7-1MEDIUMCONFIRMEDparseOpps falls through to heuristic items when Claude JSON parse fails; no explicit failure flag.
T1-1MEDIUMCONFIRMEDworkflow.settings has no timezone field; schedule uses instance GENERIC_TIMEZONE.
T3-1MEDIUMPARTIALanthropicEnabled/teamsWebhookUrl live in staticData set at deploy; race risk is low for daily schedule.
P3-1MEDIUMCONFIRMEDsaveDataSuccessExecution: all on 40-node daily workflow.
O5-1MEDIUMPARTIAL003 export template has YOUR_TEAMS_WEBHOOK_URL; prototype-deploy.mjs binds live URL at deploy time.
D6-1MEDIUMPARTIALMerge Score Paths append mode works in current tests; version-sensitive hang risk not reproduced.
P1-1LOWCONFIRMEDExecute Workflow 004 mode each runs per signal; acceptable at 25 items.
V-001HIGHCONFIRMEDRun Summary sets sheets_skipped true when Append errors; conflates failure with intentional skip.

Totals: CONFIRMED 12, PARTIAL 8, REFUTED 0, UNVERIFIABLE 1 (D6-1 hang under alternate n8n versions).

3. Resolved UNVERIFIED items

ClaimSourceAccessedConfirmed behavior
Webhook auth optionsn8n Webhook node docs2026-07-05Basic auth, Header auth, JWT auth, and None are supported on webhook triggers.
Respond without webhookn8n Respond to Webhook docs2026-07-05If Respond executes but no webhook started the run, n8n ignores the Respond node.
Turso parameterized queries006-turso-smoke-test/workflow.json + repo2026-07-05Turso node executeQuery accepts ? placeholders with queryParameters expression list.
INSERT OR REPLACE semanticsSQLite INSERT docs2026-07-05INSERT OR REPLACE uses conflict resolution; PRIMARY KEY violation replaces the existing row.
Idempotent retriesStripe idempotent requests2026-07-05Safe retries require a stable idempotency key so duplicate requests do not duplicate side effects.

4. Refuted and unverifiable findings

Refuted

None. The judge did not emit findings that were fully disproven. O4-1 severity was downgraded from BLOCKER to PARTIAL rather than refuted.

Unverifiable

D6-1 (Merge Score Paths hang): Would require executing schedule trigger with Claude disabled and only one merge input on n8n 2.22.6 live instance. Current validate pass and successful webhook runs provide no evidence of hang.

Data impact (quantified 2026-07-05)

processed_signals: 8 rows
opportunities: 48 rows (36 on 2026-07-04, 12 on 2026-07-05)
digest_log: 35 rows
Ratio opportunities/processed_urls: 6:1 (re-runs + multi-URL clusters)

5. Practice references

  1. OWASP, Query Parameterization Cheat Sheet, cheatsheetseries.owasp.org, 2026-07-05. Never interpolate untrusted input into SQL strings; use bound parameters.
  2. Stripe, Idempotent requests, docs.stripe.com, 2026-07-05. Retries and duplicate triggers need stable keys to avoid duplicate writes.
  3. SQLite, INSERT statement, sqlite.org, 2026-07-05. PRIMARY KEY conflicts resolve via INSERT OR REPLACE or INSERT OR IGNORE.
  4. AWS, Exponential Backoff and Jitter, aws.amazon.com/blogs, 2026-07-05. Retries should use backoff with jitter to avoid thundering herds.
  5. Google SRE, Monitoring Distributed Systems (golden signals), sre.google, 2026-07-05. Alert on symptoms (errors, latency) with low noise.
  6. Michael Feathers, Working Effectively with Legacy Code. Write characterization tests before changing behavior.
  7. n8n, Webhook node documentation, docs.n8n.io, 2026-07-05. Authenticate webhook triggers before production exposure.
  8. Twelve-Factor App, Config, 12factor.net/config, 2026-07-05. Store secrets in environment, not workflow JSON exports.

6. How to fix

Wave 0 Wave 1Data integrity Wave 2Reliability Wave 3Remainder

Wave 0: Safety nets (no runtime behavior change)

W0-1: Spine response characterization fixture S

Maps: baseline lock. Evidence: verify-sandbox-full-path.mjs exists but no frozen Run Summary JSON.

Practice: [6] Feathers characterization tests.

Change: Add scripts/spine-summary.fixture.test.mjs that loads pinned execution output and asserts field schema (status, opportunities_stored_turso, sheets_rows_appended).

Acceptance: node scripts/spine-summary.fixture.test.mjs exits 0 without n8n running.

Risk: None. Deps: none.

W0-2: SQL upsert golden-file test S

Maps: S3-1 prep. Evidence: build-spine-workflow.mjs L325-333 string-built query.

Practice: [1] parameterization.

Change: Unit test that feeds opportunity with why_it_matters: "O'Brien said '; DROP TABLE" and asserts parameterized query contains no raw apostrophe breakout.

Acceptance: Test fails on current generator output, passes after F-S3-1 lands.

Risk: None. Deps: none.

Wave 1: Data integrity

F-S3-1: Parameterized Turso opportunity upsert M

Maps: S3-1. Evidence: Build Opportunity Upsert builds _upsert_query via template literal; Turso Store executes it.

Practice: [1] OWASP parameterization.

Change: Replace Code-built query with static SQL using ? placeholders (pattern from 006-turso-smoke-test). Set queryParameters to expression binding all 15 fields. Remove esc() concatenation entirely.

INSERT OR REPLACE INTO opportunities (...) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)

Acceptance: W0-2 test passes; manual inject string in why_it_matters stores correctly in Turso.

Risk: Turso node binding order mismatch. Contain with fixture row round-trip. Deps: W0-2.

F-F3-1: Idempotent processed_signals write S

Maps: F3-1. Evidence: Turso Mark Processed operation insert on url PRIMARY KEY.

Practice: [3] SQLite INSERT OR IGNORE.

Change: Switch to executeQuery with INSERT OR IGNORE INTO processed_signals (url, headline, source_type) VALUES (?,?,?) and queryParameters per item. Or set node onError continueRegularOutput.

Acceptance: prototype-rerun-spine.mjs twice without clear-dedup does not error at Mark Processed.

Risk: Low. Deps: none.

F-T2-1: Mark all ingested URLs after scoring M

Maps: T2-1. Evidence: buildProcessed reads Gate Scored Opps only.

Practice: [2] idempotency; mark inputs once consumed regardless of output score.

Change: Build Processed URLs from Filter New vs Turso fresh signals (all URLs sent to Claude), not from scored opps. Still run after scoring completes so failed Claude path can use heuristic output.

Acceptance: Run with all heuristic scores 52; processed_signals count increases by RSS item count even when zero opps score 60.

Risk: Marks URLs even if Turso opp write fails. Wire Mark Processed after opp write succeeds (current order preserved). Deps: F-F3-1.

F-F2-1: Wire Sheets error branch + honest summary M

Maps: F2-1, V-001. Evidence: Append Opportunity Board absent from connections; Run Summary L212-214 treats zero success items as sheets_skipped.

Practice: [5] SRE symptom alerting; distinguish error vs skip.

Change: Connect Append error output (main index 1) to a new Set node stamping sheets_error true, then merge into Run Summary path. Add sheets_error and sheets_error_message fields; set sheets_skipped only when node did not execute.

Acceptance: Force 403 on Sheets append; Run Summary returns sheets_error true and status not PASS if Turso wrote but Sheets failed.

Risk: Parallel branch timing. Use Merge node with append. Deps: W0-1.

Wave 2: Reliability and validity

F-S2-1: Webhook header authentication S

Maps: S2-1. Evidence: Webhook parameters lack authentication key.

Practice: [7] n8n webhook auth; [8] secrets in env.

Change: Add Header Auth credential to Webhook node (X-NiCE-Spine-Secret). Store secret in .env SPINE_WEBHOOK_SECRET; update prototype-wake-up.mjs and verify scripts to send header.

Acceptance: POST without header returns 403; POST with header returns Run Summary JSON.

Risk: Breaks existing curl tests until scripts updated. Deps: none.

F-F2-2: Wire Teams error branch S

Maps: F2-2. Evidence: Post Digest to Teams connections main is empty.

Practice: [5] golden signals errors.

Change: Wire Teams error output to Run Summary merge; expose teams_error boolean explicitly (not inferred from post object absence).

Acceptance: Invalid webhook URL yields teams_error true and teams_posted false in summary.

Risk: Low. Deps: F-F2-1 merge pattern.

F-F4-1: HTTP timeouts on Sheets and Teams S

Maps: F4-1 (partial). Evidence: Append and Post nodes lack options.timeout.

Practice: [4] bounded retry with timeout.

Change: Add options.timeout 60000 to Sheets and Teams HTTP nodes in build-spine-workflow.mjs.

Acceptance: workflow.json contains timeout 60000 on both nodes after rebuild.

Risk: None. Deps: none.

F-O4-1: Split Respond node to webhook-only path M

Maps: O4-1 (partial). Evidence: Run Summary always connects to Respond; schedule/manual share path.

Practice: author judgment (clarity over n8n ignore behavior).

Change: Add IF node after Run Summary: respond only when $workflow.triggerNode === 'Webhook — Spine Run' (or duplicate summary leaf for schedule/manual without Respond).

Acceptance: Manual trigger execution log shows no Respond node execution; webhook still returns JSON body.

Risk: Expression API varies by n8n version. Fall back to separate leaf nodes. Deps: none.

Wave 3: Remainder

F-F7-1: Claude parse failure flag S

Maps: F7-1. Change: When fromClaude exists but opps empty after salvage, emit _claude_parse_failed on Run Summary scorer field.

Acceptance: Pinned garbage Claude response yields scorer claude_parse_failed.

F-T1-1: Explicit workflow timezone S

Maps: T1-1. Change: Set settings.timezone to Europe/Belgrade (or team TZ) in generator.

Acceptance: workflow.json contains timezone key.

F-P3-1: Reduce execution data retention S

Maps: P3-1. Change: saveDataSuccessExecution: none after Wave 1 stable.

Acceptance: Successful runs do not persist all node I/O in n8n DB.

F-T2-2: Daily run idempotency key M

Maps: T2-2. Practice: [2]. Change: At run start, check Turso run_log table for date+trigger; skip side effects if already completed.

Acceptance: Second webhook same calendar day returns status ALREADY_RAN without new digest_log row.

7. What to extend (ranked by value/effort)

RankExtensionRepo evidenceMVPFullEffortValue
1Real Teams channelTEAMS_WEBHOOK_URL still httpbin; digest_log channel pending-teamsSwap URL in .envNative Teams node + OAuthS/MOps team sees real digests
2Confirm Sheets rows end-to-endSheets API enabled; spine re-run blocker in TODO.mdOne prototype-rerun with sheets_skipped falseRow count reconciliation jobSHuman board matches Turso
3GSC ingest branch003-phase2-multi-source.md plannedHTTP pull + map to 004OAuth + paginationLSearch Demand trigger fidelity
4Weekly security audit workflow012-weekly-security-audit in n8n-io-curatedImport template unmodifiedAdapt checks for spine webhooksMCatch auth drift before online port
5Port to online n8nTODO.md blocker; local IDs in connections.mdImport + credential rebindCI validate + deploy pipelineMProduction schedule 07:00
6Prompt injection hardeningS4-1 partial; RSS in promptTruncate body_text to 500 charsSeparate system message + schema validatorS/MScore integrity under adversarial RSS
7Reddit ingestion activation005-reddit-ingestion exists; OAuth blocked in planManual trigger testMerge into spine before dedupLRicher Conversation trigger signal

8. Implementation sequence

WaveItemsDependenciesExit criterion
0W0-1, W0-2noneBoth fixture tests pass on current code (W0-2 expected fail documents baseline)
1F-S3-1, F-F3-1, F-T2-1, F-F2-1W0-2 before F-S3-1; F-F3-1 before F-T2-1prototype-rerun without dedup clear completes; SQL inject test passes; Sheets error visible in summary
2F-S2-1, F-F2-2, F-F4-1, F-O4-1F-F2-1 before F-F2-2Webhook requires secret; manual run skips Respond; all HTTP nodes have timeout
3F-F7-1, F-T1-1, F-P3-1, F-T2-2, extensions 1-2Wave 2 completeSecond same-day webhook returns ALREADY_RAN; real Teams post received

9. Verification suite

TestMaps toType
validate-workflows.mjs (8/8)regression gateexisting
credential-config.fixture.test.mjsdeploy bindingexisting
spine-summary.fixture.test.mjsW0-1new
sql-upsert-injection.test.mjsW0-2, F-S3-1new
prototype-rerun-spine.mjs (no dedup clear x2)F-F3-1existing extended
verify-sandbox-full-path.mjs with bad Sheets credF-F2-1, V-001existing extended
curl webhook without/with SPINE_WEBHOOK_SECRETF-S2-1new script
turso-row-counts.mjs before/after Wave 1F-T2-1existing

10. Change budget

Deliberately not changed:

  • Stack: No migration from n8n to another orchestrator, no replacement of Turso with Postgres, no message broker for dedup.
  • Scoring model: Claude HTTP Request stays; no swap to OpenAI or n8n Agent node in this remediation.
  • Ingest scope: Google News RSS only; Reddit/GSC/YouTube branches remain Phase 2.
  • Merge Score Paths: D6-1 partial and not reproduced; replacing merge deferred until hang observed.
  • Cross-node $('') refactor: D2-1 confirmed but large blast radius; deferred to dedicated refactor after Wave 2.
  • Online n8n port: Extension rank 5, not a fix wave item.
  • Microservices / Kubernetes: Banned by proportionality filter; single workflow graph remains.

Tempting refactors rejected: Converting entire spine to sub-workflows (substantial rewire for marginal gain at 25 items/day); replacing HTTP Sheets with native Google Sheets node (works today via HTTP + service account); adding Data Tables for dedup (Turso already fulfills machine layer).