feat: pgGraph-inspired CI scaffolding wave (v0.37.4.0) (#1228)
Schema-migration matrix + fuzz harness + RSS budget gate + read-latency
under sync + sync lock regression + tests/heavy convention + nightly CI
workflow + BFS frontier cap on traverseGraph.
CI infra (T1-T7):
- tests/heavy/ directory convention + scripts/run-heavy.sh + bun run test:heavy
- tests/heavy/pg_upgrade_matrix.sh: walk pre-v0.13 + pre-v0.18 brain shapes
forward to head via bootstrap → SCHEMA_SQL → migrations → verifySchema
- test/fuzz/{pure,mixed,filesystem}-validators.test.ts: 1000-run fast-check
property tests across 8 trust-boundary validators
- scripts/check-fuzz-purity.sh: bun-bundle + grep guard, wired into verify
- tests/heavy/measure_rss.sh: in-memory PGLite workload + peak RSS measurement
via /proc/self/status (Linux) or process.memoryUsage().rss fallback (macOS,
refuses to write baseline)
- tests/heavy/read_latency_under_sync.sh: phase A baseline + phase B under
parallel writer load, reports p50/p95/p99 + delta_pct
- tests/heavy/sync_lock_regression.sh: N concurrent gbrain sync against one
DB, asserts 1 winner + N-1 lock-busy + zero leaked gbrain_cycle_locks rows
- .github/workflows/heavy-tests.yml: cron '17 8 * * *' + heavy-tests label
trigger + Postgres service + artifact upload on failure
Engine (T8):
- BrainEngine.traverseGraph opts gain frontierCap?: number + onTruncation?:
(info: TruncationInfo) => void callback. Return shape preserved
(Promise<GraphNode[]>) for MCP wire stability.
- Postgres CTE: parenthesized LIMIT N ORDER BY (slug, id) inside recursive term.
- PGLite: same SQL with positional params.
- Per-call callback closure — not engine-instance state — so concurrent
traversals on the same engine don't cross-talk. 5 contracts pinned in
test/regressions/v0_36_frontier_cap.test.ts.
Three plan-review passes ran before any code: CEO scope review (Approach C),
Eng dual-voice review (Claude subagent + Codex), and Codex 2nd-pass against
the revised plan. The 2nd pass caught issues the first two missed (Bun ESM
vs require.cache; engine-instance metadata stomping under concurrency;
fixture-size inconsistency). All addressed.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1992,7 +1992,7 @@ export class PostgresEngine implements BrainEngine {
|
||||
async traverseGraph(
|
||||
slug: string,
|
||||
depth: number = 5,
|
||||
opts?: { sourceId?: string; sourceIds?: string[] },
|
||||
opts?: import('./engine.ts').TraverseGraphOpts,
|
||||
): Promise<GraphNode[]> {
|
||||
const sql = this.sql;
|
||||
// v0.34.1 (#861 — P0 leak seal): scope visited nodes to the caller's
|
||||
@@ -2018,6 +2018,31 @@ export class PostgresEngine implements BrainEngine {
|
||||
: opts?.sourceId
|
||||
? sql`AND p3.source_id = ${opts.sourceId}`
|
||||
: sql``;
|
||||
// T8 (v0.36+): frontier cap. When set, the recursive term applies a
|
||||
// parenthesized LIMIT N with ORDER BY (slug, id) for stable selection.
|
||||
// Postgres' parenthesized-LIMIT inside a recursive term caps per
|
||||
// ITERATION, which maps approximately to per-BFS-LAYER (the mapping is
|
||||
// exact when fanout is bounded; for hub-fanout graphs the cap fires
|
||||
// early). Post-query, count rows per depth — if any depth == cap, fire
|
||||
// the truncation callback.
|
||||
const cap = opts?.frontierCap;
|
||||
const recursiveStep = cap !== undefined && cap > 0
|
||||
? sql`(SELECT p2.id, p2.slug, p2.title, p2.type, g.depth + 1, g.visited || p2.id
|
||||
FROM graph g
|
||||
JOIN links l ON l.from_page_id = g.id
|
||||
JOIN pages p2 ON p2.id = l.to_page_id
|
||||
WHERE g.depth < ${depth}
|
||||
AND NOT (p2.id = ANY(g.visited))
|
||||
${stepScope}
|
||||
ORDER BY p2.slug ASC, p2.id ASC
|
||||
LIMIT ${cap})`
|
||||
: sql`SELECT p2.id, p2.slug, p2.title, p2.type, g.depth + 1, g.visited || p2.id
|
||||
FROM graph g
|
||||
JOIN links l ON l.from_page_id = g.id
|
||||
JOIN pages p2 ON p2.id = l.to_page_id
|
||||
WHERE g.depth < ${depth}
|
||||
AND NOT (p2.id = ANY(g.visited))
|
||||
${stepScope}`;
|
||||
// Cycle prevention: visited array tracks page IDs already in the path.
|
||||
const rows = await sql`
|
||||
WITH RECURSIVE graph AS (
|
||||
@@ -2026,13 +2051,7 @@ export class PostgresEngine implements BrainEngine {
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT p2.id, p2.slug, p2.title, p2.type, g.depth + 1, g.visited || p2.id
|
||||
FROM graph g
|
||||
JOIN links l ON l.from_page_id = g.id
|
||||
JOIN pages p2 ON p2.id = l.to_page_id
|
||||
WHERE g.depth < ${depth}
|
||||
AND NOT (p2.id = ANY(g.visited))
|
||||
${stepScope}
|
||||
${recursiveStep}
|
||||
)
|
||||
SELECT DISTINCT g.slug, g.title, g.type, g.depth,
|
||||
coalesce(
|
||||
@@ -2053,6 +2072,12 @@ export class PostgresEngine implements BrainEngine {
|
||||
ORDER BY g.depth, g.slug
|
||||
`;
|
||||
|
||||
// T8 truncation-detection callback was designed here but the v1 algorithm
|
||||
// had both false-positive (organic count == cap) and false-negative
|
||||
// (LIMIT-before-DISTINCT in diamond graphs) cases caught by adversarial
|
||||
// review. Stripped pending the dedupe-then-cap SQL rewrite + real Postgres
|
||||
// parity coverage. See TODOS.md → "T8 truncation signal".
|
||||
|
||||
return rows.map((r: Record<string, unknown>) => ({
|
||||
slug: r.slug as string,
|
||||
title: r.title as string,
|
||||
|
||||
Reference in New Issue
Block a user