r16: guard R9 con terminos de la fuente (no dominio generico) + doc de sesion
This commit is contained in:
@@ -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
@@ -130,16 +130,55 @@ async function r5_caps(pool, channel, type, out) {
|
||||
}
|
||||
|
||||
|
||||
// R9 (ronda-14): la pieza debe compartir terminos con su fuente
|
||||
function r9_atribucion(full, reasons) {
|
||||
// R9 (ronda-14 → ronda-16): la pieza debe compartir TERMINOS ESPECIFICOS de su
|
||||
// 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: ')
|
||||
if (idx === -1) return
|
||||
const text = full.slice(0, idx).toLowerCase()
|
||||
const words = text.split(/[^a-z0-9]+/).filter(w => w.length > 4)
|
||||
const body = full.slice(0, idx).toLowerCase()
|
||||
const fuente = full.slice(idx)
|
||||
const words = body.split(/[^a-z0-9]+/).filter(w => w.length > 4)
|
||||
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 hasDomain = words.some(w => DOMAIN.includes(w) || w.includes('bitcoin') || w.includes('lightning') || w.includes('chain') || w.includes('taproot'))
|
||||
if (!hasDomain && words.length > 12) {
|
||||
const url = (fuente.match(/https?:\/\/[^\s·]+/) || [''])[0]
|
||||
const terms = r9_sourceTerms(url, sourceTitle)
|
||||
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')
|
||||
}
|
||||
}
|
||||
@@ -154,7 +193,7 @@ export function ensureGuardTables(pool) {
|
||||
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 full = String(content ?? '')
|
||||
// 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)
|
||||
r7_frescura(channel, full, reasons)
|
||||
r8_cierre(body, reasons)
|
||||
r9_atribucion(full, reasons)
|
||||
r9_atribucion(full, reasons, sourceTitle)
|
||||
await r4_casiDuplicado(pool, body, reasons)
|
||||
await r5_caps(pool, channel, type, reasons)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user