AI change proposals (ai-patch)
Let AI suggest edits to a stored feature.ts — and keep a human between
suggestion and source. A small change-proposal feature reads the current
source server-side from pattern-storage, bridges into the enterprise
ai-patch feature via ctx.query, and stores the suggested changes as a
reviewable proposal. Nothing is ever applied automatically.
What it shows
Section titled “What it shows”- Server-side source lookup, like the Designer — the client only names
the path; the handler resolves it through pattern-storage’s deterministic
(tenantId, path)aggregate-id and hands ai-patch the CURRENT stored source. Stale client copies can’t sneak into the prompt. - Proposals are data — the model’s
PatternChange[]lands verbatim in ajsonbfield next to the aggregated rationale; the integration test proves the round-trip. The review queue is just the entity’s list/detail handlers; there is deliberately no free-form create. - Handling the result union explicitly —
patchis stored for review,clarifyhands the model’s question back as a structured422,errorrejects without storing anything. - Access stays honest — ai-patch and pattern-storage are both
admin-gated, and
ctx.queryruns as the current user, so the bridging handler carries the same gate instead of escalating throughqueryAs.
When to reach for it
Section titled “When to reach for it”Whenever AI-suggested code changes need an approval step — Designer-style
editing flows, migration assistants, config-as-code tooling. Pair it with the
ai-generate recipe: generate creates the file, patch evolves it, a human
reviews both.
Wiring
Section titled “Wiring”ai-patch requires ai-foundation (provider host); pattern-storage
requires config and tenant. Mount an LLM provider plugin
(ai-provider-anthropic, ai-provider-openai-compat, or a mock in tests)
alongside. The integration test under src/__tests__/ boots that exact stack
against real Postgres + Redis, seeds a pattern file over HTTP, and scripts
the provider, so every assertion above is proven, not described.
bun --env-file=../.env test --config=bunfig.integration.toml samples/recipes/ai-patchSource code
Section titled “Source code”The complete feature — embedded straight from the source file, so the code here is exactly what runs:
// AI-Patch Sample// Shows: a change-proposal queue on top of the enterprise `ai-patch` feature.// The handler reads the CURRENT source server-side from pattern-storage// (exactly like the Designer does — the client only names the path), asks// ai-patch for suggested changes, and stores them as a proposal for human// review. Nothing is ever applied automatically.
import { createEntity, createEntityExecutor, createJsonbField, createLongTextField, createTextField, defineEntityDetailHandler, defineEntityListHandler, defineFeature,} from "@cosmicdrift/kumiko-framework/engine";import { failNotFound, failUnprocessable } from "@cosmicdrift/kumiko-framework/errors";import { aiPatchFeature, SUGGEST_HANDLER_QN, type SuggestPatchResult,} from "@cosmicdriftgamestudio/kumiko-ai-patch";import { patternFileAggregateId, patternStorageFeature,} from "@cosmicdriftgamestudio/kumiko-pattern-storage";import { z } from "zod";
const PATTERN_DETAIL_QN = "pattern-storage:query:pattern-file:detail";
export const proposalEntity = createEntity({ table: "read_sample_change_proposals", fields: { path: createTextField({ required: true }), prompt: createLongTextField({ required: true, maxLength: 10_000 }), rationale: createLongTextField({ maxLength: 10_000 }), // The model's suggested PatternChange list, verbatim — the reviewer needs // to see WHAT would change, so this is data, not a display artifact. changes: createJsonbField(), },});
// ai-patch's query and pattern-storage's reads are both gated to// TenantAdmin/SystemAdmin, and ctx.query runs as the CURRENT user — so the// bridging handler carries the same gate instead of escalating.const adminOnly = { access: { roles: ["TenantAdmin", "SystemAdmin"] } } as const;
export const changeProposalsFeature = defineFeature("change-proposals", (r) => { r.requires(aiPatchFeature.name); r.requires(patternStorageFeature.name); r.entity("change-proposal", proposalEntity);
const { executor } = createEntityExecutor("change-proposal", proposalEntity);
// Proposals are only born through `propose` — the review queue exposes // read handlers, no free-form create. r.queryHandler(defineEntityListHandler("change-proposal", proposalEntity, adminOnly)); r.queryHandler(defineEntityDetailHandler("change-proposal", proposalEntity, adminOnly));
// The point of this sample: name a stored file + describe the change, // get a reviewable proposal back. r.writeHandler( "propose", z.object({ path: z.string().min(1), prompt: z.string().min(1) }), async (event, ctx) => { // Server-side source lookup, like the Designer: the client names the // path; the deterministic (tenantId, path) aggregate-id finds the file. const aggregateId = patternFileAggregateId(event.user.tenantId, event.payload.path); const file = (await ctx.query(PATTERN_DETAIL_QN, { id: aggregateId })) as { source?: unknown; } | null; // boundary: ctx.query erases the target handler's return type if (!file || typeof file.source !== "string") { return failNotFound("pattern-file", event.payload.path); }
// Cross-feature bridge: ai-patch sees the CURRENT stored source, not // whatever the client believes the source is. const result = (await ctx.query(SUGGEST_HANDLER_QN, { featureSourcePath: event.payload.path, currentFeatureSource: file.source, userPrompt: event.payload.prompt, })) as SuggestPatchResult; // boundary: ctx.query erases the target handler's return type
switch (result.type) { case "patch": // Store the suggestion for review; applying it stays a separate, // human-triggered step. return executor.create( { path: event.payload.path, prompt: event.payload.prompt, rationale: result.rationale, changes: result.changes, }, event.user, ctx.db, ); case "clarify": return failUnprocessable("proposal needs clarification", { question: result.question, reason: result.reason, }); case "error": return failUnprocessable("proposal failed", { reason: result.reason }); } }, adminOnly, );});Enterprise recipe — samples/recipes/ai-patch/src/feature.ts in the private kumiko-enterprise workspace.