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>
187 lines
7.2 KiB
TypeScript
187 lines
7.2 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
|
import { lintContent } from '../src/commands/lint.ts';
|
|
|
|
const MINIMAL_FRONTMATTER = `---
|
|
title: Test Page
|
|
type: note
|
|
created: 2026-05-24
|
|
---
|
|
|
|
`;
|
|
|
|
describe('lint — huge-page rule', () => {
|
|
test('does not fire below warn threshold', () => {
|
|
const content = MINIMAL_FRONTMATTER + 'a'.repeat(40_000);
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.find((i) => i.rule === 'huge-page')).toBeUndefined();
|
|
});
|
|
|
|
test('fires when body exceeds warn threshold (default 50K)', () => {
|
|
const content = MINIMAL_FRONTMATTER + 'a'.repeat(60_000);
|
|
const issues = lintContent(content, 'test.md');
|
|
const huge = issues.find((i) => i.rule === 'huge-page');
|
|
expect(huge).toBeDefined();
|
|
expect(huge!.message).toContain('60');
|
|
expect(huge!.fixable).toBe(false);
|
|
expect(huge!.line).toBe(1);
|
|
});
|
|
|
|
test('fires with block-threshold language when body exceeds block', () => {
|
|
const content = MINIMAL_FRONTMATTER + 'a'.repeat(600_000);
|
|
const issues = lintContent(content, 'test.md');
|
|
const huge = issues.find((i) => i.rule === 'huge-page');
|
|
expect(huge).toBeDefined();
|
|
expect(huge!.message).toContain('block');
|
|
});
|
|
|
|
test('respects custom bytes_warn override', () => {
|
|
const content = MINIMAL_FRONTMATTER + 'a'.repeat(1000);
|
|
const issues = lintContent(content, 'test.md', {
|
|
contentSanity: { bytes_warn: 500, bytes_block: 50_000 },
|
|
});
|
|
expect(issues.find((i) => i.rule === 'huge-page')).toBeDefined();
|
|
});
|
|
|
|
test('disabled kill-switch suppresses huge-page rule', () => {
|
|
const content = MINIMAL_FRONTMATTER + 'a'.repeat(600_000);
|
|
const issues = lintContent(content, 'test.md', {
|
|
contentSanity: { disabled: true },
|
|
});
|
|
expect(issues.find((i) => i.rule === 'huge-page')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('lint — scraper-junk rule', () => {
|
|
test('does not fire on clean content', () => {
|
|
const content = MINIMAL_FRONTMATTER + 'This is a thoughtful essay about software design.';
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.find((i) => i.rule === 'scraper-junk')).toBeUndefined();
|
|
});
|
|
|
|
test('fires when title matches cloudflare_attention_required pattern', () => {
|
|
const content = `---
|
|
title: 'Attention Required! | Cloudflare'
|
|
type: note
|
|
created: 2026-05-24
|
|
---
|
|
|
|
Body content.`;
|
|
const issues = lintContent(content, 'test.md');
|
|
const junk = issues.find((i) => i.rule === 'scraper-junk');
|
|
expect(junk).toBeDefined();
|
|
expect(junk!.message).toContain('cloudflare_attention_required');
|
|
});
|
|
|
|
test('fires on access_denied body pattern', () => {
|
|
const content = MINIMAL_FRONTMATTER + 'Access denied\n\nYou do not have permission.';
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.find((i) => i.rule === 'scraper-junk')).toBeDefined();
|
|
});
|
|
|
|
test('operator literal hits also surface', () => {
|
|
const content = MINIMAL_FRONTMATTER + "You're being blocked from accessing this site.";
|
|
const issues = lintContent(content, 'test.md', {
|
|
contentSanity: {
|
|
operator_literals: [{ name: 'reddit_blocked', substring: "you're being blocked from accessing" }],
|
|
},
|
|
});
|
|
const junk = issues.find((i) => i.rule === 'scraper-junk');
|
|
expect(junk).toBeDefined();
|
|
expect(junk!.message).toContain('reddit_blocked');
|
|
});
|
|
|
|
test('junk_patterns_enabled=false suppresses operator literals AND built-ins via consumer wiring', () => {
|
|
// The assessor honors junk_patterns_enabled implicitly via the
|
|
// operator_literals=[] passed by runLintCore. Lint here tests the
|
|
// direct call path: when caller passes junk_patterns_enabled=false,
|
|
// operator_literals should already be empty (production resolver
|
|
// handles that gate). This test pins built-in patterns still fire
|
|
// even when junk_patterns_enabled flag is on the opts but no
|
|
// literals are passed — i.e., the flag is informational at this
|
|
// layer; the resolver consults it before constructing opts.
|
|
const content = `---
|
|
title: 'Attention Required! | Cloudflare'
|
|
type: note
|
|
created: 2026-05-24
|
|
---
|
|
|
|
body`;
|
|
const issues = lintContent(content, 'test.md', {
|
|
contentSanity: { junk_patterns_enabled: false, operator_literals: [] },
|
|
});
|
|
// Built-in pattern still fires here (resolver doesn't strip
|
|
// built-ins; only operator literals are gated by the flag).
|
|
expect(issues.find((i) => i.rule === 'scraper-junk')).toBeDefined();
|
|
});
|
|
|
|
test('disabled kill-switch suppresses scraper-junk rule', () => {
|
|
const content = `---
|
|
title: 'Access Denied'
|
|
type: note
|
|
created: 2026-05-24
|
|
---
|
|
|
|
body`;
|
|
const issues = lintContent(content, 'test.md', {
|
|
contentSanity: { disabled: true },
|
|
});
|
|
expect(issues.find((i) => i.rule === 'scraper-junk')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('lint — markup-heavy rule (v0.42 #1699)', () => {
|
|
test('fires on markup-heavy body in the warn window', () => {
|
|
const navRow = '| [a](http://a) | [b](http://b) | [c](http://c) | [d](http://d) |\n';
|
|
const content = MINIMAL_FRONTMATTER + navRow.repeat(1200); // ~60K, > 50K warn
|
|
const issues = lintContent(content, 'test.md');
|
|
const mk = issues.find((i) => i.rule === 'markup-heavy');
|
|
expect(mk).toBeDefined();
|
|
expect(mk!.message).toContain('boilerplate');
|
|
expect(mk!.fixable).toBe(false);
|
|
});
|
|
|
|
test('does NOT fire on prose-heavy body of the same size', () => {
|
|
const content = MINIMAL_FRONTMATTER + 'real sentences with actual words. '.repeat(2000); // ~68K prose
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.find((i) => i.rule === 'markup-heavy')).toBeUndefined();
|
|
});
|
|
|
|
test('does NOT fire when prose_check_enabled is false', () => {
|
|
const navRow = '| [a](http://a) | [b](http://b) | [c](http://c) | [d](http://d) |\n';
|
|
const content = MINIMAL_FRONTMATTER + navRow.repeat(1200);
|
|
const issues = lintContent(content, 'test.md', { contentSanity: { prose_check_enabled: false } });
|
|
expect(issues.find((i) => i.rule === 'markup-heavy')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('lint — bytes parity with doctor (D2)', () => {
|
|
test('lint measures body-only bytes (not file bytes)', () => {
|
|
// A page with large frontmatter but small body should NOT trip
|
|
// huge-page — the rule keys on body bytes only, matching what the
|
|
// doctor `oversized_pages` check sees via octet_length(compiled_truth + timeline).
|
|
const fm = '---\ntitle: Test\ntype: note\ncreated: 2026-05-24\nbig_meta: ' + 'x'.repeat(60_000) + '\n---\n\n';
|
|
const content = fm + 'small body';
|
|
const issues = lintContent(content, 'test.md');
|
|
// The body is "small body" → ~10 bytes. Should NOT trip warn.
|
|
expect(issues.find((i) => i.rule === 'huge-page')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('lint — existing rules unaffected by content-sanity extension', () => {
|
|
test('LLM preamble rule still fires', () => {
|
|
// The LLM_PREAMBLES regex anchors on `^Of course\.?\s*Here is` so
|
|
// we use the period form (not exclamation) for an exact match.
|
|
const content = `---
|
|
title: T
|
|
type: note
|
|
created: 2026-05-24
|
|
---
|
|
|
|
Of course. Here is the brain page.
|
|
|
|
Real content.`;
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.find((i) => i.rule === 'llm-preamble')).toBeDefined();
|
|
});
|
|
});
|