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

feat: add length governance models

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

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

@@ -18,6 +18,11 @@ import {
   RuleStackSchema,
   ChapterTraceSchema,
 } from "../models/input-governance.js";
+import {
+  LengthSpecSchema,
+  LengthTelemetrySchema,
+  LengthWarningSchema,
+} from "../models/length-governance.js";
 
 // ---------------------------------------------------------------------------
 // BookConfig
@@ -635,3 +640,54 @@ describe("ChapterTraceSchema", () => {
     expect(result.notes).toEqual([]);
   });
 });
+
+describe("Length governance schemas", () => {
+  it("accepts a conservative length spec", () => {
+    const result = LengthSpecSchema.parse({
+      target: 2200,
+      softMin: 1900,
+      softMax: 2500,
+      hardMin: 1600,
+      hardMax: 2800,
+      countingMode: "zh_chars",
+      normalizeMode: "compress",
+    });
+
+    expect(result.target).toBe(2200);
+    expect(result.softMin).toBe(1900);
+    expect(result.normalizeMode).toBe("compress");
+  });
+
+  it("accepts telemetry for a chapter length pass", () => {
+    const result = LengthTelemetrySchema.parse({
+      target: 2200,
+      softMin: 1900,
+      softMax: 2500,
+      hardMin: 1600,
+      hardMax: 2800,
+      countingMode: "en_words",
+      writerCount: 2600,
+      postWriterNormalizeCount: 2450,
+      postReviseCount: 2480,
+      finalCount: 2480,
+      normalizeApplied: true,
+      lengthWarning: false,
+    });
+
+    expect(result.writerCount).toBe(2600);
+    expect(result.normalizeApplied).toBe(true);
+  });
+
+  it("accepts a warning when final length drifts outside the hard band", () => {
+    const result = LengthWarningSchema.parse({
+      chapter: 12,
+      target: 2200,
+      actual: 3100,
+      countingMode: "zh_chars",
+      reason: "chapter exceeded the hard max after one normalization pass",
+    });
+
+    expect(result.chapter).toBe(12);
+    expect(result.actual).toBe(3100);
+  });
+});

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

@@ -7,6 +7,7 @@ export { type GenreProfile, type ParsedGenreProfile, GenreProfileSchema, parseGe
 export { type BookRules, type ParsedBookRules, BookRulesSchema, parseBookRules } from "./models/book-rules.js";
 export { type DetectionHistoryEntry, type DetectionStats } from "./models/detection.js";
 export { type StyleProfile } from "./models/style-profile.js";
+export { type LengthCountingMode, type LengthNormalizeMode, type LengthSpec, type LengthTelemetry, type LengthWarning, LengthCountingModeSchema, LengthNormalizeModeSchema, LengthSpecSchema, LengthTelemetrySchema, LengthWarningSchema } from "./models/length-governance.js";
 export {
   type ChapterConflict,
   type ChapterIntent,

+ 46 - 0
packages/core/src/models/length-governance.ts

@@ -0,0 +1,46 @@
+import { z } from "zod";
+
+export const LengthCountingModeSchema = z.enum(["zh_chars", "en_words"]);
+export type LengthCountingMode = z.infer<typeof LengthCountingModeSchema>;
+
+export const LengthNormalizeModeSchema = z.enum(["expand", "compress", "none"]);
+export type LengthNormalizeMode = z.infer<typeof LengthNormalizeModeSchema>;
+
+export const LengthSpecSchema = z.object({
+  target: z.number().int().min(1),
+  softMin: z.number().int().min(1),
+  softMax: z.number().int().min(1),
+  hardMin: z.number().int().min(1),
+  hardMax: z.number().int().min(1),
+  countingMode: LengthCountingModeSchema,
+  normalizeMode: LengthNormalizeModeSchema,
+});
+
+export type LengthSpec = z.infer<typeof LengthSpecSchema>;
+
+export const LengthTelemetrySchema = z.object({
+  target: z.number().int().min(1),
+  softMin: z.number().int().min(1),
+  softMax: z.number().int().min(1),
+  hardMin: z.number().int().min(1),
+  hardMax: z.number().int().min(1),
+  countingMode: LengthCountingModeSchema,
+  writerCount: z.number().int().min(0),
+  postWriterNormalizeCount: z.number().int().min(0),
+  postReviseCount: z.number().int().min(0),
+  finalCount: z.number().int().min(0),
+  normalizeApplied: z.boolean(),
+  lengthWarning: z.boolean(),
+});
+
+export type LengthTelemetry = z.infer<typeof LengthTelemetrySchema>;
+
+export const LengthWarningSchema = z.object({
+  chapter: z.number().int().min(1),
+  target: z.number().int().min(1),
+  actual: z.number().int().min(0),
+  countingMode: LengthCountingModeSchema,
+  reason: z.string().min(1),
+});
+
+export type LengthWarning = z.infer<typeof LengthWarningSchema>;