r17: runtime/ eliminado (fuente unica entity) + docs de sesion r16/r17 #34

Merged
pattern merged 5 commits from r16-r9-gemini into main 2026-11-02 06:37:52 +00:00
2 changed files with 73 additions and 9 deletions
Showing only changes of commit 6a16c64f45 - Show all commits
@@ -0,0 +1,25 @@
# Ronda 16 — Verificación (OpenCode/GLM en m1, operador patternn)
## Hallazgos
- EXPOSICION previa: secretos en juego (nombres, nunca valores): core .env → ZAI_API_KEY, PGURL; mail .env → MAIL_PASS, GITEA_TOKEN. Rotación PENDIENTE (decisión del operador). Token de Gitea también visible en `git remote get-url origin` de cayce-ops (remote con credencial embebida).
- VOZ: main y despliegue = ff2cd822 (v2 vieja). PR #23 (voz v3) OPEN, sin merge. identity_path real verificado en outbound_drafts.
- PUBLICADO 48h: 4 piezas nostr con event id (2 verificables en relays: 99cc75f4, 449a39bf; 8a81c688 y 09eb4e9a NO están en relays configurados) + hardcap article_note/backlog. La opinion 04:01 (BTCPay) tenia fuente de WPR (planta de gas Wisconsin): mala atribución real.
- actions_log de core.mjs --posts registra "ok" aunque el guard rechace (falso ok). Bug abierto.
## Cambios (desplegados y con tests 27/27)
- llm.mjs (core + copia cayce-nostr): SOLO Gemini, fuera zai/glm-4.6/4.7. La copia del daemon apuntaba a /srv/hardcap-backend/.env inexistente (sin fallback). Daemon cayce-nostr reiniciado (guard/llm nuevos activos).
- guard.mjs R9 reescrito: terminos ESPECIFICOS de la fuente (slug del titular + sourceTitle: nombres propios, cifras ≥3 dígitos, keywords ≥5 chars con stopwords), ids opacos excluidos. Fuera vocabulario DOMAIN genérico. guardCheck acepta sourceTitle; outbox lo pasa desde meta.
- Tests: R16a (caso real BTCPay/Wisconsin → rechazado), R16b (id opaco no bloquea), R16c (sourceTitle verifica). 27/27 verde.
- Prueba en vivo: pieza generada (BTCPay) con fuente theblock.co/etf-binance → RECHAZADA por R9 en tiempo real.
## Timing ciclo posts (Gemini-only)
- selectSource 61ms · selectBatch 2ms · fetch artículos 333ms · gemini 10.0s · guard 4ms = 10.4s/post (instrumentado).
- Run systemd real: opinion 50.3s (start 04:08:27.257 → spacing 04:09:17.528), rechazado por tope 3/3 + aforismo.
- Ciclo --posts completo = 3 posts + 2 esperas de 45 MIN por diseño (regla supervisor) ≈ 92 min. El objetivo <5 min aplica al procesamiento (✓ 10-50s/post), no al ciclo con espaciado.
## Pendientes
- Rotar ZAI_API_KEY, PGURL, MAIL_PASS, GITEA_TOKEN (y token embebido en remote de cayce-ops).
- Merges Gitea: #23 (voz v3), #25.
- Decidir qué hacer con /srv/cayce-entity (copia muerta: sin servicio, guard sin R9, mtime 21:08 Nov1).
- Bug falso-ok en core.mjs --posts (no chequea resultado del gate).
- cayce-promo y cayce-cycle-monitor en failed.
+48 -9
View File
@@ -130,16 +130,55 @@ async function r5_caps(pool, channel, type, out) {
} }
// R9 (ronda-14): la pieza debe compartir terminos con su fuente // R9 (ronda-14 → ronda-16): la pieza debe compartir TERMINOS ESPECIFICOS de su
function r9_atribucion(full, reasons) { // fuente: nombres propios, cifras y palabras clave del titular (slug del URL y,
// si se pasa, sourceTitle). El vocabulario generico ("wallet", "chain") no
// verifica nada: un texto de BTCPay con fuente de una planta de gas en
// Wisconsin paso el guard el 2026-11-02 porque el cuerpo tenia palabras de
// dominio genericas. Ahora el cuerpo debe contener al menos un termino
// distintivo DE ESA FUENTE; si no, se rechaza.
const R9_STOP = new Set(['https', 'http', 'www', 'html', 'php', 'asp', 'com', 'net', 'org', 'co', 'io', 'gov', 'edu', 'news', 'article', 'articles', 'story', 'stories', 'post', 'posts', 'blog', 'index', 'page', 'pages', 'url', 'link', 'video', 'watch', 'pdf', 'summary', 'summaries', 'en', 'es', 'the', 'and', 'for', 'with', 'from', 'this', 'that', 'what', 'when', 'where', 'which', 'while', 'would', 'could', 'should', 'about', 'after', 'before', 'over', 'under', 'between', 'through', 'during', 'their', 'there', 'these', 'those', 'other', 'more', 'most', 'than', 'then', 'they', 'them', 'were', 'have', 'having', 'been', 'being', 'into', 'onto', 'upon', 'says', 'said', 'amid', 'after', 'because', 'still', 'just', 'also', 'first', 'last'])
function r9_sourceTerms(url, title) {
const terms = new Set()
// palabras clave del slug del URL (derivadas del titular): se descartan
// dominio, stopwords y ids opacos (letras+digitos, largos: no son titulares)
const slugWords = []
for (const part of String(url || '').split(/[/?#]/).slice(3)) {
for (const w of part.split(/[-_.+]+/)) {
const t = w.toLowerCase()
if (!t || t.length < 5 || R9_STOP.has(t) || /^(19|20)\d{2}$/.test(t)) continue
if (/\d/.test(t) && /[a-z]/.test(t) && t.length >= 10) continue // id opaco
slugWords.push(t)
}
}
// cifras distintivas (>=3 digitos): del titular o del slug ya filtrado —
// nunca de dentro de un id opaco (sus digitos no son del titular)
for (const m of `${title || ''} ${slugWords.join(' ')}`.matchAll(/\d[\d.,]*/g)) {
const raw = m[0].replace(/[.,]/g, '')
if (raw.length >= 3) terms.add(raw)
}
for (const t of slugWords) terms.add(t)
// nombres propios y palabras del titular (si el ciclo lo pasa como sourceTitle)
for (const w of String(title || '').split(/[^A-Za-z0-9]+/)) {
if (!w) continue
const t = w.toLowerCase()
if (/^(19|20)\d{2}$/.test(t)) continue
if ((/^[A-Z]/.test(w) && w.length >= 3 || t.length >= 5) && !R9_STOP.has(t)) terms.add(t)
}
return terms
}
function r9_atribucion(full, reasons, sourceTitle = null) {
const idx = full.indexOf('\n\n\u2014 fuente: ') const idx = full.indexOf('\n\n\u2014 fuente: ')
if (idx === -1) return if (idx === -1) return
const text = full.slice(0, idx).toLowerCase() const body = full.slice(0, idx).toLowerCase()
const words = text.split(/[^a-z0-9]+/).filter(w => w.length > 4) const fuente = full.slice(idx)
const words = body.split(/[^a-z0-9]+/).filter(w => w.length > 4)
if (words.length < 5) return if (words.length < 5) return
const DOMAIN = ['fee','block','channel','lightning','transaction','script','taproot','multisig','utxo','satoshi','chain','node','mining','pool','hash','signature','key','address','protocol','wallet','custody','exchange','etf','outflow','treasury','hack','exploit','phishing','security','privacy','regulation','stablecoin','nostr','zap','invoice','payment','merchant','btcpay','server','self-hosted','open-source','spot','futures','leverage','liquidation'] const url = (fuente.match(/https?:\/\/[^\s·]+/) || [''])[0]
const hasDomain = words.some(w => DOMAIN.includes(w) || w.includes('bitcoin') || w.includes('lightning') || w.includes('chain') || w.includes('taproot')) const terms = r9_sourceTerms(url, sourceTitle)
if (!hasDomain && words.length > 12) { if (!terms.size) return // fuente sin terminos distinguibles (shortener/id): R9 no puede verificar
const bodyTokens = new Set(body.split(/[^a-z0-9]+/).filter(Boolean))
if (![...terms].some(t => bodyTokens.has(t))) {
reasons.push('R9-atribucion-no-verificable') reasons.push('R9-atribucion-no-verificable')
} }
} }
@@ -154,7 +193,7 @@ export function ensureGuardTables(pool) {
reasons text NOT NULL, content text NOT NULL reasons text NOT NULL, content text NOT NULL
)`) )`)
} }
export async function guardCheck(pool, { channel, type, content }) { export async function guardCheck(pool, { channel, type, content, sourceTitle = null }) {
const reasons = [] const reasons = []
const full = String(content ?? '') const full = String(content ?? '')
// ronda-14: separar el texto del LLM de la línea de atribución // ronda-14: separar el texto del LLM de la línea de atribución
@@ -169,7 +208,7 @@ export async function guardCheck(pool, { channel, type, content }) {
r3_hechos(full, reasons) r3_hechos(full, reasons)
r7_frescura(channel, full, reasons) r7_frescura(channel, full, reasons)
r8_cierre(body, reasons) r8_cierre(body, reasons)
r9_atribucion(full, reasons) r9_atribucion(full, reasons, sourceTitle)
await r4_casiDuplicado(pool, body, reasons) await r4_casiDuplicado(pool, body, reasons)
await r5_caps(pool, channel, type, reasons) await r5_caps(pool, channel, type, reasons)
} }