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

feat: add length counting helpers

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

+ 59 - 0
packages/core/src/__tests__/length-metrics.test.ts

@@ -0,0 +1,59 @@
+import { describe, it, expect } from "vitest";
+import {
+  buildLengthSpec,
+  chooseNormalizeMode,
+  countChapterLength,
+  isOutsideHardRange,
+  isOutsideSoftRange,
+} from "../utils/length-metrics.js";
+
+describe("length metrics", () => {
+  it("counts Chinese chapter length using zh_chars", () => {
+    expect(countChapterLength("他抬头看天。", "zh_chars")).toBe(6);
+  });
+
+  it("counts English chapter length using en_words", () => {
+    expect(countChapterLength("He looked at the sky.", "en_words")).toBe(5);
+  });
+
+  it("builds a conservative length spec for Chinese chapters", () => {
+    const spec = buildLengthSpec(2200, "zh");
+
+    expect(spec).toEqual({
+      target: 2200,
+      softMin: 1900,
+      softMax: 2500,
+      hardMin: 1600,
+      hardMax: 2800,
+      countingMode: "zh_chars",
+      normalizeMode: "none",
+    });
+  });
+
+  it("builds a conservative length spec for English chapters", () => {
+    const spec = buildLengthSpec(2200, "en");
+
+    expect(spec.countingMode).toBe("en_words");
+    expect(spec.softMin).toBe(1900);
+    expect(spec.softMax).toBe(2500);
+    expect(spec.hardMin).toBe(1600);
+    expect(spec.hardMax).toBe(2800);
+  });
+
+  it("detects soft and hard range drift", () => {
+    const spec = buildLengthSpec(2200, "zh");
+
+    expect(isOutsideSoftRange(1800, spec)).toBe(true);
+    expect(isOutsideSoftRange(2200, spec)).toBe(false);
+    expect(isOutsideHardRange(1500, spec)).toBe(true);
+    expect(isOutsideHardRange(2200, spec)).toBe(false);
+  });
+
+  it("chooses normalization direction from the measured length", () => {
+    const spec = buildLengthSpec(2200, "zh");
+
+    expect(chooseNormalizeMode(1800, spec)).toBe("expand");
+    expect(chooseNormalizeMode(2200, spec)).toBe("none");
+    expect(chooseNormalizeMode(2600, spec)).toBe("compress");
+  });
+});

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

@@ -70,6 +70,7 @@ export { ConsolidatorAgent } from "./agents/consolidator.js";
 export { MemoryDB, type Fact, type StoredSummary } from "./state/memory-db.js";
 export { StateValidatorAgent } from "./agents/state-validator.js";
 export { splitChapters, type SplitChapter } from "./utils/chapter-splitter.js";
+export { countChapterLength, buildLengthSpec, isOutsideSoftRange, isOutsideHardRange, chooseNormalizeMode, type LengthLanguage } from "./utils/length-metrics.js";
 export { createLogger, createStderrSink, createJsonLineSink, nullSink, type Logger, type LogSink, type LogLevel, type LogEntry } from "./utils/logger.js";
 export { loadProjectConfig, GLOBAL_CONFIG_DIR, GLOBAL_ENV_PATH } from "./utils/config-loader.js";
 export { computeAnalytics, type AnalyticsData, type TokenStats } from "./utils/analytics.js";

+ 63 - 0
packages/core/src/utils/length-metrics.ts

@@ -0,0 +1,63 @@
+import type { LengthCountingMode, LengthNormalizeMode, LengthSpec } from "../models/length-governance.js";
+
+export type LengthLanguage = "zh" | "en";
+
+const SOFT_RANGE_DELTA = 300;
+const HARD_RANGE_DELTA = 600;
+
+export function countChapterLength(
+  content: string,
+  countingMode: LengthCountingMode,
+): number {
+  const normalized = content.replace(/\r\n/g, "\n");
+
+  if (countingMode === "en_words") {
+    const words = normalized.match(/[A-Za-z0-9]+(?:'[A-Za-z0-9]+)?/g);
+    return words?.length ?? 0;
+  }
+
+  return normalized.replace(/\s+/g, "").length;
+}
+
+export function buildLengthSpec(
+  target: number,
+  language: LengthLanguage = "zh",
+): LengthSpec {
+  const softMin = Math.max(1, target - SOFT_RANGE_DELTA);
+  const softMax = target + SOFT_RANGE_DELTA;
+  const hardMin = Math.max(1, target - HARD_RANGE_DELTA);
+  const hardMax = target + HARD_RANGE_DELTA;
+
+  return {
+    target,
+    softMin,
+    softMax,
+    hardMin,
+    hardMax,
+    countingMode: language === "en" ? "en_words" : "zh_chars",
+    normalizeMode: "none",
+  };
+}
+
+export function isOutsideSoftRange(
+  count: number,
+  spec: Pick<LengthSpec, "softMin" | "softMax">,
+): boolean {
+  return count < spec.softMin || count > spec.softMax;
+}
+
+export function isOutsideHardRange(
+  count: number,
+  spec: Pick<LengthSpec, "hardMin" | "hardMax">,
+): boolean {
+  return count < spec.hardMin || count > spec.hardMax;
+}
+
+export function chooseNormalizeMode(
+  count: number,
+  spec: Pick<LengthSpec, "softMin" | "softMax">,
+): LengthNormalizeMode {
+  if (count < spec.softMin) return "expand";
+  if (count > spec.softMax) return "compress";
+  return "none";
+}