Просмотр исходного кода

feat: add input governance runtime models

Ma 7 месяцев назад
Родитель
Сommit
59436e9267

+ 170 - 0
packages/core/src/__tests__/models.test.ts

@@ -11,6 +11,12 @@ import {
   LLMConfigSchema,
   LLMConfigSchema,
   NotifyChannelSchema,
   NotifyChannelSchema,
 } from "../models/project.js";
 } from "../models/project.js";
+import {
+  ChapterIntentSchema,
+  ContextPackageSchema,
+  RuleStackSchema,
+  ChapterTraceSchema,
+} from "../models/input-governance.js";
 
 
 // ---------------------------------------------------------------------------
 // ---------------------------------------------------------------------------
 // BookConfig
 // BookConfig
@@ -438,3 +444,167 @@ describe("NotifyChannelSchema", () => {
     ).toThrow();
     ).toThrow();
   });
   });
 });
 });
+
+// ---------------------------------------------------------------------------
+// Input Governance
+// ---------------------------------------------------------------------------
+
+describe("ChapterIntentSchema", () => {
+  it("accepts a valid chapter intent", () => {
+    const result = ChapterIntentSchema.parse({
+      chapter: 12,
+      goal: "Pull focus back to the mentor conflict",
+      outlineNode: "Volume 2 / Chapter 12",
+      mustKeep: ["Protagonist remains injured"],
+      mustAvoid: ["Do not reveal the mastermind"],
+      styleEmphasis: ["dialogue tension", "character conflict"],
+      conflicts: [
+        {
+          type: "outline_vs_focus",
+          resolution: "allow local outline deferral",
+        },
+      ],
+    });
+
+    expect(result.chapter).toBe(12);
+    expect(result.goal).toContain("mentor conflict");
+    expect(result.conflicts).toHaveLength(1);
+  });
+
+  it("defaults optional arrays to empty", () => {
+    const result = ChapterIntentSchema.parse({
+      chapter: 1,
+      goal: "Establish the protagonist's first setback",
+    });
+
+    expect(result.mustKeep).toEqual([]);
+    expect(result.mustAvoid).toEqual([]);
+    expect(result.styleEmphasis).toEqual([]);
+    expect(result.conflicts).toEqual([]);
+  });
+
+  it("rejects invalid chapter numbers", () => {
+    expect(() =>
+      ChapterIntentSchema.parse({
+        chapter: 0,
+        goal: "Bad chapter",
+      }),
+    ).toThrow();
+  });
+});
+
+describe("ContextPackageSchema", () => {
+  it("accepts selected context with provenance", () => {
+    const result = ContextPackageSchema.parse({
+      chapter: 8,
+      selectedContext: [
+        {
+          source: "story/current_focus.md",
+          reason: "Current focus requests mentor conflict recovery",
+          excerpt: "Recent chapters should center the mentor/student break.",
+        },
+        {
+          source: "story/chapter_summaries.md#10",
+          reason: "Provide prior conflict context",
+        },
+      ],
+    });
+
+    expect(result.chapter).toBe(8);
+    expect(result.selectedContext).toHaveLength(2);
+  });
+
+  it("rejects context entries without source", () => {
+    expect(() =>
+      ContextPackageSchema.parse({
+        chapter: 8,
+        selectedContext: [
+          {
+            reason: "Missing source",
+          },
+        ],
+      }),
+    ).toThrow();
+  });
+});
+
+describe("RuleStackSchema", () => {
+  it("accepts explicit layer precedence and overrides", () => {
+    const result = RuleStackSchema.parse({
+      layers: [
+        { id: "L1", name: "hard_facts", precedence: 100, scope: "global" },
+        { id: "L2", name: "author_intent", precedence: 80, scope: "book" },
+        { id: "L3", name: "planning", precedence: 60, scope: "arc" },
+        { id: "L4", name: "current_task", precedence: 70, scope: "local" },
+      ],
+      overrideEdges: [
+        { from: "L4", to: "L3", allowed: true, scope: "current_chapter" },
+        { from: "L4", to: "L2", allowed: false, scope: "current_chapter" },
+      ],
+      activeOverrides: [
+        {
+          from: "L4",
+          to: "L3",
+          target: "volume_outline.chapter_12",
+          reason: "Current focus overrides the local plan",
+        },
+      ],
+    });
+
+    expect(result.layers[0]?.id).toBe("L1");
+    expect(result.activeOverrides).toHaveLength(1);
+  });
+
+  it("defaults override lists to empty", () => {
+    const result = RuleStackSchema.parse({
+      layers: [
+        { id: "L1", name: "hard_facts", precedence: 100, scope: "global" },
+      ],
+    });
+
+    expect(result.overrideEdges).toEqual([]);
+    expect(result.activeOverrides).toEqual([]);
+  });
+
+  it("rejects empty rule stacks", () => {
+    expect(() =>
+      RuleStackSchema.parse({
+        layers: [],
+      }),
+    ).toThrow();
+  });
+});
+
+describe("ChapterTraceSchema", () => {
+  it("accepts trace metadata for planner/composer output", () => {
+    const result = ChapterTraceSchema.parse({
+      chapter: 8,
+      plannerInputs: [
+        "story/author_intent.md",
+        "story/current_focus.md",
+      ],
+      composerInputs: [
+        "story/runtime/chapter-0008.intent.md",
+      ],
+      selectedSources: [
+        "story/current_state.md",
+        "story/chapter_summaries.md#7",
+      ],
+      notes: ["current_focus locally overrides planning"],
+    });
+
+    expect(result.plannerInputs).toContain("story/author_intent.md");
+    expect(result.notes).toHaveLength(1);
+  });
+
+  it("defaults notes to empty", () => {
+    const result = ChapterTraceSchema.parse({
+      chapter: 2,
+      plannerInputs: [],
+      composerInputs: [],
+      selectedSources: [],
+    });
+
+    expect(result.notes).toEqual([]);
+  });
+});

+ 22 - 0
packages/core/src/index.ts

@@ -7,6 +7,28 @@ export { type GenreProfile, type ParsedGenreProfile, GenreProfileSchema, parseGe
 export { type BookRules, type ParsedBookRules, BookRulesSchema, parseBookRules } from "./models/book-rules.js";
 export { type BookRules, type ParsedBookRules, BookRulesSchema, parseBookRules } from "./models/book-rules.js";
 export { type DetectionHistoryEntry, type DetectionStats } from "./models/detection.js";
 export { type DetectionHistoryEntry, type DetectionStats } from "./models/detection.js";
 export { type StyleProfile } from "./models/style-profile.js";
 export { type StyleProfile } from "./models/style-profile.js";
+export {
+  type ChapterConflict,
+  type ChapterIntent,
+  type ContextSource,
+  type ContextPackage,
+  type RuleLayerScope,
+  type RuleLayer,
+  type OverrideEdge,
+  type ActiveOverride,
+  type RuleStack,
+  type ChapterTrace,
+  ChapterConflictSchema,
+  ChapterIntentSchema,
+  ContextSourceSchema,
+  ContextPackageSchema,
+  RuleLayerScopeSchema,
+  RuleLayerSchema,
+  OverrideEdgeSchema,
+  ActiveOverrideSchema,
+  RuleStackSchema,
+  ChapterTraceSchema,
+} from "./models/input-governance.js";
 
 
 // LLM
 // LLM
 export { createLLMClient, chatCompletion, chatWithTools, createStreamMonitor, PartialResponseError, type LLMClient, type LLMResponse, type LLMMessage, type ToolDefinition, type ToolCall, type AgentMessage, type ChatWithToolsResult, type StreamProgress, type OnStreamProgress } from "./llm/provider.js";
 export { createLLMClient, chatCompletion, chatWithTools, createStreamMonitor, PartialResponseError, type LLMClient, type LLMResponse, type LLMMessage, type ToolDefinition, type ToolCall, type AgentMessage, type ChatWithToolsResult, type StreamProgress, type OnStreamProgress } from "./llm/provider.js";

+ 84 - 0
packages/core/src/models/input-governance.ts

@@ -0,0 +1,84 @@
+import { z } from "zod";
+
+export const ChapterConflictSchema = z.object({
+  type: z.string().min(1),
+  resolution: z.string().min(1),
+  detail: z.string().optional(),
+});
+
+export type ChapterConflict = z.infer<typeof ChapterConflictSchema>;
+
+export const ChapterIntentSchema = z.object({
+  chapter: z.number().int().min(1),
+  goal: z.string().min(1),
+  outlineNode: z.string().optional(),
+  mustKeep: z.array(z.string()).default([]),
+  mustAvoid: z.array(z.string()).default([]),
+  styleEmphasis: z.array(z.string()).default([]),
+  conflicts: z.array(ChapterConflictSchema).default([]),
+});
+
+export type ChapterIntent = z.infer<typeof ChapterIntentSchema>;
+
+export const ContextSourceSchema = z.object({
+  source: z.string().min(1),
+  reason: z.string().min(1),
+  excerpt: z.string().optional(),
+});
+
+export type ContextSource = z.infer<typeof ContextSourceSchema>;
+
+export const ContextPackageSchema = z.object({
+  chapter: z.number().int().min(1),
+  selectedContext: z.array(ContextSourceSchema).default([]),
+});
+
+export type ContextPackage = z.infer<typeof ContextPackageSchema>;
+
+export const RuleLayerScopeSchema = z.enum(["global", "book", "arc", "local"]);
+export type RuleLayerScope = z.infer<typeof RuleLayerScopeSchema>;
+
+export const RuleLayerSchema = z.object({
+  id: z.string().min(1),
+  name: z.string().min(1),
+  precedence: z.number().int(),
+  scope: RuleLayerScopeSchema,
+});
+
+export type RuleLayer = z.infer<typeof RuleLayerSchema>;
+
+export const OverrideEdgeSchema = z.object({
+  from: z.string().min(1),
+  to: z.string().min(1),
+  allowed: z.boolean(),
+  scope: z.string().min(1),
+});
+
+export type OverrideEdge = z.infer<typeof OverrideEdgeSchema>;
+
+export const ActiveOverrideSchema = z.object({
+  from: z.string().min(1),
+  to: z.string().min(1),
+  target: z.string().min(1),
+  reason: z.string().min(1),
+});
+
+export type ActiveOverride = z.infer<typeof ActiveOverrideSchema>;
+
+export const RuleStackSchema = z.object({
+  layers: z.array(RuleLayerSchema).min(1),
+  overrideEdges: z.array(OverrideEdgeSchema).default([]),
+  activeOverrides: z.array(ActiveOverrideSchema).default([]),
+});
+
+export type RuleStack = z.infer<typeof RuleStackSchema>;
+
+export const ChapterTraceSchema = z.object({
+  chapter: z.number().int().min(1),
+  plannerInputs: z.array(z.string()),
+  composerInputs: z.array(z.string()),
+  selectedSources: z.array(z.string()),
+  notes: z.array(z.string()).default([]),
+});
+
+export type ChapterTrace = z.infer<typeof ChapterTraceSchema>;