mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-30 11:22:34 +00:00
calibration: pattern drill-down route + revisit-now CLI (TD3 / D29 + TD4 / D30)
TD3 (D29) — clickable pattern drill-down endpoint:
GET /admin/api/calibration/pattern/:id (requireAdmin)
Returns the pattern statement at index `id` plus the top 25 resolved
takes for the holder, sorted by weight desc. v0.36.0.0 ship-state
approximation: surfaces broad provenance evidence (top resolved
takes). v0.37+ stores per-pattern source_take_ids[] on a
calibration_profile_patterns join table so the drill-down shows the
EXACT takes that drove the pattern.
Surfaces a `provenance_note` field in the response so the operator
sees the v0.36.0.0-vs-v0.37 fidelity boundary inline.
The admin SPA's renderPatternStatementsCard SVG already emits anchor
tags pointing at /admin/calibration/pattern/<i> (T15 ship state).
This route makes those anchors clickable — closes the trust loop that
was the rationale for D29 ("pattern statements without their evidence
are dressed-up LLM hallucinations").
TD4 (D30) — `gbrain takes revisit <slug>` editor-open action:
Adds the `revisit` subcommand to gbrain takes. Opens $EDITOR (falling
back to vi) on the source markdown file for the slug. Appends a
`<!-- gbrain:revisit -->` cursor marker at the bottom of the page on
first invocation so the editor opens with intent visible.
Reads sync.repo_path from config to locate the brain repo. Refuses to
proceed with a clear error when the repo isn't configured or the page
doesn't exist.
spawnSync with stdio:'inherit' so the editor takes the terminal. Exit
status surfaced on failure.
The SVG renderer's revisit-now anchor for each abandoned thread row
emits /admin/calibration/revisit/<takeId>. A small route handler that
resolves take_id → page_slug then dispatches `gbrain takes revisit`
via spawn is a v0.37 follow-up — the CLI command exists now so
developers can wire it directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
c45b4329f7
commit
c4e4c67f51
@@ -622,6 +622,59 @@ export async function runServeHttp(engine: BrainEngine, options: ServeHttpOption
|
||||
|
||||
// v0.36.0.0 (T15 / E6 / D23) — Calibration tab data endpoints.
|
||||
// Server-rendered SVG charts; admin SPA renders via TrustedSVG wrapper.
|
||||
// v0.36.0.0 (TD3) — pattern drill-down. Returns the source takes that
|
||||
// produced the pattern statement at index `id` of the active profile.
|
||||
// v0.36.0.0 ship state: returns the top N takes in the holder's overall
|
||||
// takes table, sorted by weight desc. v0.37+ will store per-pattern
|
||||
// source_take_ids on calibration_profiles_patterns so the drill-down
|
||||
// shows the EXACT takes that drove the pattern.
|
||||
app.get('/admin/api/calibration/pattern/:id', requireAdmin, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { getLatestProfile } = await import('./calibration.ts');
|
||||
const holder = (req.query.holder as string) || 'garry';
|
||||
const profile = await getLatestProfile(engine, { holder });
|
||||
if (!profile) {
|
||||
res.status(404).json({ error: 'no_profile' });
|
||||
return;
|
||||
}
|
||||
const rawId = req.params.id;
|
||||
const idStr = Array.isArray(rawId) ? rawId[0] : rawId;
|
||||
const idx = Number.parseInt(idStr ?? '', 10) - 1;
|
||||
if (!Number.isFinite(idx) || idx < 0 || idx >= profile.pattern_statements.length) {
|
||||
res.status(400).json({ error: 'invalid_pattern_index', max: profile.pattern_statements.length });
|
||||
return;
|
||||
}
|
||||
const statement = profile.pattern_statements[idx];
|
||||
// v0.36.0.0 ship state: surface the top resolved takes for the
|
||||
// holder as drill-down evidence. Per-pattern provenance is v0.37.
|
||||
const takes = await engine.executeRaw<{
|
||||
id: number;
|
||||
page_slug: string;
|
||||
row_num: number;
|
||||
claim: string;
|
||||
weight: number;
|
||||
resolved_quality: string | null;
|
||||
since_date: string | null;
|
||||
}>(
|
||||
`SELECT id, page_slug, row_num, claim, weight, resolved_quality, since_date
|
||||
FROM takes
|
||||
WHERE holder = $1 AND active = true AND resolved_at IS NOT NULL
|
||||
ORDER BY weight DESC, since_date DESC
|
||||
LIMIT 25`,
|
||||
[holder],
|
||||
);
|
||||
res.json({
|
||||
pattern_statement: statement,
|
||||
pattern_index: idx + 1,
|
||||
holder,
|
||||
provenance_note: 'v0.36.0.0 ship state shows top-25 resolved takes for this holder; per-pattern source_take_ids land in v0.37.',
|
||||
takes,
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err instanceof Error ? err.message : 'unknown' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/api/calibration/profile', requireAdmin, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { getLatestProfile } = await import('./calibration.ts');
|
||||
|
||||
@@ -552,8 +552,55 @@ Common flags:
|
||||
case 'resolve': return cmdResolve(engine, rest);
|
||||
case 'scorecard': return cmdScorecard(engine, rest);
|
||||
case 'calibration': return cmdCalibration(engine, rest);
|
||||
case 'revisit': return cmdRevisit(engine, rest);
|
||||
default:
|
||||
// No subcommand keyword → treat first arg as <slug> for the list path.
|
||||
return cmdList(engine, args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.36.0.0 (TD4 / D30) — `gbrain takes revisit <slug>` opens $EDITOR on
|
||||
* the source page so the user can write a follow-up immediately. The
|
||||
* action the admin SPA's "revisit now" link triggers (via a small
|
||||
* route handler that dispatches into this CLI command).
|
||||
*
|
||||
* Inserts a `<!-- gbrain:revisit -->` cursor marker at the bottom of the
|
||||
* page body so the editor opens with intent visible.
|
||||
*/
|
||||
async function cmdRevisit(_engine: BrainEngine, rest: string[]): Promise<void> {
|
||||
const slug = rest[0];
|
||||
if (!slug) {
|
||||
process.stderr.write('Usage: gbrain takes revisit <slug>\n');
|
||||
process.exit(1);
|
||||
}
|
||||
const { existsSync, readFileSync, writeFileSync } = await import('node:fs');
|
||||
const { join } = await import('node:path');
|
||||
const { execFileSync, spawnSync } = await import('node:child_process');
|
||||
const { loadConfig } = await import('../core/config.ts');
|
||||
const cfg = loadConfig();
|
||||
const repoPath = (cfg as { sync?: { repo_path?: string } } | null)?.sync?.repo_path;
|
||||
if (!repoPath) {
|
||||
process.stderr.write('No brain repo configured. Run `gbrain config set sync.repo_path /path/to/brain`.\n');
|
||||
process.exit(1);
|
||||
}
|
||||
const filePath = join(repoPath, `${slug}.md`);
|
||||
if (!existsSync(filePath)) {
|
||||
process.stderr.write(`Page not found: ${filePath}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
// Append a cursor marker if not already present.
|
||||
const existing = readFileSync(filePath, 'utf8');
|
||||
const marker = '\n<!-- gbrain:revisit -->\n';
|
||||
if (!existing.includes('<!-- gbrain:revisit -->')) {
|
||||
writeFileSync(filePath, existing + marker);
|
||||
}
|
||||
const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
|
||||
process.stderr.write(`Opening ${filePath} in ${editor}...\n`);
|
||||
// Use spawnSync with stdio:'inherit' so the editor takes the terminal.
|
||||
const result = spawnSync(editor, [filePath], { stdio: 'inherit' });
|
||||
if (result.status !== 0) {
|
||||
process.stderr.write(`Editor exited with status ${result.status ?? 'unknown'}\n`);
|
||||
}
|
||||
void execFileSync;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user