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
8 changes: 8 additions & 0 deletions .changeset/validation-phase3-compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@linchkit/core": minor
"@linchkit/cap-adapter-server": minor
---

Implement proposal-validation Phase 3 (compatibility / breaking-reference checks, Spec 09 §4.5). `validatePhase3` inspects a proposal's delete/narrowing changes against the current meta-model (via the OntologyRegistry impact graph + field-reference scan) and flags breaking references: deleting a field still referenced by a view/rule, deleting an action/state with dependents, changing a field type, dropping a required field's default, or removing an enum value.

Warn-only by default (does not affect `passed`); escalates to blocking errors when `strictCompatibility` is set. A new `features.strictCompatibility` env flag (default: true in production and staging, false in dev/test, mirroring `strictValidation`) wires this through `mountProposalAPI` so production and staging refuse proposals that break existing references while dev/test stays advisory. `ValidationContext` gains optional `ontology` + `strictCompatibility` fields; when absent, Phase 3 degrades to "skipped" so existing callers are unchanged.
54 changes: 47 additions & 7 deletions addons/adapter-server/cap-adapter-server/src/proposal-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
*/

import { PatternDetector, type PatternInsight } from "@linchkit/cap-ai-provider";
import type { ExecutionLogger, ProposalDefinition } from "@linchkit/core";
import { createProposalEngine, type ProposalEngine } from "@linchkit/core/server";
import type { ExecutionLogger, OntologyRegistry, ProposalDefinition } from "@linchkit/core";
import {
createProposalEngine,
type ProposalEngine,
type ValidationContext,
} from "@linchkit/core/server";

// ── Types ─────────────────────────────────────────────────

Expand Down Expand Up @@ -164,15 +168,51 @@ async function scanInsights(executionLogger: ExecutionLogger): Promise<AIInsight

// ── Mount endpoints onto Elysia app ──────────────────────

/** Extra wiring for proposal validation (Phase 3 compatibility checks). */
export interface MountProposalAPIOptions {
/** Execution logger for real PatternDetector analysis. */
executionLogger?: ExecutionLogger;
/**
* Current meta-model view used by validation Phase 3 to detect breaking
* references. When absent, Phase 3 degrades to "skipped".
*/
ontology?: OntologyRegistry;
/**
* Escalate Phase 3 breaking-reference findings from WARN to BLOCK. Sourced
* from `detectEnvironment().features.strictCompatibility` (true in prod/staging).
*/
strictCompatibility?: boolean;
}

/**
* Register proposal/evolution/insights REST endpoints on the given Elysia app.
* Call this in createServer() after the main app is created.
*
* @param app Elysia app instance
* @param executionLogger Optional execution logger for real PatternDetector analysis
* @param options Execution logger + Phase 3 compatibility wiring (also accepts a
* bare ExecutionLogger for backward compatibility).
*/
// biome-ignore lint/suspicious/noExplicitAny: Elysia plugin typing
export function mountProposalAPI(app: any, executionLogger?: ExecutionLogger): void {
export function mountProposalAPI(
// biome-ignore lint/suspicious/noExplicitAny: Elysia plugin typing
app: any,
options?: MountProposalAPIOptions | ExecutionLogger,
): void {
// Backward-compatible: a bare ExecutionLogger may still be passed positionally.
// Discriminate by its `log` method (MountProposalAPIOptions has no such field).
const isLogger = (v: unknown): v is ExecutionLogger =>
typeof (v as { log?: unknown } | undefined)?.log === "function";
const opts: MountProposalAPIOptions = isLogger(options)
? { executionLogger: options }
: (options ?? {});
const executionLogger = opts.executionLogger;

// ValidationContext threaded into both submit sites so Phase 3 can run.
// Built once; when ontology is absent Phase 3 returns "skipped" (unchanged).
const validationContext: ValidationContext = {
ontology: opts.ontology,
strictCompatibility: opts.strictCompatibility,
};

// Run initial pattern scan in background if execution logger is available
if (executionLogger) {
scanInsights(executionLogger).catch(() => {
Expand Down Expand Up @@ -231,7 +271,7 @@ export function mountProposalAPI(app: any, executionLogger?: ExecutionLogger): v

// If draft, auto-submit first
if (proposal.status === "draft") {
proposalEngine.submitProposal({ proposalId: params.id });
proposalEngine.submitProposal({ proposalId: params.id, context: validationContext });
}

// Re-fetch to check validation result
Expand Down Expand Up @@ -276,7 +316,7 @@ export function mountProposalAPI(app: any, executionLogger?: ExecutionLogger): v

// If draft, auto-submit first
if (proposal.status === "draft") {
proposalEngine.submitProposal({ proposalId: params.id });
proposalEngine.submitProposal({ proposalId: params.id, context: validationContext });
}

const refreshed = proposalEngine.getProposal(params.id);
Expand Down
9 changes: 8 additions & 1 deletion addons/adapter-server/cap-adapter-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,14 @@ export function createServer(
mountDeployRoutes(app, opts.deployWebhookHandler);

// ── Proposal / Evolution / AI Insights endpoints ──────────────
mountProposalAPI(app, executionLogger);
// Thread the ontology + the env compatibility policy so proposal validation
// Phase 3 (Spec 09 §4.5) can detect breaking references. strictCompatibility
// blocks breaking proposals in prod/staging; dev/test stay warn-only.
mountProposalAPI(app, {
executionLogger,
ontology: opts.ontologyRegistry,
strictCompatibility: environment.features.strictCompatibility,
});

// ── SSE Subscription endpoint (/api/subscribe) ────────────────
mountSubscriptionRoutes(app, opts);
Expand Down
1 change: 1 addition & 0 deletions packages/core/__tests__/barrel-public-surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ const EXPECTED_SERVER_EXPORTS = [
"validateAIOutput",
"validateAIProposal",
"validatePhase1",
"validatePhase3",
"validateProposal",
"validateProposalExtended",
"validateRequiredEnvVars",
Expand Down
Loading
Loading