mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-30 19:49:14 +00:00
* v0.41.15.0 feat: conversation parser cathedral + progressive-batch primitive (closes #1461) Replaces PR #1461's single-format Telegram regex with a 12-pattern built-in registry covering iMessage/Slack, Telegram (×2), Discord (×2), WhatsApp (×2 locales), Signal, Matrix/Element, IRC (×2), Teams. Each pattern is hand-vetted from public format docs (signal-cli, DiscordChatExporter, Telegram Desktop, WhatsApp export docs, Element matrix-archive, irssi/weechat defaults); module-load validation runs test_positive[] + test_negative[] for every pattern at startup so a typo makes gbrain refuse to start. PR #1461 contributor's BRACKET_TIME_RX + cleanSpeaker survive verbatim as the `telegram-bracket` built-in pattern + DEFAULT_SPEAKER_CLEAN export. All 33 of their test cases pass against the new orchestrator. Three layers per page (orchestrator chooses): 1. Built-in pattern registry (zero-cost, deterministic) 2. User-declared simple_pattern via config (deferred to v0.42+) 3. Opt-IN LLM polish + fallback (privacy-first; chat content goes to Anthropic only when user explicitly enables) D18 priority scoring picks the highest-match-rate pattern across the first 10 lines (not first-wins) so overlapping formats don't silently mis-route. D5 multi_line per-pattern + D11 quick_reject prefix screen + D19 timezone_policy per-pattern complete the registry shape. Companion: src/core/progressive-batch/ primitive (rule of three satisfied across 12+ ad-hoc cost-prompt sites). Wintermute-inspired ramp shape (trial 10 → 100 → 500 → full with verification at each stage), productionized with verifier+policy injection (callers describe HOW TO MEASURE SUCCESS, not WHEN TO WAIT FOR CTRL-C). D3 fail-closed budget gate: null tracker + null Policy.maxCostUsd → abort_cost_cap reason='no_budget_safety_net'. D20 discriminated Verifier union (output_count | idempotent_mutation | noop). extract-conversation-facts is the one proven consumer in v0.41.15.0; 9-site retrofit deferred to v0.41.16.0+ per TODOS.md. Codex outside-voice review absorbed 8 substantive findings: - Privacy posture (LLM polish/fallback flipped to opt-IN) - ReDoS theater (dropped arbitrary user regex; v0.42+ uses RE2) - LLM-inferred-regex persistence as silent-corruption machine - Pattern priority scoring across first 10 lines - Timezone policy on every PatternEntry - Verifier shape discriminated union - Behavior parity for sites that "jumped straight to full" - Real-corpus-redacted fixture gap (v0.42+ TODO) CI gates: - bun run check:conversation-parser (13 fixtures, --no-llm, deterministic) - bun run check:fixture-privacy (banned-token grep) Doctor surfaces 3 new checks: conversation_format_coverage, progressive_batch_audit_health, conversation_parser_probe_health. Tests: 198/198 across primitive + parser + LLM + nightly probe + eval CLI + debug CLI + doctor checks + migration v97 round-trip + E2E parser ↔ engine integration. Real bug caught + fixed during gap audit: IdempotentMutationVerifier was comparing absolute mutated-count vs per-stage expected (failed silently on stage 2+); now uses per-stage delta semantics matching OutputCountVerifier. Schema migration v97: conversation_parser_llm_cache table with (content_sha256, model_id, call_shape) composite key. NO inferred_patterns table (D17: silent-corruption machine). Plan + 23 decisions + codex outside-voice absorption at ~/.claude/plans/system-instruction-you-are-working-cuddly-hollerith.md. Co-Authored-By: garrytan-agents (PR #1461) <noreply@github.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(check-privacy): allowlist scripts/check-fixture-privacy.sh The new sibling privacy guard literally names the banned tokens in its BANNED_TOKENS array — same meta-exception that check-privacy.sh itself gets. Without this allowlist entry, bun run verify rejects the file post-merge because the banned name appears in the rule-definition script. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: renumber v0.41.15.0 → v0.41.16.0 (queue drift) Mechanical rename across all surfaces: VERSION, package.json, CHANGELOG (header + body refs), CLAUDE.md, TODOS.md, src/core/ migrate.ts (migration v98 comment), all src/core/conversation-parser/* and src/core/progressive-batch/* file headers, all test/ headers, scripts/check-privacy.sh allowlist comment, llms-full.txt regenerated. Audit clean: VERSION + package.json + CHANGELOG header all show 0.41.16.0. verify 24/24, touched tests 179/179. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: garrytan-agents (PR #1461) <noreply@github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
181 lines
5.1 KiB
TypeScript
181 lines
5.1 KiB
TypeScript
/**
|
|
* v0.41.16.0 — `gbrain eval conversation-parser <fixture.jsonl>` CLI verb.
|
|
*
|
|
* Per Layer 4a: fixture-corpus CI gate. Deterministic; runs in PR CI
|
|
* with `--no-llm` so built-in regex regressions block PRs WITHOUT
|
|
* spending API tokens.
|
|
*
|
|
* Exit codes (match eval-gate convention):
|
|
* 0 — PASS (all fixtures passed)
|
|
* 1 — FAIL (any fixture failed)
|
|
* 2 — USAGE (bad args, missing fixture, malformed JSONL)
|
|
*
|
|
* Stable JSON envelope under `--json`:
|
|
* {
|
|
* schema_version: 1,
|
|
* ...EvalReport
|
|
* }
|
|
*/
|
|
|
|
import { readFileSync, existsSync } from 'node:fs';
|
|
import {
|
|
parseFixtureJsonl,
|
|
scoreFixture,
|
|
aggregateScores,
|
|
type ConversationFixture,
|
|
type EvalReport,
|
|
} from '../core/conversation-parser/eval.ts';
|
|
|
|
interface CliArgs {
|
|
fixtures?: string;
|
|
minRecall: number;
|
|
noLlm: boolean;
|
|
json: boolean;
|
|
help: boolean;
|
|
}
|
|
|
|
function parseArgs(args: string[]): CliArgs {
|
|
const out: CliArgs = {
|
|
minRecall: 0.9,
|
|
noLlm: false,
|
|
json: false,
|
|
help: false,
|
|
};
|
|
for (let i = 0; i < args.length; i++) {
|
|
const a = args[i];
|
|
if (a === '--fixtures') {
|
|
out.fixtures = args[++i];
|
|
} else if (a.startsWith('--fixtures=')) {
|
|
out.fixtures = a.slice('--fixtures='.length);
|
|
} else if (a === '--min-recall') {
|
|
out.minRecall = Number(args[++i]);
|
|
} else if (a.startsWith('--min-recall=')) {
|
|
out.minRecall = Number(a.slice('--min-recall='.length));
|
|
} else if (a === '--no-llm') {
|
|
out.noLlm = true;
|
|
} else if (a === '--json') {
|
|
out.json = true;
|
|
} else if (a === '--help' || a === '-h') {
|
|
out.help = true;
|
|
} else if (!out.fixtures && !a.startsWith('--')) {
|
|
// Positional fixture path (e.g. `gbrain eval conversation-parser path.jsonl`).
|
|
out.fixtures = a;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function printHelp(): void {
|
|
process.stdout.write(`Usage: gbrain eval conversation-parser [options] <fixture.jsonl>
|
|
|
|
Run the v0.41.16.0 conversation parser against a JSONL fixture
|
|
corpus and report per-fixture quality + an aggregate verdict.
|
|
|
|
Options:
|
|
--fixtures <path> Path to JSONL fixture file. Also positional.
|
|
--min-recall <float> Recall floor for positive fixtures. Default 0.9.
|
|
--no-llm Disable LLM polish + fallback (built-in regex only).
|
|
Use for CI gating to keep the run deterministic.
|
|
--json Emit stable JSON envelope on stdout.
|
|
--help, -h Print this help.
|
|
|
|
Exit codes:
|
|
0 PASS — every fixture met its criteria.
|
|
1 FAIL — one or more fixtures failed; failure list printed.
|
|
2 USAGE — bad args, missing fixture, malformed JSONL.
|
|
|
|
Fixture line shape (one JSON object per line):
|
|
{
|
|
"fixture_id": "string",
|
|
"pattern": "string | null (null = adversarial)",
|
|
"frontmatter": { "date": "YYYY-MM-DD", ... },
|
|
"body": "string",
|
|
"expected_messages": number,
|
|
"expected_participants": ["string", ...]
|
|
}
|
|
`);
|
|
}
|
|
|
|
/**
|
|
* Returns the exit code. CLI dispatcher process.exit's on the return.
|
|
*/
|
|
export async function runEvalConversationParser(
|
|
args: string[],
|
|
): Promise<number> {
|
|
const cli = parseArgs(args);
|
|
if (cli.help) {
|
|
printHelp();
|
|
return 0;
|
|
}
|
|
if (!cli.fixtures) {
|
|
process.stderr.write(
|
|
'[eval conversation-parser] USAGE: missing fixture path. See --help.\n',
|
|
);
|
|
return 2;
|
|
}
|
|
if (!existsSync(cli.fixtures)) {
|
|
process.stderr.write(
|
|
`[eval conversation-parser] USAGE: fixture file not found: ${cli.fixtures}\n`,
|
|
);
|
|
return 2;
|
|
}
|
|
|
|
let fixtures: ConversationFixture[];
|
|
try {
|
|
const content = readFileSync(cli.fixtures, 'utf8');
|
|
fixtures = parseFixtureJsonl(content);
|
|
} catch (err) {
|
|
process.stderr.write(
|
|
`[eval conversation-parser] USAGE: ${(err as Error).message}\n`,
|
|
);
|
|
return 2;
|
|
}
|
|
|
|
if (fixtures.length === 0) {
|
|
process.stderr.write(
|
|
'[eval conversation-parser] USAGE: fixture file has zero parseable lines.\n',
|
|
);
|
|
return 2;
|
|
}
|
|
|
|
const scores = fixtures.map((f) =>
|
|
scoreFixture(f, { minRecall: cli.minRecall, noLlm: cli.noLlm }),
|
|
);
|
|
const report: EvalReport = aggregateScores(scores);
|
|
|
|
if (cli.json) {
|
|
process.stdout.write(JSON.stringify(report) + '\n');
|
|
} else {
|
|
formatHumanReport(report);
|
|
}
|
|
|
|
return report.failed > 0 ? 1 : 0;
|
|
}
|
|
|
|
function formatHumanReport(report: EvalReport): void {
|
|
process.stdout.write(
|
|
`[eval conversation-parser] ${report.passed}/${report.total_fixtures} fixtures passed\n`,
|
|
);
|
|
process.stdout.write(
|
|
`[eval conversation-parser] recall_mean=${report.recall_mean.toFixed(3)} ` +
|
|
`participants_recall_mean=${report.participants_recall_mean.toFixed(3)}\n`,
|
|
);
|
|
process.stdout.write(
|
|
`[eval conversation-parser] pattern_coverage: ${Object.entries(report.pattern_coverage)
|
|
.map(([k, v]) => `${k}=${v}`)
|
|
.join(', ')}\n`,
|
|
);
|
|
if (report.failed > 0) {
|
|
process.stdout.write('\nFAILED FIXTURES:\n');
|
|
for (const s of report.fixtures) {
|
|
if (s.passed) continue;
|
|
process.stdout.write(
|
|
` - ${s.fixture_id} (expected=${s.expected_pattern}, got=${s.matched_pattern_id ?? 'no_match'})\n`,
|
|
);
|
|
for (const r of s.reasons) {
|
|
process.stdout.write(` • ${r}\n`);
|
|
}
|
|
}
|
|
}
|
|
}
|