rules

package
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 13, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package rules 实现用户偏好的持久化输入层(Policy)。

Rule 是第四类事实,跟 Progress / Checkpoint / Artifact 并列,但性质相反: 前三类是系统输出,Rule 是用户意图的持久化输入。

设计约束(不可妥协):

  • 工具只返事实,不返指令(Violation 是事实,由 editor 决定是否触发重写)
  • 不引入新的 verdict 路径(复用 PendingRewrites)
  • 不引入严格度字段(severity 由规则类型固定映射,editor 自主语义裁定)
  • 不静默吞冲突(所有异常进 Bundle.Conflicts,让 LLM 与 /diag 可见)
  • 不动 Flow Router(rule 不参与路由)

Index

Constants

View Source
const ChapterWordsDeviationThreshold = 0.20

ChapterWordsDeviationThreshold 定义 chapter_words 偏差升级为 error 的临界值(20%)。

Variables

This section is empty.

Functions

func DefaultHomeRulesDir added in v0.6.0

func DefaultHomeRulesDir() string

DefaultHomeRulesDir 拼出 ~/.ainovel/rules/ 目录的绝对路径。 home 解析失败返回空串(调用方据此跳过该来源)。

func DefaultProjectRulesPath

func DefaultProjectRulesPath(projectDir string) string

DefaultProjectRulesPath 拼出 ./rules.md 的绝对路径(基于给定项目目录)。 调用方传入项目根,避免在 loader 内部依赖 cwd。

func EnsureHomeRulesDir added in v0.6.0

func EnsureHomeRulesDir()

EnsureHomeRulesDir 尽力创建 ~/.ainovel/rules/ 目录并写入 README.txt 引导, 让用户发现这个全局偏好扩展点、知道怎么写。 nice-to-have,非关键路径:home 解析失败或写入出错都静默吞掉,绝不阻断启动。

Types

type Bundle

type Bundle struct {
	Structured  Structured `json:"structured"`
	Preferences string     `json:"preferences"`
	Sources     []string   `json:"sources"`
	Conflicts   []Conflict `json:"conflicts"`
}

Bundle 是合并后注入 working_memory.user_rules 的最终形态。

字段映射到 JSON 输出:

{
  "structured": {...},
  "preferences": "...合并 markdown...",
  "sources": ["..."],
  "conflicts": [...]
}

func Merge

func Merge(layers []Parsed) Bundle

Merge 把 loader 返回的多个来源合并成最终 Bundle。

合并规则:

  • 普通结构化字段:就近优先(后者覆盖前者),多来源声明同一字段且值不一致写 field_conflict
  • fatigue_words:按词合并;同一词多来源声明且阈值不一致时,就近优先并写 field_conflict
  • Markdown 正文:按来源顺序拼接,每段加来源标题,不覆盖
  • sources:所有成功加载的文件路径
  • conflicts:解析期 conflicts + 合并期 field_conflict

入参 layers 应已按 SourceKind 升序排好(loader.Load 的输出形态)。

func (Bundle) IsEmpty

func (b Bundle) IsEmpty() bool

IsEmpty 表示 Bundle 完全无内容(结构化字段为空 + 偏好正文为空)。 注入 user_rules 时仍应保留空 Bundle,避免 LLM 处理 nil。

type Conflict

type Conflict struct {
	Source string       `json:"source"`          // 文件路径(绝对或相对,按来源记录)
	Kind   ConflictKind `json:"kind"`            // 冲突类型
	Field  string       `json:"field,omitempty"` // 受影响字段名(如 forbidden_chars);parse_error 时为空
	Detail string       `json:"detail"`          // 人类可读的详情(含来源列表 / 错误信息)
}

Conflict 一条冲突或异常记录。

永远不会阻断加载——所有异常都在这里暴露给 LLM 与 /diag,不静默处理。

type ConflictKind

type ConflictKind string

ConflictKind 标记冲突或异常类型,便于 LLM 与诊断面板分类处理。

const (
	// ConflictParseError — front matter 整体解析失败;正文仍作为偏好注入。
	ConflictParseError ConflictKind = "parse_error"
	// ConflictUnknownField — 用户写了 Phase 1 未支持的字段(forward-compatible)。
	ConflictUnknownField ConflictKind = "unknown_field"
	// ConflictTypeError — 字段类型错误(如 forbidden_chars 写成字符串);该字段丢弃。
	ConflictTypeError ConflictKind = "type_error"
	// ConflictFieldConflict — 多来源同一结构化字段值不一致;就近优先生效。
	ConflictFieldConflict ConflictKind = "field_conflict"
	// ConflictInvalidValue — 字段值格式非法(如 chapter_words: "abc");该字段丢弃。
	ConflictInvalidValue ConflictKind = "invalid_value"
)

type LoadOptions

type LoadOptions struct {
	// RulesFS 是 assets/rules 子树。约定根目录直接包含 default.md。
	// 通常通过 fs.Sub(embedFS, "rules") 得到;nil 表示跳过内置规则。
	RulesFS fs.FS

	// HomeRulesDir 是 ~/.ainovel/rules/ 目录;loader 扫描其下所有顶层 .md(文件名字典序合并)。空表示跳过。
	HomeRulesDir string

	// ProjectRulesPath 是 ./rules.md(或调用方指定的项目根);空表示跳过。
	ProjectRulesPath string
}

LoadOptions 是 Load 的输入参数。

文件不存在不算错误,loader 静默跳过;解析失败不阻断,conflicts 由 parser 写入 Parsed.Conflicts。

func DefaultOptions

func DefaultOptions(rulesFS fs.FS) LoadOptions

DefaultOptions 根据当前工作目录构造常用 LoadOptions。

适合 Host 启动时调用一次,让 ContextTool / CommitChapterTool 复用同一份配置。 解析 cwd 失败时 ProjectRulesPath 留空(loader 会跳过该来源)。

路径语义:ProjectRulesPath 绑定 **当前工作目录(cwd)** 而非 outputDir。 用户 cd 到不同目录启动写不同的书,./rules.md 自然跟着 cwd 走;如需跨书共享, 放 ~/.ainovel/rules/ 全局目录即可(其下所有 .md 都会被加载)。

type Parsed

type Parsed struct {
	Source     string     // 文件路径
	Kind       SourceKind // 来源类型,用于合并优先级
	Structured Structured // 该文件声明的 front matter 字段
	Preference string     // 该文件的 Markdown 正文(front matter 之外的部分)
	Conflicts  []Conflict // 该文件解析期间产生的 conflicts(未知字段 / 类型错误)
}

Parsed 是单份 rules.md 解析后的结果。

func Load

func Load(opts LoadOptions) []Parsed

Load 按 Default → Global → Project 顺序读取,返回升序排好的 Parsed 列表。

merger 接收返回值后只需按列表顺序合并即可,后者覆盖前者。 不引入二阶段加载——Genre / Learned 等扩展层在真有内容前不开洞。

func Parse

func Parse(source string, kind SourceKind, content []byte) Parsed

Parse 解析单份 rules.md 内容(front matter + Markdown)。

容错策略:

  • front matter 整体解析失败:不阻断,正文仍作为偏好,conflicts 记录 parse_error
  • 未知字段:丢弃,conflicts 记录 unknown_field
  • 字段类型错误:丢弃该字段,conflicts 记录 type_error
  • 字段值非法(如 chapter_words 无法解析为范围):丢弃,conflicts 记录 invalid_value

source 是文件路径,仅用于 conflicts.source;kind 决定优先级。

type Severity

type Severity string

Severity 标记 Violation 的严重等级。 固定映射(用户不可配置):

forbidden_chars 出现             -> Error
forbidden_phrases 出现           -> Error
fatigue_words 超阈值             -> Warning
chapter_words 偏差 < 20%         -> Warning
chapter_words 偏差 >= 20%        -> Error
const (
	SeverityWarning Severity = "warning"
	SeverityError   Severity = "error"
)

type SourceKind

type SourceKind int

SourceKind 标记规则来源,用于合并时的就近优先排序。 值越大越就近:Project > Global > Default。

Phase 1.1 起只支持三层。Genre / Learned 层在实际题材库 / save_rule 落地前不开洞—— 真要扩展时再加常量并补 loader 即可,不留空架子。

const (
	// SourceDefault — 项目内置默认规则(assets/rules/default.md),优先级最低。
	SourceDefault SourceKind = iota
	// SourceGlobal — 用户全局偏好(~/.ainovel/rules/ 目录下所有 .md,按文件名字典序合并),跨书复用。
	SourceGlobal
	// SourceProject — 本书规则(./rules.md),优先级最高。
	SourceProject
)

func (SourceKind) String

func (k SourceKind) String() string

String 返回来源的可读名称,用于 markdown 拼接时的来源标题与 conflicts.detail。

type Structured

type Structured struct {
	Genre            string         `json:"genre,omitempty"`
	ChapterWords     *WordRange     `json:"chapter_words,omitempty"`
	ForbiddenChars   []string       `json:"forbidden_chars,omitempty"`
	ForbiddenPhrases []string       `json:"forbidden_phrases,omitempty"`
	FatigueWords     map[string]int `json:"fatigue_words,omitempty"`
}

Structured 装载 front matter 的结构化字段。

单文件解析时,Parsed.Structured 只填该文件声明的字段,其余保持零值。 合并后 Bundle.Structured 是各来源就近优先后的整体结果。

func (Structured) IsEmpty

func (s Structured) IsEmpty() bool

IsEmpty 用于判定是否完全没有结构化规则;checker 可据此跳过。

type Violation

type Violation struct {
	Rule      string   `json:"rule"`                // forbidden_chars / forbidden_phrases / fatigue_words / chapter_words
	Target    string   `json:"target,omitempty"`    // 具体违规对象(哪个词/字符);chapter_words 留空
	Limit     any      `json:"limit,omitempty"`     // 阈值;fatigue_words=int / chapter_words="3000-6000" / forbidden_*=空
	Actual    any      `json:"actual"`              // 实际值;fatigue_words/forbidden_*=出现次数 / chapter_words=本章字数
	Deviation float64  `json:"deviation,omitempty"` // chapter_words 偏差率(0~1),其他规则留空
	Severity  Severity `json:"severity"`            // error / warning
}

Violation 是 checker 的输出:本章违反了某条机械规则的事实陈述。

注意:commit_chapter 把 violations 透传到返回 JSON,不阻断 commit; editor 在审阅时把这些事实映射到现有七维(aesthetic/pacing/character/consistency), 由 LLM 自主决定是否升级 verdict 触发 polish/rewrite。

func Check

func Check(text string, wordCount int, s Structured) []Violation

Check 对章节正文按结构化规则进行机械检查,返回违规事实列表。

设计契约:

  • 仅返事实,不下指令(铁律一)
  • 不阻断任何调用方流程
  • severity 按规则类型固定映射(参见 types.go 注释表)

参数:

  • text:章节正文(终稿或草稿都可)
  • wordCount:章节字数(rune 计数)。<0 时由 checker 自行计算,避免调用方重复 O(n) 扫描。
  • s:合并后的结构化规则;IsEmpty 时直接返回 nil。

type WordRange

type WordRange struct {
	Min int `json:"min"`
	Max int `json:"max"`
}

WordRange 表示章节字数的允许范围;nil 表示未声明。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL