Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 243 additions & 19 deletions mcp-server/index.js

Large diffs are not rendered by default.

115 changes: 115 additions & 0 deletions packages/meta-prompting/src/__tests__/prompt-builders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,121 @@ describe("buildSectionPrompt", () => {
expect(out).not.toContain("FORBIDDEN (do NOT apply");
});

it("renders a <codebase_grounding> block with real symbols/communities/processes when grounding is provided", () => {
const out = buildSectionPrompt({
section_type: "requirements",
feature_description: "OAuth login",
prd_context: "feature",
recall_summary: "",
clarification_qa: [],
prior_violations: [],
attempt: 1,
codebase_grounding: {
finding_summary: "OAuth touches 3 auth-layer symbols.",
matched_symbols: [
{
qualified_name: "src/auth.ts::loginHandler",
name: "loginHandler",
label: "function",
file_path: "src/auth.ts",
community_id: 7,
},
{
qualified_name: "src/session.ts::SessionStore",
name: "SessionStore",
label: "class",
file_path: "src/session.ts",
community_id: 7,
},
],
impacted_communities: ["auth", "session"],
impacted_processes: ["login_flow", "token_refresh"],
graph_stats: {
nodes: 1200,
edges: 4300,
communities: 18,
processes: 12,
},
},
});
expect(out).toContain("<codebase_grounding>");
// matched-symbol text: name + file_path + community
expect(out).toContain("loginHandler");
expect(out).toContain("src/auth.ts");
expect(out).toContain("community 7");
expect(out).toContain("SessionStore");
// impacted communities + processes
expect(out).toContain("auth");
expect(out).toContain("login_flow");
// graph stats header
expect(out).toContain("1200 nodes");
// finding summary
expect(out).toContain("OAuth touches 3 auth-layer symbols.");
});

it("caps matched symbols at 15 and reports the true total", () => {
const matched_symbols = Array.from({ length: 40 }, (_, i) => ({
qualified_name: `src/f${i}.ts::sym${i}`,
name: `sym${i}`,
file_path: `src/f${i}.ts`,
community_id: i,
}));
const out = buildSectionPrompt({
section_type: "requirements",
feature_description: "x",
prd_context: "feature",
recall_summary: "",
clarification_qa: [],
prior_violations: [],
attempt: 1,
codebase_grounding: { matched_symbols },
});
expect(out).toContain("showing 15 of 40");
expect(out).toContain("sym0");
expect(out).toContain("sym14");
// 16th symbol (index 15) must be dropped by the cap
expect(out).not.toContain("sym15 —");
});

it("omits the <codebase_grounding> block when grounding is absent (back-compat, byte-identical)", () => {
const base = {
section_type: "overview" as const,
feature_description: "x",
prd_context: "feature" as const,
recall_summary: "",
clarification_qa: [],
prior_violations: [],
attempt: 1,
};
const withoutField = buildSectionPrompt(base);
const withUndefined = buildSectionPrompt({
...base,
codebase_grounding: undefined,
});
expect(withoutField).not.toContain("<codebase_grounding>");
expect(withUndefined).not.toContain("<codebase_grounding>");
// No empty tag, and the two renderings are identical → backward compatible.
expect(withUndefined).toBe(withoutField);
});

it("omits the <codebase_grounding> block when grounding is present but carries no usable evidence", () => {
const out = buildSectionPrompt({
section_type: "overview",
feature_description: "x",
prd_context: "feature",
recall_summary: "",
clarification_qa: [],
prior_violations: [],
attempt: 1,
codebase_grounding: {
matched_symbols: [],
impacted_communities: [],
impacted_processes: [],
},
});
expect(out).not.toContain("<codebase_grounding>");
});

it("includes the COMMON_RULES that gate downstream validators", () => {
const out = buildSectionPrompt({
section_type: "requirements",
Expand Down
2 changes: 2 additions & 0 deletions packages/meta-prompting/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export {
buildSectionPrompt,
type SectionPromptInput,
type CodebaseGrounding,
type GroundedSymbol,
} from "./section-prompts.js";

export {
Expand Down
148 changes: 148 additions & 0 deletions packages/meta-prompting/src/section-prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,46 @@ import {
} from "@prd-gen/core";
import type { StrategyAssignment } from "@prd-gen/strategy";

/**
* One symbol matched against the codebase graph by automatised-pipeline's
* feature-mode `prepare_prd_input`. Only the fields the rendered grounding
* block consumes are typed; the AP payload may carry more (relationships,
* processes), which this builder deliberately does not render to keep the
* prompt token-bounded.
*
* source: AP feature-mode prepare_prd_input contract (prd_context shape),
* shipped 2026-06.
*/
export interface GroundedSymbol {
readonly qualified_name?: string;
readonly name?: string;
readonly label?: string;
readonly file_path?: string;
readonly community_id?: string | number;
}

/**
* Code-graph grounding for the feature (AP `prepare_prd_input.prd_context`).
* Every field is optional so a partial / older payload still type-checks; the
* renderer guards each field independently and emits NOTHING when the grounding
* carries no usable evidence (backward-compatible with pre-grounding callers).
*
* source: AP feature-mode prepare_prd_input contract (prd_context shape),
* shipped 2026-06.
*/
export interface CodebaseGrounding {
readonly finding_summary?: string;
readonly matched_symbols?: ReadonlyArray<GroundedSymbol>;
readonly impacted_communities?: readonly string[];
readonly impacted_processes?: readonly string[];
readonly graph_stats?: {
readonly nodes?: number;
readonly edges?: number;
readonly communities?: number;
readonly processes?: number;
};
}

export interface SectionPromptInput {
readonly section_type: SectionType;
readonly feature_description: string;
Expand All @@ -40,6 +80,20 @@ export interface SectionPromptInput {
* source: Phase 4 strategy-wiring (2026-04).
*/
readonly strategy_assignment?: StrategyAssignment;
/**
* Code-graph grounding for the feature, produced by automatised-pipeline's
* feature-mode `prepare_prd_input` (its `prd_context` payload) and threaded
* through from `PipelineState.codebase_grounding`. When present and non-empty,
* the rendered prompt gains a `<codebase_grounding>` block listing real
* matched symbols / files / communities / processes so the drafted section
* references the actual codebase rather than inventing structure.
*
* Optional: pipelines without a codebase (or predating grounding) omit it,
* and the rendered prompt is then byte-identical to before.
*
* source: AP feature-mode prepare_prd_input contract, shipped 2026-06.
*/
readonly codebase_grounding?: CodebaseGrounding;
}

const COMMON_RULES = [
Expand Down Expand Up @@ -136,6 +190,97 @@ function renderStrategiesBlock(
return lines.join("\n");
}

/**
* Size caps for the grounding block. These bound prompt tokens — a large graph
* (hundreds of matched symbols / communities) must not blow the per-section
* prompt budget. Caps mirror the intent of summarizeRecall's truncation in
* orchestration: include enough real evidence to ground the draft, drop the
* long tail.
*
* source: provisional heuristic, parallel to RECALL_MAX_RESULTS_INCLUDED=8 in
* orchestration/section-generation.ts (Phase 3+4 retrieval budget). Symbols get
* a higher cap (15) because each is one short line and they are the primary
* grounding signal; community/process name lists get 10 each.
*/
const GROUNDING_MAX_SYMBOLS = 15;
const GROUNDING_MAX_COMMUNITY_NAMES = 10;
const GROUNDING_MAX_PROCESS_NAMES = 10;

/**
* Render the `<codebase_grounding>` block. Returns "" when grounding is absent
* or carries no usable evidence (no symbols, no communities, no processes, no
* stats) — so a pipeline without grounding produces a byte-identical prompt to
* before (no empty tags). Concise on purpose: a stats header, then capped lists
* of real symbols/files/communities/processes.
*
* source: AP feature-mode prepare_prd_input contract (prd_context), 2026-06.
*/
function renderGroundingBlock(
grounding: CodebaseGrounding | undefined,
): string {
if (!grounding) return "";

const symbols = grounding.matched_symbols ?? [];
const communities = grounding.impacted_communities ?? [];
const processes = grounding.impacted_processes ?? [];
const stats = grounding.graph_stats;
const summary = grounding.finding_summary?.trim() ?? "";

const hasStats =
!!stats &&
[stats.nodes, stats.edges, stats.communities, stats.processes].some(
(n) => typeof n === "number",
);
const hasContent =
symbols.length > 0 ||
communities.length > 0 ||
processes.length > 0 ||
summary.length > 0 ||
hasStats;
if (!hasContent) return "";

const lines: string[] = [`<codebase_grounding>`];

if (summary) lines.push(summary);

if (hasStats && stats) {
lines.push(
`Graph: ${stats.nodes ?? "?"} nodes, ${stats.edges ?? "?"} edges, ` +
`${stats.communities ?? "?"} communities, ${stats.processes ?? "?"} processes.`,
);
}

if (symbols.length > 0) {
lines.push(
`Matched symbols (showing ${Math.min(symbols.length, GROUNDING_MAX_SYMBOLS)} of ${symbols.length}):`,
);
for (const s of symbols.slice(0, GROUNDING_MAX_SYMBOLS)) {
const label = s.name ?? s.qualified_name ?? "(unnamed)";
const file = s.file_path ? ` — ${s.file_path}` : "";
const community =
s.community_id !== undefined ? ` (community ${s.community_id})` : "";
lines.push(` - ${label}${file}${community}`);
}
}

if (communities.length > 0) {
const shown = communities.slice(0, GROUNDING_MAX_COMMUNITY_NAMES);
lines.push(
`Impacted communities (${communities.length}): ${shown.join(", ")}`,
);
}

if (processes.length > 0) {
const shown = processes.slice(0, GROUNDING_MAX_PROCESS_NAMES);
lines.push(
`Impacted processes (${processes.length}): ${shown.join(", ")}`,
);
}

lines.push(`</codebase_grounding>`);
return lines.join("\n");
}

export function buildSectionPrompt(input: SectionPromptInput): string {
const display = SECTION_DISPLAY_NAMES[input.section_type];
const contextConfig = PRD_CONTEXT_CONFIGS[input.prd_context];
Expand All @@ -159,6 +304,7 @@ export function buildSectionPrompt(input: SectionPromptInput): string {
: "";

const strategiesBlock = renderStrategiesBlock(input.strategy_assignment);
const groundingBlock = renderGroundingBlock(input.codebase_grounding);

return [
`<role>You draft section "${display}" of a ${contextConfig.displayName} PRD.</role>`,
Expand All @@ -174,6 +320,8 @@ export function buildSectionPrompt(input: SectionPromptInput): string {
input.recall_summary
? `<codebase_context>\n${input.recall_summary}\n</codebase_context>\n`
: "",
groundingBlock,
groundingBlock ? "" : "",
clarificationLines
? `<clarifications>\n${clarificationLines}\n</clarifications>\n`
: "",
Expand Down
Loading
Loading