From 053003033aa51b27bd76bddcfdf97960bbf614c1 Mon Sep 17 00:00:00 2001 From: laofahai Date: Sun, 7 Jun 2026 14:38:10 +0800 Subject: [PATCH 1/3] feat(ai-provider,core): implement CodeGenerationProvider seam (G5 Phase 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit core declared a `CodeGenerationProvider` interface ("implemented by cap-ai-provider") but no implementation existed and the type was on no public barrel. This is G5 Phase 1 — the foundation for AI code generation of the irreducibly-code parts of a proposal (action / event-handler / flow logic bodies a declarative ChangeDefinition cannot express). - @linchkit/core/server now exports CodeGenerationProvider / CodeGenerationResult / ProjectContext / QualityGateRunner (type-only) so a capability can implement the seam. - @linchkit/cap-ai-provider adds createCodeGenerationProvider(ai, options): a thin adapter over the configured AIService (GLM per linchkit.config, never hardcoded). generateCode(prompt, context?) shapes context→system + prompt→user, defaults temperature 0 + taskType "code", forwards model/maxTokens/tenantId, and throws when the AIService is unconfigured. SAFETY: adds only the generator — NOT wired into any live path. It only produces candidate source as a string; never writes files, runs code, or touches the approval/graduation pipeline. Generated source still flows through validation + double human review (draft + graduation PR). "AI never modifies production directly." Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/g5-codegen-provider.md | 24 +++++ .../code-generation-provider.test.ts | 94 +++++++++++++++++++ .../src/code-generation-provider.ts | 78 +++++++++++++++ .../ai-provider/cap-ai-provider/src/index.ts | 6 +- packages/core/src/exports/server/ai.ts | 11 ++- 5 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 .changeset/g5-codegen-provider.md create mode 100644 addons/ai-provider/cap-ai-provider/__tests__/code-generation-provider.test.ts create mode 100644 addons/ai-provider/cap-ai-provider/src/code-generation-provider.ts diff --git a/.changeset/g5-codegen-provider.md b/.changeset/g5-codegen-provider.md new file mode 100644 index 000000000..150035956 --- /dev/null +++ b/.changeset/g5-codegen-provider.md @@ -0,0 +1,24 @@ +--- +"@linchkit/core": minor +"@linchkit/cap-ai-provider": minor +--- + +G5 Phase 1 — implement the `CodeGenerationProvider` seam. + +`@linchkit/core` previously declared a `CodeGenerationProvider` interface +("implemented by cap-ai-provider") but no implementation existed and the type was +not exported from any public barrel. This adds: + +- `@linchkit/core/server` now exports the `CodeGenerationProvider`, + `CodeGenerationResult`, `ProjectContext`, and `QualityGateRunner` types so a + capability can implement the seam. +- `@linchkit/cap-ai-provider` exports `createCodeGenerationProvider(ai, options)` + — a thin adapter over the configured `AIService` (GLM/zhipu/etc. per + `linchkit.config`) that turns a prompt (+ optional context) into generated + TypeScript source. + +This is the foundation for AI code generation of the irreducibly-code parts of a +proposal (action / event-handler / flow logic bodies). It only PRODUCES candidate +source as a string — it never writes files, runs code, or touches the approval / +graduation path. Generated source still flows through validation and double human +review (draft + graduation PR) before it can land. diff --git a/addons/ai-provider/cap-ai-provider/__tests__/code-generation-provider.test.ts b/addons/ai-provider/cap-ai-provider/__tests__/code-generation-provider.test.ts new file mode 100644 index 000000000..6dbde05c7 --- /dev/null +++ b/addons/ai-provider/cap-ai-provider/__tests__/code-generation-provider.test.ts @@ -0,0 +1,94 @@ +/** + * CodeGenerationProvider (G5 Phase 1) — unit tests. + * + * Verifies the thin adapter over AIService: message shaping (context→system, + * prompt→user), defaults (temperature 0, taskType "code"), option forwarding, + * and the fail-loud behaviour when the AIService is not configured. A fake + * AIService is injected — no real model is called. + */ + +import { describe, expect, test } from "bun:test"; +import type { AICompletionOptions, AICompletionResult, AIService } from "@linchkit/core"; +import { createCodeGenerationProvider } from "../src/code-generation-provider"; + +function makeAI(opts: { configured?: boolean; content?: string } = {}): { + ai: AIService; + calls: AICompletionOptions[]; +} { + const calls: AICompletionOptions[] = []; + const ai: AIService = { + configured: opts.configured ?? true, + defaultProvider: "glm", + providerNames: ["glm"], + async complete(options: AICompletionOptions): Promise { + calls.push(options); + return { + content: opts.content ?? "export const x = 1;", + usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 }, + model: "glm-4", + provider: "glm", + duration: 1, + }; + }, + }; + return { ai, calls }; +} + +describe("createCodeGenerationProvider", () => { + test("returns the completion content verbatim", async () => { + const { ai } = makeAI({ content: "export const late_fee = defineRule({});" }); + const provider = createCodeGenerationProvider(ai); + const code = await provider.generateCode("Generate a late_fee rule"); + expect(code).toBe("export const late_fee = defineRule({});"); + }); + + test("sends context as a system message and prompt as the user message", async () => { + const { ai, calls } = makeAI(); + const provider = createCodeGenerationProvider(ai); + await provider.generateCode("PROMPT", "CONTEXT"); + expect(calls).toHaveLength(1); + expect(calls[0]?.messages).toEqual([ + { role: "system", content: "CONTEXT" }, + { role: "user", content: "PROMPT" }, + ]); + expect(calls[0]?.temperature).toBe(0); + expect(calls[0]?.taskType).toBe("code"); + }); + + test("omits the system message when no context is given", async () => { + const { ai, calls } = makeAI(); + const provider = createCodeGenerationProvider(ai); + await provider.generateCode("PROMPT"); + expect(calls[0]?.messages).toEqual([{ role: "user", content: "PROMPT" }]); + }); + + test("omits the system message for whitespace-only context", async () => { + const { ai, calls } = makeAI(); + const provider = createCodeGenerationProvider(ai); + await provider.generateCode("PROMPT", " "); + expect(calls[0]?.messages).toEqual([{ role: "user", content: "PROMPT" }]); + }); + + test("forwards model / maxTokens / temperature / tenantId / taskType overrides", async () => { + const { ai, calls } = makeAI(); + const provider = createCodeGenerationProvider(ai, { + model: "advanced", + maxTokens: 2048, + temperature: 0.2, + tenantId: "t-1", + taskType: "generation", + }); + await provider.generateCode("PROMPT"); + expect(calls[0]?.model).toBe("advanced"); + expect(calls[0]?.maxTokens).toBe(2048); + expect(calls[0]?.temperature).toBe(0.2); + expect(calls[0]?.tenantId).toBe("t-1"); + expect(calls[0]?.taskType).toBe("generation"); + }); + + test("throws when the AIService is not configured", async () => { + const { ai } = makeAI({ configured: false }); + const provider = createCodeGenerationProvider(ai); + await expect(provider.generateCode("PROMPT")).rejects.toThrow(/not configured/); + }); +}); diff --git a/addons/ai-provider/cap-ai-provider/src/code-generation-provider.ts b/addons/ai-provider/cap-ai-provider/src/code-generation-provider.ts new file mode 100644 index 000000000..88c5764c8 --- /dev/null +++ b/addons/ai-provider/cap-ai-provider/src/code-generation-provider.ts @@ -0,0 +1,78 @@ +/** + * CodeGenerationProvider implementation (G5 Phase 1). + * + * Implements core's `CodeGenerationProvider` seam. The interface in + * `@linchkit/core` is documented as "implemented by cap-ai-provider", but no + * implementation existed. This is a thin adapter over the configured + * {@link AIService} (GLM / zhipu / etc. per `linchkit.config`), intended for the + * proposal code-generation pipeline that materializes the irreducibly-code parts + * of a proposal — action / event-handler / flow logic bodies that a declarative + * `ChangeDefinition` cannot express — into TypeScript source. + * + * SAFETY BOUNDARY ("AI never modifies production directly"): this only PRODUCES + * candidate source as a string. It never writes files, runs code, or touches the + * approval / graduation path. Generated source must flow through validation + * (build + quality gates) and double human review (draft review + graduation PR) + * before it can land. + */ + +import type { AICompletionOptions, AIMessage, AIService, AITaskType } from "@linchkit/core"; +import type { CodeGenerationProvider } from "@linchkit/core/server"; + +export interface CodeGenerationProviderOptions { + /** + * Model alias (`fast` / `standard` / `advanced`) or a full model id. Omit to + * let the AIService pick its configured default provider/model. + */ + model?: string; + /** Sampling temperature. Defaults to 0 — code generation should be deterministic. */ + temperature?: number; + /** Max output tokens for a single generation. */ + maxTokens?: number; + /** Task-type hint for model routing. Defaults to `"code"`. */ + taskType?: AITaskType; + /** Tenant id for BYOK (bring-your-own-key) config resolution. */ + tenantId?: string; +} + +/** + * Build a {@link CodeGenerationProvider} backed by a configured {@link AIService}. + * + * `generateCode(prompt, context?)` sends `context` as a system message (when + * non-empty) and `prompt` as the user message, returning the model's raw text + * content. It throws when the AIService is not configured, so the caller's retry + * / quality-gate loop surfaces a clear failure instead of silently treating an + * empty completion as generated code. + */ +export function createCodeGenerationProvider( + ai: AIService, + options: CodeGenerationProviderOptions = {}, +): CodeGenerationProvider { + return { + async generateCode(prompt: string, context?: string): Promise { + if (!ai.configured) { + throw new Error( + "CodeGenerationProvider: AIService is not configured — cannot generate code.", + ); + } + + const messages: AIMessage[] = []; + if (context && context.trim().length > 0) { + messages.push({ role: "system", content: context }); + } + messages.push({ role: "user", content: prompt }); + + const completion: AICompletionOptions = { + messages, + temperature: options.temperature ?? 0, + taskType: options.taskType ?? "code", + }; + if (options.model) completion.model = options.model; + if (options.maxTokens !== undefined) completion.maxTokens = options.maxTokens; + if (options.tenantId) completion.tenantId = options.tenantId; + + const result = await ai.complete(completion); + return result.content; + }, + }; +} diff --git a/addons/ai-provider/cap-ai-provider/src/index.ts b/addons/ai-provider/cap-ai-provider/src/index.ts index e41e06458..2d03b7b4e 100644 --- a/addons/ai-provider/cap-ai-provider/src/index.ts +++ b/addons/ai-provider/cap-ai-provider/src/index.ts @@ -45,7 +45,11 @@ export type { UsageEvent, } from "./anomaly-detector"; export { AnomalyDetector } from "./anomaly-detector"; - +// Code generation provider (G5 Phase 1) — implements core's CodeGenerationProvider +// seam over the configured AIService. Produces candidate source only; never +// writes/runs code (gated by validation + double human review). +export type { CodeGenerationProviderOptions } from "./code-generation-provider"; +export { createCodeGenerationProvider } from "./code-generation-provider"; // Cost Estimator export type { ModelPricing } from "./cost-estimator"; export { CostEstimator, defaultCostEstimator } from "./cost-estimator"; diff --git a/packages/core/src/exports/server/ai.ts b/packages/core/src/exports/server/ai.ts index 622cb4961..d9643389b 100644 --- a/packages/core/src/exports/server/ai.ts +++ b/packages/core/src/exports/server/ai.ts @@ -16,7 +16,16 @@ */ // AI Boundary -export type { PatternEvidence, PatternInsight, PatternType } from "../../ai"; +// Code-generation seam (G5) — type-only; the impl lives in cap-ai-provider. +export type { + CodeGenerationProvider, + CodeGenerationResult, + PatternEvidence, + PatternInsight, + PatternType, + ProjectContext, + QualityGateRunner, +} from "../../ai"; // AI Audit // AI Prompt Sanitizer // AI Intent Resolver (Spec 52 §2.2 / §2.5) From a0b3a15c317f0b8ec86376f03d838e9d0ca9ed0c Mon Sep 17 00:00:00 2001 From: laofahai Date: Sun, 7 Jun 2026 14:44:54 +0800 Subject: [PATCH 2/3] fix(ai-provider): validate prompt + guard missing completion content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address gemini-code-assist review on #494: - generateCode throws on an empty / whitespace-only prompt (before any model call) — robust input validation, avoids a wasted generation. - Guard a completion with no string `content`: throw instead of returning a non-string, so the Promise contract holds and the caller's retry / quality gate surfaces the failure. - Tests for both new failure paths (empty prompt, whitespace prompt, no content). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../code-generation-provider.test.ts | 34 +++++++++++++++++++ .../src/code-generation-provider.ts | 9 +++++ 2 files changed, 43 insertions(+) diff --git a/addons/ai-provider/cap-ai-provider/__tests__/code-generation-provider.test.ts b/addons/ai-provider/cap-ai-provider/__tests__/code-generation-provider.test.ts index 6dbde05c7..4b6d216a4 100644 --- a/addons/ai-provider/cap-ai-provider/__tests__/code-generation-provider.test.ts +++ b/addons/ai-provider/cap-ai-provider/__tests__/code-generation-provider.test.ts @@ -91,4 +91,38 @@ describe("createCodeGenerationProvider", () => { const provider = createCodeGenerationProvider(ai); await expect(provider.generateCode("PROMPT")).rejects.toThrow(/not configured/); }); + + test("throws on an empty prompt; the model is never called", async () => { + const { ai, calls } = makeAI(); + const provider = createCodeGenerationProvider(ai); + await expect(provider.generateCode("")).rejects.toThrow(/non-empty string/); + expect(calls).toHaveLength(0); + }); + + test("throws on a whitespace-only prompt; the model is never called", async () => { + const { ai, calls } = makeAI(); + const provider = createCodeGenerationProvider(ai); + await expect(provider.generateCode(" \n\t ")).rejects.toThrow(/non-empty string/); + expect(calls).toHaveLength(0); + }); + + test("throws when the completion has no text content", async () => { + const ai: AIService = { + configured: true, + defaultProvider: "glm", + providerNames: ["glm"], + // Simulate a provider returning a result without usable text content. + async complete(): Promise { + return { + content: undefined as unknown as string, + usage: { inputTokens: 1, outputTokens: 0, totalTokens: 1 }, + model: "glm-4", + provider: "glm", + duration: 1, + }; + }, + }; + const provider = createCodeGenerationProvider(ai); + await expect(provider.generateCode("PROMPT")).rejects.toThrow(/no text content/); + }); }); diff --git a/addons/ai-provider/cap-ai-provider/src/code-generation-provider.ts b/addons/ai-provider/cap-ai-provider/src/code-generation-provider.ts index 88c5764c8..48ce1d9b7 100644 --- a/addons/ai-provider/cap-ai-provider/src/code-generation-provider.ts +++ b/addons/ai-provider/cap-ai-provider/src/code-generation-provider.ts @@ -55,6 +55,9 @@ export function createCodeGenerationProvider( "CodeGenerationProvider: AIService is not configured — cannot generate code.", ); } + if (typeof prompt !== "string" || prompt.trim().length === 0) { + throw new Error("CodeGenerationProvider: prompt must be a non-empty string."); + } const messages: AIMessage[] = []; if (context && context.trim().length > 0) { @@ -72,6 +75,12 @@ export function createCodeGenerationProvider( if (options.tenantId) completion.tenantId = options.tenantId; const result = await ai.complete(completion); + // A completion with no text content is a generation failure, not empty + // code — throw so the caller's retry / quality gate surfaces it rather than + // silently returning a non-string (the contract is Promise). + if (typeof result?.content !== "string") { + throw new Error("CodeGenerationProvider: AI returned no text content."); + } return result.content; }, }; From e012354d326bd23e4f3506b23720f90485d197fa Mon Sep 17 00:00:00 2001 From: laofahai Date: Sun, 7 Jun 2026 14:45:30 +0800 Subject: [PATCH 3/3] chore: refresh Review Threads check after resolving review threads