mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
fix(takes): scope page lookup by source (#2698)
This commit is contained in:
+32
-18
@@ -28,6 +28,7 @@ import {
|
||||
type ParsedTake,
|
||||
} from '../core/takes-fence.ts';
|
||||
import { withPageLock } from '../core/page-lock.ts';
|
||||
import { resolveSourceId } from '../core/source-resolver.ts';
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
@@ -83,18 +84,31 @@ function ensureFloat(raw: string | undefined, fallback: number): number {
|
||||
return n;
|
||||
}
|
||||
|
||||
async function getPageId(engine: BrainEngine, slug: string): Promise<number> {
|
||||
const rows = await engine.executeRaw<{ id: number }>(
|
||||
`SELECT id FROM pages WHERE slug = $1 LIMIT 1`,
|
||||
[slug],
|
||||
);
|
||||
async function getPageId(engine: BrainEngine, slug: string, sourceId?: string): Promise<number> {
|
||||
const rows = sourceId
|
||||
? await engine.executeRaw<{ id: number }>(
|
||||
`SELECT id FROM pages WHERE slug = $1 AND source_id = $2 LIMIT 1`,
|
||||
[slug, sourceId],
|
||||
)
|
||||
: await engine.executeRaw<{ id: number }>(
|
||||
`SELECT id FROM pages WHERE slug = $1 LIMIT 1`,
|
||||
[slug],
|
||||
);
|
||||
if (!rows[0]) {
|
||||
console.error(`Page not found in brain: ${slug}. Run \`gbrain sync\` first.`);
|
||||
console.error(`Page not found in brain: ${slug}${sourceId ? ` (source=${sourceId})` : ''}. Run \`gbrain sync\` first.`);
|
||||
process.exit(1);
|
||||
}
|
||||
return rows[0].id;
|
||||
}
|
||||
|
||||
async function resolveTakesSourceId(engine: BrainEngine): Promise<string | undefined> {
|
||||
try {
|
||||
return await resolveSourceId(engine, null);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function readBodyOrEmpty(path: string): string {
|
||||
if (!existsSync(path)) return '';
|
||||
return readFileSync(path, 'utf-8');
|
||||
@@ -169,7 +183,7 @@ async function cmdSearch(engine: BrainEngine, args: string[]): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function cmdAdd(engine: BrainEngine, args: string[]): Promise<void> {
|
||||
async function cmdAdd(engine: BrainEngine, args: string[], sourceId?: string): Promise<void> {
|
||||
const slug = args[0];
|
||||
if (!slug) {
|
||||
console.error('Usage: gbrain takes add <slug> --claim "..." --kind <k> --who <h> [--weight 0.5] [--source "..."] [--since YYYY-MM]');
|
||||
@@ -195,7 +209,7 @@ async function cmdAdd(engine: BrainEngine, args: string[]): Promise<void> {
|
||||
writeBody(path, nextBody);
|
||||
|
||||
// Mirror to DB. Page may not be in DB yet if not synced — caller must run sync first.
|
||||
const pageId = await getPageId(engine, slug);
|
||||
const pageId = await getPageId(engine, slug, sourceId);
|
||||
await engine.addTakesBatch([{
|
||||
page_id: pageId, row_num: rowNum, claim, kind, holder, weight,
|
||||
since_date: since, source, active: true, superseded_by: null,
|
||||
@@ -204,7 +218,7 @@ async function cmdAdd(engine: BrainEngine, args: string[]): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
async function cmdUpdate(engine: BrainEngine, args: string[]): Promise<void> {
|
||||
async function cmdUpdate(engine: BrainEngine, args: string[], sourceId?: string): Promise<void> {
|
||||
const slug = args[0];
|
||||
const rowNumStr = flagValue(args, '--row');
|
||||
if (!slug || !rowNumStr) {
|
||||
@@ -223,7 +237,7 @@ async function cmdUpdate(engine: BrainEngine, args: string[]): Promise<void> {
|
||||
const brainDir = await resolveBrainDir(engine, dirArg ?? null);
|
||||
|
||||
await withPageLock(slug, async () => {
|
||||
const pageId = await getPageId(engine, slug);
|
||||
const pageId = await getPageId(engine, slug, sourceId);
|
||||
await engine.updateTake(pageId, rowNum, fields);
|
||||
|
||||
// Sync the markdown table: read fence, find row, apply field updates, re-render.
|
||||
@@ -254,7 +268,7 @@ async function cmdUpdate(engine: BrainEngine, args: string[]): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
async function cmdSupersede(engine: BrainEngine, args: string[]): Promise<void> {
|
||||
async function cmdSupersede(engine: BrainEngine, args: string[], sourceId?: string): Promise<void> {
|
||||
const slug = args[0];
|
||||
const rowNumStr = flagValue(args, '--row');
|
||||
if (!slug || !rowNumStr) {
|
||||
@@ -268,7 +282,7 @@ async function cmdSupersede(engine: BrainEngine, args: string[]): Promise<void>
|
||||
const brainDir = await resolveBrainDir(engine, dirArg ?? null);
|
||||
|
||||
await withPageLock(slug, async () => {
|
||||
const pageId = await getPageId(engine, slug);
|
||||
const pageId = await getPageId(engine, slug, sourceId);
|
||||
|
||||
// Read existing row to inherit kind/holder unless overridden
|
||||
const existing = await engine.listTakes({ page_id: pageId, active: false, limit: 500 });
|
||||
@@ -302,7 +316,7 @@ async function cmdSupersede(engine: BrainEngine, args: string[]): Promise<void>
|
||||
});
|
||||
}
|
||||
|
||||
async function cmdResolve(engine: BrainEngine, args: string[]): Promise<void> {
|
||||
async function cmdResolve(engine: BrainEngine, args: string[], sourceId?: string): Promise<void> {
|
||||
const slug = args[0];
|
||||
const rowNumStr = flagValue(args, '--row');
|
||||
const qualityStr = flagValue(args, '--quality');
|
||||
@@ -347,7 +361,7 @@ async function cmdResolve(engine: BrainEngine, args: string[]): Promise<void> {
|
||||
const resolvedBy = flagValue(args, '--by') ?? 'garry';
|
||||
const dirArg = flagValue(args, '--dir');
|
||||
|
||||
const pageId = await getPageId(engine, slug);
|
||||
const pageId = await getPageId(engine, slug, sourceId);
|
||||
await engine.resolveTake(pageId, rowNum, {
|
||||
quality,
|
||||
outcome,
|
||||
@@ -564,10 +578,10 @@ Common flags:
|
||||
|
||||
switch (sub) {
|
||||
case 'search': return cmdSearch(engine, rest);
|
||||
case 'add': return cmdAdd(engine, rest);
|
||||
case 'update': return cmdUpdate(engine, rest);
|
||||
case 'supersede': return cmdSupersede(engine, rest);
|
||||
case 'resolve': return cmdResolve(engine, rest);
|
||||
case 'add': return cmdAdd(engine, rest, await resolveTakesSourceId(engine));
|
||||
case 'update': return cmdUpdate(engine, rest, await resolveTakesSourceId(engine));
|
||||
case 'supersede': return cmdSupersede(engine, rest, await resolveTakesSourceId(engine));
|
||||
case 'resolve': return cmdResolve(engine, rest, await resolveTakesSourceId(engine));
|
||||
case 'scorecard': return cmdScorecard(engine, rest);
|
||||
case 'calibration': return cmdCalibration(engine, rest);
|
||||
case 'revisit': return cmdRevisit(engine, rest);
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { runTakes } from '../src/commands/takes.ts';
|
||||
import type { BrainEngine, TakeBatchInput } from '../src/core/engine.ts';
|
||||
import { withEnv } from './helpers/with-env.ts';
|
||||
|
||||
const tmpRoots: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of tmpRoots.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function makeEngine() {
|
||||
const added: TakeBatchInput[][] = [];
|
||||
const pageLookups: unknown[][] = [];
|
||||
const engine = {
|
||||
getConfig: async () => null,
|
||||
executeRaw: async (sql: string, params: unknown[] = []) => {
|
||||
if (sql.includes('FROM sources WHERE id = $1')) {
|
||||
return [{ id: params[0] as string }];
|
||||
}
|
||||
if (sql.includes('FROM pages WHERE slug = $1 AND source_id = $2')) {
|
||||
pageLookups.push(params);
|
||||
if (params[0] === 'shared/page' && params[1] === 'dept') return [{ id: 22 }];
|
||||
if (params[0] === 'shared/page' && params[1] === 'default') return [{ id: 11 }];
|
||||
return [];
|
||||
}
|
||||
if (sql.includes('FROM pages WHERE slug = $1 LIMIT 1')) {
|
||||
pageLookups.push(params);
|
||||
return [{ id: 11 }];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
addTakesBatch: async (rows: TakeBatchInput[]) => {
|
||||
added.push(rows);
|
||||
return rows.length;
|
||||
},
|
||||
} as unknown as BrainEngine;
|
||||
return { engine, added, pageLookups };
|
||||
}
|
||||
|
||||
describe('gbrain takes CLI source scoping', () => {
|
||||
test('add mirrors to the page in GBRAIN_SOURCE, not an arbitrary same-slug page (#2684)', async () => {
|
||||
const brainDir = mkdtempSync(join(tmpdir(), 'gbrain-takes-source-'));
|
||||
const home = mkdtempSync(join(tmpdir(), 'gbrain-takes-home-'));
|
||||
tmpRoots.push(brainDir, home);
|
||||
const { engine, added, pageLookups } = makeEngine();
|
||||
|
||||
await withEnv({ GBRAIN_SOURCE: 'dept', GBRAIN_HOME: home }, async () => {
|
||||
await runTakes(engine, [
|
||||
'add',
|
||||
'shared/page',
|
||||
'--claim',
|
||||
'Dept-scoped claim',
|
||||
'--kind',
|
||||
'take',
|
||||
'--who',
|
||||
'self',
|
||||
'--dir',
|
||||
brainDir,
|
||||
]);
|
||||
});
|
||||
|
||||
expect(pageLookups).toEqual([['shared/page', 'dept']]);
|
||||
expect(added).toHaveLength(1);
|
||||
expect(added[0]![0]!.page_id).toBe(22);
|
||||
|
||||
const written = join(brainDir, 'shared/page.md');
|
||||
expect(existsSync(written)).toBe(true);
|
||||
expect(readFileSync(written, 'utf-8')).toContain('Dept-scoped claim');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user