|
|
@@ -1,4 +1,5 @@
|
|
|
import type {
|
|
|
+ ActiveOverride,
|
|
|
ChapterTrace,
|
|
|
ContextPackage,
|
|
|
RuleStack,
|
|
|
@@ -9,9 +10,56 @@ import {
|
|
|
} from "../models/input-governance.js";
|
|
|
import type { PlanChapterOutput } from "../agents/planner.js";
|
|
|
|
|
|
+const MAX_OVERRIDE_REASON_CHARS = 80;
|
|
|
+
|
|
|
+function truncateForOverrideReason(value: string): string {
|
|
|
+ const collapsed = value.replace(/\s+/g, " ").trim();
|
|
|
+ return collapsed.length > MAX_OVERRIDE_REASON_CHARS
|
|
|
+ ? `${collapsed.slice(0, MAX_OVERRIDE_REASON_CHARS - 1)}…`
|
|
|
+ : collapsed;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Compose the per-chapter rule stack used by writer / continuity / reviser
|
|
|
+ * prompts. Source names follow the Phase 5 layout (story_frame, volume_map,
|
|
|
+ * roles/) and activeOverrides are derived from the planner's intent so the
|
|
|
+ * "Governed Control Stack" block surfaces the actual gating in effect for
|
|
|
+ * the current chapter — it used to be a static stub that ignored both
|
|
|
+ * `plan` and `chapterNumber`.
|
|
|
+ *
|
|
|
+ * Phase hotfix 6 (Option A): make this honestly dynamic instead of deleting
|
|
|
+ * it, because writer.ts (~L820/L900), continuity.ts (~L590), and
|
|
|
+ * reviser.ts (~L600) all render ruleStack.sections / activeOverrides into
|
|
|
+ * the model prompt. Removing the function would require a much larger
|
|
|
+ * prompt refactor; making it real fixes the lie at the source.
|
|
|
+ */
|
|
|
export function buildGovernedRuleStack(plan: PlanChapterOutput, chapterNumber: number): RuleStack {
|
|
|
- void plan;
|
|
|
- void chapterNumber;
|
|
|
+ const activeOverrides: ActiveOverride[] = [];
|
|
|
+
|
|
|
+ // L4 → L3: per-chapter prohibitions narrow the planning layer for this
|
|
|
+ // chapter only. mustAvoid items come from rules-reader prohibitions +
|
|
|
+ // current_focus avoid section (planner.collectMustAvoid).
|
|
|
+ for (const item of plan.intent.mustAvoid) {
|
|
|
+ activeOverrides.push({
|
|
|
+ from: "L4",
|
|
|
+ to: "L3",
|
|
|
+ target: `chapter:${chapterNumber}/mustAvoid`,
|
|
|
+ reason: truncateForOverrideReason(item),
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // L4 → L3: planner-issued style emphasis is also a per-chapter override
|
|
|
+ // on the planning layer. Style emphasis surfaces things like POV tightness
|
|
|
+ // or character-conflict focus that the writer must honor this chapter.
|
|
|
+ for (const item of plan.intent.styleEmphasis) {
|
|
|
+ activeOverrides.push({
|
|
|
+ from: "L4",
|
|
|
+ to: "L3",
|
|
|
+ target: `chapter:${chapterNumber}/styleEmphasis`,
|
|
|
+ reason: truncateForOverrideReason(item),
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
return RuleStackSchema.parse({
|
|
|
layers: [
|
|
|
{ id: "L1", name: "hard_facts", precedence: 100, scope: "global" },
|
|
|
@@ -20,8 +68,9 @@ export function buildGovernedRuleStack(plan: PlanChapterOutput, chapterNumber: n
|
|
|
{ id: "L4", name: "current_task", precedence: 70, scope: "local" },
|
|
|
],
|
|
|
sections: {
|
|
|
- hard: ["story_bible", "current_state", "book_rules"],
|
|
|
- soft: ["author_intent", "current_focus", "volume_outline"],
|
|
|
+ // Phase 5 authoritative source names (was: story_bible, volume_outline).
|
|
|
+ hard: ["story_frame", "current_state", "book_rules", "roles"],
|
|
|
+ soft: ["author_intent", "current_focus", "volume_map"],
|
|
|
diagnostic: ["anti_ai_checks", "continuity_audit", "style_regression_checks"],
|
|
|
},
|
|
|
overrideEdges: [
|
|
|
@@ -29,7 +78,7 @@ export function buildGovernedRuleStack(plan: PlanChapterOutput, chapterNumber: n
|
|
|
{ from: "L4", to: "L2", allowed: false, scope: "current_chapter" },
|
|
|
{ from: "L4", to: "L1", allowed: false, scope: "current_chapter" },
|
|
|
],
|
|
|
- activeOverrides: [],
|
|
|
+ activeOverrides,
|
|
|
});
|
|
|
}
|
|
|
|