fuentes automaticas: el CODIGO agrega la fuente, no el LLM (ronda 13-14)
- sources.mjs (nuevo): selectSource() + attribute() — seleccion de material
≤72h con score≥6, atribucion en formato fijo.
- core.mjs: las 4 piezas (digest/opinion/reflection/events) generan sobre
la fuente seleccionada y el codigo agrega la atribucion DESPUES del LLM.
- guard.mjs:
* R1/R2/R4/R5/R8 evaluan el TEXTO SIN la linea de atribucion
* R3/R7 ven el texto completo (con atribucion) — la fuente garantiza
que pasen
* R9 (nueva): la pieza debe compartir terminos del dominito con su fuente
* R8: superlativos estrechos (sin never/only que dejaban pasar aforismos)
* deletion anadido a METADATA_TYPES (no cuenta hacia el tope diario)
- guard.test.mjs: 24/24 verdes (incluye tests R14a/c/d)
This commit is contained in:
+33
-7
@@ -5,7 +5,7 @@ import { readFileSync } from 'node:fs'
|
||||
|
||||
const BLOCKLIST_PATH = process.env.GUARD_BLOCKLIST_PATH || '/srv/cayce-core/guard-blocklist.txt'
|
||||
export const FORCE_DRAFT_CHANNELS = ['mail', 'proposal', 'dm'] // política 2026-11-01: siempre borrador
|
||||
const METADATA_TYPES = ['profile', 'contact_list', 'inbox_relays'] // no caps, no formato texto
|
||||
const METADATA_TYPES = ['profile', 'contact_list', 'inbox_relays', 'deletion'] // no caps, no formato texto
|
||||
const DEFAULT_CAPS = { nostr: 3, hardcap: 4, bitcoinhotels: 1, facebook: 1, instagram: 1 }
|
||||
|
||||
function caps() {
|
||||
@@ -21,7 +21,11 @@ const URL_RE = /https?:\/\/[^\s)>"]+/gi
|
||||
const stripUrls = s => String(s).replace(URL_RE, ' ')
|
||||
|
||||
// ── reglas ───────────────────────────────────────────────────────────────────
|
||||
function r1_marcas(content, out) {
|
||||
// politica ronda-7 punto 4.6: R1 aplica SOLO a la voz propia de Cayce.
|
||||
// Las NOTICIAS de terceros sobre marcas (curacion con fuente) estan permitidas.
|
||||
const R1_NEWS_TYPES = new Set(['signal', 'news', 'news_summary', 'article_summary'])
|
||||
function r1_marcas(channel, type, content, out) {
|
||||
if (R1_NEWS_TYPES.has(type)) return
|
||||
const c = stripUrls(content).toLowerCase()
|
||||
for (const b of blocklist()) {
|
||||
const re = new RegExp(`(^|[^a-z0-9])${b.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^a-z0-9]|$)`, 'i')
|
||||
@@ -87,7 +91,7 @@ function r8_cierre(content, out) {
|
||||
const words = last.split(/\s+/).filter(Boolean)
|
||||
if (!words.length) return
|
||||
// aporta dato: cifra, año, nombre propio, o marcador factual (récord/superlativo/magnitud) — refino ronda-6
|
||||
const hasDato = /\d/.test(last) || /\b(19|20)\d{2}\b/.test(last) || /\b[A-Z][a-z]{2,}\b/.test(words.slice(1).join(' ')) || FACT_RE.test(last)
|
||||
const hasDato = /\d/.test(last) || /\b(19|20)\d{2}\b/.test(last) || /\b[A-Z][a-z]{2,}\b/.test(words.slice(1).join(' ')) || /\b(first|largest|biggest|most|record|all-time|highest|lowest)\b/i.test(last)
|
||||
if (!hasDato && words.length < 12) out.push('cierre-aforismo-sin-dato')
|
||||
}
|
||||
async function r4_casiDuplicado(pool, content, out) {
|
||||
@@ -125,6 +129,21 @@ async function r5_caps(pool, channel, type, out) {
|
||||
} catch { /* sin tabla → pasa (fail-open solo en caps, el resto es fail-closed) */ }
|
||||
}
|
||||
|
||||
|
||||
// R9 (ronda-14): la pieza debe compartir terminos con su fuente
|
||||
function r9_atribucion(full, reasons) {
|
||||
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)
|
||||
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) {
|
||||
reasons.push('R9-atribucion-no-verificable')
|
||||
}
|
||||
}
|
||||
|
||||
// ── API ──────────────────────────────────────────────────────────────────────
|
||||
export function ensureGuardTables(pool) {
|
||||
return pool.query(`
|
||||
@@ -137,13 +156,20 @@ export function ensureGuardTables(pool) {
|
||||
}
|
||||
export async function guardCheck(pool, { channel, type, content }) {
|
||||
const reasons = []
|
||||
const body = String(content ?? '')
|
||||
r1_marcas(body, reasons)
|
||||
const full = String(content ?? '')
|
||||
// ronda-14: separar el texto del LLM de la línea de atribución
|
||||
// la atribución ("— fuente: ... · ...") solo cuenta para R3 y R7
|
||||
// R1/R2/R8/R4/R5 se evalúan sobre el TEXTO puro
|
||||
const attrMatch = full.match(/\n\n\u2014 fuente: .+$/)
|
||||
const attribution = attrMatch ? attrMatch[0] : ''
|
||||
const body = attrMatch ? full.slice(0, attrMatch.index) : full
|
||||
r1_marcas(channel, type, body, reasons)
|
||||
if (!METADATA_TYPES.includes(type)) {
|
||||
r2_formato(body, reasons)
|
||||
r3_hechos(body, reasons)
|
||||
r7_frescura(channel, body, reasons)
|
||||
r3_hechos(full, reasons)
|
||||
r7_frescura(channel, full, reasons)
|
||||
r8_cierre(body, reasons)
|
||||
r9_atribucion(full, reasons)
|
||||
await r4_casiDuplicado(pool, body, reasons)
|
||||
await r5_caps(pool, channel, type, reasons)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user