mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* feat: content-quality gate on sync — quarantine junk + flag boilerplate (#1699) Three-tier disposition at the importFromContent narrow waist: - High-confidence junk (Cloudflare/CAPTCHA interstitial patterns + operator literals) -> quarantine (hidden from search, zero chunks) or reject. - Fuzzy markup-heavy (prose-vs-markup ratio, warn-tier window, code-exempt) -> content_flag marker, stays searchable, agent warned. - Oversize -> existing embed_skip soft-block + content_flag:oversized warning. Agent-warning channel: SearchResult.content_flag (stamped in hybridSearch + the keyword-only search op) and a top-level content_flag on get_page. New quarantine.ts markers, gbrain quarantine CLI (list/clear/scan), doctor quarantined_pages + flagged_pages checks (engine.executeRaw, works on PGLite), sources-audit disposition awareness, markup-heavy lint rule, config keys. Security: gate-owned markers stripped from untrusted (remote MCP) frontmatter so a write-scoped client can't hide pages or inject the warning channel. Markers excluded from content_hash so flagged pages don't re-embed every sync. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.42.8.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: document content-quality gate (quarantine + content_flag) for v0.42.8.0 Add CLAUDE.md Key Files + Commands entries for the #1699 content-quality gate: src/core/quarantine.ts, gbrain quarantine CLI (list/clear/scan), the agent-warning channel (SearchResult.content_flag + get_page), doctor quarantined_pages/flagged_pages checks, the markup-heavy lint rule, sources-audit disposition awareness, and the three new content_sanity config keys. Regenerate llms-full.txt from CLAUDE.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
229 lines
8.7 KiB
TypeScript
229 lines
8.7 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
|
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { tmpdir } from 'os';
|
|
import { withEnv } from '../helpers/with-env.ts';
|
|
import {
|
|
logContentSanityAssessment,
|
|
readRecentContentSanityEvents,
|
|
summarizeContentSanityEvents,
|
|
computeContentSanityAuditFilename,
|
|
type ContentSanityAuditEvent,
|
|
} from '../../src/core/audit/content-sanity-audit.ts';
|
|
import type { ContentSanityResult } from '../../src/core/content-sanity.ts';
|
|
|
|
function makeResult(opts: {
|
|
bytes?: number;
|
|
hard?: boolean;
|
|
soft?: boolean;
|
|
warn?: boolean;
|
|
pattern?: string;
|
|
literal?: string;
|
|
}): ContentSanityResult {
|
|
const junk_pattern_matches: string[] = opts.pattern ? [opts.pattern] : [];
|
|
const literal_substring_matches: string[] = opts.literal ? [opts.literal] : [];
|
|
const reasons: ContentSanityResult['reasons'] = [];
|
|
const reason_messages: string[] = [];
|
|
if (opts.soft) {
|
|
reasons.push('oversize_block');
|
|
reason_messages.push('PAGE_OVERSIZED: body 600000 bytes');
|
|
} else if (opts.warn) {
|
|
reasons.push('oversize_warn');
|
|
reason_messages.push('PAGE_OVERSIZE_WARN: body 100000 bytes');
|
|
}
|
|
if (junk_pattern_matches.length > 0) {
|
|
reasons.push('junk_pattern');
|
|
reason_messages.push(`PAGE_JUNK_PATTERN: matched ${junk_pattern_matches.join(', ')}`);
|
|
}
|
|
if (literal_substring_matches.length > 0) {
|
|
reasons.push('literal_substring');
|
|
reason_messages.push(`PAGE_JUNK_PATTERN: literal ${literal_substring_matches.join(', ')}`);
|
|
}
|
|
const shouldQuarantine = !!opts.hard || junk_pattern_matches.length > 0 || literal_substring_matches.length > 0;
|
|
const shouldSkipEmbed = !!opts.soft && !shouldQuarantine;
|
|
return {
|
|
bytes: opts.bytes ?? 1000,
|
|
oversize: !!opts.soft,
|
|
junk_pattern_matches,
|
|
literal_substring_matches,
|
|
prose_chars: null,
|
|
markup_ratio: null,
|
|
reasons,
|
|
reason_messages,
|
|
shouldQuarantine,
|
|
shouldHardBlock: shouldQuarantine,
|
|
shouldFlag: shouldSkipEmbed,
|
|
flag_reason: shouldSkipEmbed ? 'oversized' : null,
|
|
shouldSkipEmbed,
|
|
};
|
|
}
|
|
|
|
describe('computeContentSanityAuditFilename', () => {
|
|
test('emits the ISO-week prefix shape', () => {
|
|
const name = computeContentSanityAuditFilename(new Date('2026-05-24T07:00:00Z'));
|
|
expect(name).toMatch(/^content-sanity-\d{4}-W\d{2}\.jsonl$/);
|
|
});
|
|
});
|
|
|
|
describe('logContentSanityAssessment (E2E via tempdir)', () => {
|
|
test('writes hard-block event', async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'cs-audit-hard-'));
|
|
try {
|
|
await withEnv({ GBRAIN_AUDIT_DIR: dir }, async () => {
|
|
const result = makeResult({ hard: true, pattern: 'cloudflare_attention_required', bytes: 287 });
|
|
logContentSanityAssessment('media/articles/foo', 'straylight-brain', result);
|
|
const events = readRecentContentSanityEvents(7);
|
|
expect(events.length).toBe(1);
|
|
expect(events[0].event_type).toBe('hard_block');
|
|
expect(events[0].slug).toBe('media/articles/foo');
|
|
expect(events[0].source_id).toBe('straylight-brain');
|
|
expect(events[0].junk_pattern_matches).toContain('cloudflare_attention_required');
|
|
expect(events[0].bytes).toBe(287);
|
|
});
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('writes soft-block event', async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'cs-audit-soft-'));
|
|
try {
|
|
await withEnv({ GBRAIN_AUDIT_DIR: dir }, async () => {
|
|
const result = makeResult({ soft: true, bytes: 890_000 });
|
|
logContentSanityAssessment('media/big-transcript', 'default', result);
|
|
const events = readRecentContentSanityEvents(7);
|
|
expect(events.length).toBe(1);
|
|
expect(events[0].event_type).toBe('soft_block');
|
|
expect(events[0].bytes).toBe(890_000);
|
|
});
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('writes warn event', async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'cs-audit-warn-'));
|
|
try {
|
|
await withEnv({ GBRAIN_AUDIT_DIR: dir }, async () => {
|
|
const result = makeResult({ warn: true, bytes: 100_000 });
|
|
logContentSanityAssessment('notes/long', 'default', result);
|
|
const events = readRecentContentSanityEvents(7);
|
|
expect(events.length).toBe(1);
|
|
expect(events[0].event_type).toBe('warn');
|
|
});
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('skips no-op rows (no reasons + no bypass)', async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'cs-audit-noop-'));
|
|
try {
|
|
await withEnv({ GBRAIN_AUDIT_DIR: dir }, async () => {
|
|
const result = makeResult({}); // no reasons fire
|
|
logContentSanityAssessment('normal-page', 'default', result);
|
|
const events = readRecentContentSanityEvents(7);
|
|
expect(events.length).toBe(0);
|
|
});
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('bypass active overrides hard/soft → records as warn with bypass_active flag', async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'cs-audit-bypass-'));
|
|
try {
|
|
await withEnv({ GBRAIN_AUDIT_DIR: dir }, async () => {
|
|
const result = makeResult({ hard: true, pattern: 'access_denied' });
|
|
logContentSanityAssessment('bypassed', 'default', result, { bypass: true });
|
|
const events = readRecentContentSanityEvents(7);
|
|
expect(events.length).toBe(1);
|
|
expect(events[0].event_type).toBe('warn');
|
|
expect(events[0].bypass_active).toBe(true);
|
|
});
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('multiple events accumulate in one file', async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'cs-audit-multi-'));
|
|
try {
|
|
await withEnv({ GBRAIN_AUDIT_DIR: dir }, async () => {
|
|
logContentSanityAssessment('a', 'src', makeResult({ hard: true, pattern: 'access_denied' }));
|
|
logContentSanityAssessment('b', 'src', makeResult({ soft: true, bytes: 600000 }));
|
|
logContentSanityAssessment('c', 'src', makeResult({ warn: true, bytes: 70000 }));
|
|
const events = readRecentContentSanityEvents(7);
|
|
expect(events.length).toBe(3);
|
|
});
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('summarizeContentSanityEvents', () => {
|
|
function event(over: Partial<ContentSanityAuditEvent>): ContentSanityAuditEvent {
|
|
return {
|
|
ts: new Date().toISOString(),
|
|
event_type: 'hard_block',
|
|
slug: 'test',
|
|
source_id: 'default',
|
|
bytes: 100,
|
|
junk_pattern_matches: [],
|
|
literal_substring_matches: [],
|
|
reason_messages: [],
|
|
...over,
|
|
};
|
|
}
|
|
test('empty input returns zero summary', () => {
|
|
const s = summarizeContentSanityEvents([]);
|
|
expect(s.total_events).toBe(0);
|
|
expect(s.by_type).toEqual({ hard_block: 0, quarantine: 0, reject: 0, flag: 0, soft_block: 0, warn: 0 });
|
|
expect(s.top_patterns).toEqual([]);
|
|
});
|
|
|
|
test('counts by type (v0.42 quarantine/reject/flag)', () => {
|
|
const s = summarizeContentSanityEvents([
|
|
event({ event_type: 'quarantine' }),
|
|
event({ event_type: 'quarantine' }),
|
|
event({ event_type: 'reject' }),
|
|
event({ event_type: 'flag' }),
|
|
event({ event_type: 'soft_block' }),
|
|
event({ event_type: 'warn' }),
|
|
]);
|
|
expect(s.by_type).toEqual({ hard_block: 0, quarantine: 2, reject: 1, flag: 1, soft_block: 1, warn: 1 });
|
|
expect(s.total_events).toBe(6);
|
|
});
|
|
|
|
test('counts by source', () => {
|
|
const s = summarizeContentSanityEvents([
|
|
event({ source_id: 'straylight-brain' }),
|
|
event({ source_id: 'straylight-brain' }),
|
|
event({ source_id: 'default' }),
|
|
]);
|
|
expect(s.by_source['straylight-brain']).toBe(2);
|
|
expect(s.by_source['default']).toBe(1);
|
|
});
|
|
|
|
test('top_patterns sorted desc by count', () => {
|
|
const s = summarizeContentSanityEvents([
|
|
event({ junk_pattern_matches: ['cloudflare_attention_required'] }),
|
|
event({ junk_pattern_matches: ['cloudflare_attention_required'] }),
|
|
event({ junk_pattern_matches: ['cloudflare_attention_required'] }),
|
|
event({ junk_pattern_matches: ['access_denied'] }),
|
|
]);
|
|
expect(s.top_patterns[0]).toEqual({ name: 'cloudflare_attention_required', count: 3 });
|
|
expect(s.top_patterns[1]).toEqual({ name: 'access_denied', count: 1 });
|
|
});
|
|
|
|
test('literal substring hits count alongside pattern hits', () => {
|
|
const s = summarizeContentSanityEvents([
|
|
event({ literal_substring_matches: ['reddit_blocked', 'linkedin_wall'] }),
|
|
event({ literal_substring_matches: ['reddit_blocked'] }),
|
|
]);
|
|
expect(s.top_patterns).toContainEqual({ name: 'reddit_blocked', count: 2 });
|
|
expect(s.top_patterns).toContainEqual({ name: 'linkedin_wall', count: 1 });
|
|
});
|
|
});
|