package memory import "strings" // 本文件是**提取器产出侧的时间性元数据位**(`temporal`,迁移 0027):它回答 // 「这条记忆说的是**一次发生的事**,还是**反复 / 长期成立的事**」。 // // 为什么需要这一位(M37 复审的结论,逐字):「真正卡住上游的是**提取器的改写**: // 一次性事件被写成 30~80 字的第三人称长期陈述(同时带『近期/目前』和持久词), // 规则闸看不到『这是今天的一次性事件』这一位。这不是阈值问题,是**判定链缺信息位**。」 // // 🔴 **这一位只接管 N6 的「位置判据」**(2026-10-27 r1 复审裁定):词项规则里唯一 // 真正不可靠的是 **N6 的「时点词必须出现在句首 12 字内」**——提取器改写出的第三人称 // 长句本来就把时点埋在中段,窗口再放宽(12→24)就会连「同一条长期约定的重复陈述」 // 一起命中并删掉约定(M37 已量:2→14,多出的 11 条逐条都是约定)。因此: // // - 模型说 `one_off` ⇒ **N6 忽略位置窗口**(时点词出现在哪儿都算锚点); // - **N6 的全部语义安全阀照旧**(习惯词 / 硬性长期事实词 / 具体约束 / 永久化 / // 状态改变 / 寒暄情绪)—— 它们判的是「这句话不止是一个时刻」,与位置无关; // - N1~N5、N7、护栏①② 一字不动。 // // 🔴 r1 复审推翻的两条自造设计(别再写回来): // // ① **不要**给 `one_off` 再叠一层「约定/长期事实」词表否决 —— 它既把该救的行一起 // 杀了(「昨晚…睡得比平时早」被习惯词杀掉、「今天就终于把 bug 修好了,以后不用 // 再惦记」被永久化标记杀掉,而这两条正是这一位该救的),又挡不住同义异形的 // 约定(`说定`/`约了` 不在任何词表里)—— 词面否决在**两个方向**上都不安全; // ② **不要**用「句首时点词且不触发任何阀门」当上界 —— 那种行 N6 本来就判 temp, // 拿它当上界是同义反复,恰好把本片要救的「时点词不在句首」整类排除在外。 // // ⚠️ **已知代价(2026-10-27 生产重放实测,r1 复审要求如实登记,不许美化)**: // 位置窗口今天**正是**那批「约定 + 某一次没走成」行的唯一保护(它们的时点词都埋在 // 中段)。放开位置之后: // // - 最近 500 条 active 里**能翻的 13 条逐条都是这种约定**(`约定/约好 + 今天没走成`); // - 全量 3694 条里能翻 26 条,其中 13 条是约定,另 13 条是「项目/宿舍/换装/探针」 // 这类状态行; // - 而 r1 复审点名的两条目标行(`昨晚…睡得比平时早` 被习惯词、 // `今天就终于把 bug 修好了,以后不用再惦记` 被永久化标记)**本片救不了** —— // 它们是被语义安全阀挡的,与位置无关。 // // 也就是说:**在当前语料上,这一位没有可测收益,风险集中在长期约定上。** 是否保留 // 是产品裁决(M45 报告 §决策矩阵),不是代码判断;本文件只保证「放开位置的边界」是 // 确定的、可测的、可回退的。 // // 闭集(与迁移 0027 的列值口径逐字一致): // // one_off 一次性事件(今天 / 这次 / 某一次发生的事)⇒ 放开 N6 的位置判据 // recurring 反复发生的规律(每天 / 每周 / 每次都…)⇒ long(不进规则) // durable 长期成立的事实 / 偏好 / 约定 / 称呼 / 身份 ⇒ long(不进规则) // unknown 模型拿不准,或没给出合法值 ⇒ **没有结论**,规则照旧 // '' 尚未分型 ⇒ 与 unknown 同路(规则照旧)。它出现在两类行上: // ① 0027 之前的历史行; // ② **没做时间性判断的写入路径** —— `ManualExtractor`(「记住 X」, // 它把档位显式钉成 long,见 extract.go)、网关/管理面的手写记忆 // (`RetentionTier: long`)、整理侧回填。这些路径本来就由调用方 // 显式给档位,因此空串在这里不丢信息(档位判定第 1 步先命中)。 // // 🔴 `unknown` 与 `”` 在判定上**走同一条路**(规则照旧),但在口径上是两件事: // `unknown` 是「问过模型了,它拿不准」;`”` 是「这条路径没问过」。把两者折成一个值 // 会让「模型到底答了什么」这个观测面当场消失,而那正是本片要验证的东西。 type TemporalKind string const ( // TemporalUnset 是「尚未分型」:历史行与没做时间性判断的写入路径(见文件头注)。 // **它不是业务取值**,判定上与 TemporalUnknown 同路,但含义不同。 TemporalUnset TemporalKind = "" // TemporalOneOff 是一次性事件。它是本片唯一会**收紧**判定的时间性位:命中即 // 让 N6 忽略位置窗口(时点词出现在句子中段也算锚点)。语义安全阀照旧,因此它 // 仍然不是「无条件判 temp」——这一点是 r1 复审的裁定,别再改回去。 TemporalOneOff TemporalKind = "one_off" // TemporalRecurring 是反复发生的规律(「每天散步十分钟」「每周三开会」)。 // 「反复」= 将来还会再发生,因此不许进临时层:命中即判 long,不再跑规则。 TemporalRecurring TemporalKind = "recurring" // TemporalDurable 是长期成立的事实 / 偏好 / 约定 / 称呼 / 身份。同样判 long、 // 不再跑规则(方向仍是 fail-safe:多留一条)。 TemporalDurable TemporalKind = "durable" // TemporalUnknown 是「问过模型了,它拿不准」。**它绝不是「一次性」的同义词**: // 判定上它与未分型一样落到 N1~N7,不许因为「正文看着像今天的事」就判 temp —— // 那等于把本片要消灭的猜测重新引回判定链。 TemporalUnknown TemporalKind = "unknown" ) // valid 报告 t 是否落在闭集内(**含**未分型的空串:它是合法的列值,不是错误)。 func (t TemporalKind) valid() bool { switch t { case TemporalUnset, TemporalOneOff, TemporalRecurring, TemporalDurable, TemporalUnknown: return true default: return false } } // assigned 报告 t 是否**已由提取器分型**(即非空串)。它是「未分型」的唯一判据, // 与 RetentionTier.assigned 同族,调用方不许用 `t == TemporalUnknown` 之类的 // 比较去替代它(那是「问过但拿不准」,不是「没问过」)。 func (t TemporalKind) assigned() bool { return t != TemporalUnset } // unlocksTimepointPosition 报告这一位是否**放开 N6 的位置判据**。 // // 只有 `one_off` 会:模型已经回答了「这是某一次发生的事」,那么时点词埋在句中 // (「…整理到一半,今天下午才收尾」)就不再是「它不是一个时刻锚点」的证据。 // `unknown` / 未分型 / durable / recurring 一律不放开。 func (t TemporalKind) unlocksTimepointPosition() bool { return t == TemporalOneOff } // normalizeTemporal 把模型输出的自由文本归一到上面四个值。 // // 🔴 **只做大小写折叠与首尾空白裁剪,绝不做同义词猜测**(「一次性」「once」→ 都不认): // 认不出来的答案就是解析失败,一律落 TemporalUnknown —— 这正是任务书 // 「模型只能选那四个值,解析失败一律归 unknown(绝不猜)」的字面实现。 // 猜同义词的代价是单向的:猜成 one_off 就删记忆,猜成别的只是多留一条。 func normalizeTemporal(s string) TemporalKind { switch TemporalKind(strings.ToLower(strings.TrimSpace(s))) { case TemporalOneOff: return TemporalOneOff case TemporalRecurring: return TemporalRecurring case TemporalDurable: return TemporalDurable default: // 空值、非法值、拼写错误、模型自造的新值:全部归 unknown(**不报错**)。 // 一条候选记忆的价值不取决于它的时间性位是否写对,因此解析失败绝不 // 让整批提取失败(那会静默丢掉同批的其他候选)。 return TemporalUnknown } } // temporalTier 给出**模型直接给出的档位结论**(只由 resolveRetention 第 4 步调用)。 // // 只有 `durable` / `recurring` 有结论(long):模型已经回答了「这不是一次性事件」, // 此时再让词项规则判 temp 就是把「判定链缺的那一位」又丢掉;方向是 fail-safe(多留)。 // // `one_off` **不在这里**出结论 —— 它作为**输入**进规则层(放开 N6 的位置判据), // 语义安全阀照旧(r1 复审裁定,见文件头注);`unknown` / 未分型同样没有结论, // 规则照旧。 func temporalTier(t TemporalKind) (RetentionTier, bool) { switch t { case TemporalDurable, TemporalRecurring: return RetentionLong, true default: return RetentionTierUnset, false } }