Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
87 lines
2.9 KiB
JavaScript
87 lines
2.9 KiB
JavaScript
/**
|
|
* BDD Tool Handlers (ADR-0003 §C-1)
|
|
*
|
|
* MCP-side view onto the same logical tables (`bdd_specs`, `bdd_criteria`)
|
|
* that back GET /api/v3/bdd/specs. Lets agents read acceptance criteria for
|
|
* a document without hitting the HTTP layer.
|
|
*/
|
|
|
|
import { dbGet, dbAll } from '../../database/connection.js';
|
|
|
|
const BDD_SPACE_ID = 11;
|
|
const bddTableIds = new Map();
|
|
|
|
async function bddTableId(name) {
|
|
if (bddTableIds.has(name)) return bddTableIds.get(name);
|
|
const row = await dbGet(
|
|
`SELECT ut.id FROM universal_tables ut
|
|
JOIN projects p ON p.id = ut.project_id
|
|
WHERE p.space_id = ? AND ut.name = ? LIMIT 1`,
|
|
[BDD_SPACE_ID, name]
|
|
);
|
|
if (row?.id) bddTableIds.set(name, row.id);
|
|
return row?.id || null;
|
|
}
|
|
|
|
function parse(v) {
|
|
if (v == null) return {};
|
|
return typeof v === 'string' ? (() => { try { return JSON.parse(v); } catch { return {}; } })() : v;
|
|
}
|
|
|
|
export const bddToolHandlers = {
|
|
async list_bdd_specs({ source_doc_id }) {
|
|
const docId = parseInt(source_doc_id, 10);
|
|
if (!Number.isFinite(docId)) {
|
|
return { error: 'source_doc_id (integer) is required' };
|
|
}
|
|
|
|
const specsTableId = await bddTableId('bdd_specs');
|
|
const critTableId = await bddTableId('bdd_criteria');
|
|
if (!specsTableId || !critTableId) {
|
|
return { error: 'BDD_TABLES_NOT_BOOTSTRAPPED: bdd_specs/bdd_criteria missing in space 11' };
|
|
}
|
|
|
|
const specRows = await dbAll(
|
|
`SELECT id, data FROM table_rows
|
|
WHERE table_id = ? AND data->>'source_doc_id' = ?
|
|
ORDER BY id ASC`,
|
|
[specsTableId, String(docId)]
|
|
);
|
|
|
|
const specs = [];
|
|
for (const s of specRows) {
|
|
const sd = parse(s.data);
|
|
const crit = await dbAll(
|
|
`SELECT id, data FROM table_rows
|
|
WHERE table_id = ? AND data->>'spec_id' = ?
|
|
ORDER BY COALESCE((data->>'order_index')::int, id) ASC`,
|
|
[critTableId, String(s.id)]
|
|
);
|
|
specs.push({
|
|
id: s.id,
|
|
code: sd.code || sd.title || `spec-${s.id}`,
|
|
owner_user_id: sd.owner_user_id ?? null,
|
|
criteria: crit.map(c => {
|
|
const d = parse(c.data);
|
|
// ADR-0002 §8 Phase 2 — canonical given/when_clause/then with
|
|
// legacy `description` fallback when all three are empty.
|
|
const gwtPresent = !!(d.given || d.when_clause || d.when || d.then);
|
|
return {
|
|
id: c.id,
|
|
code: d.code || null,
|
|
title: d.title || null,
|
|
given: d.given || (gwtPresent ? '' : (d.description || '')),
|
|
when: d.when_clause || d.when || '',
|
|
then: d.then || (gwtPresent ? '' : (d.title || '')),
|
|
description: d.description || null,
|
|
priority: d.priority || null,
|
|
status: d.status || 'pending',
|
|
owner_user_id: d.owner_user_id ?? null,
|
|
};
|
|
}),
|
|
});
|
|
}
|
|
|
|
return { source_doc_id: docId, specs, spec_count: specs.length };
|
|
},
|
|
};
|