rcs-context 0.0.0-stage → 0.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +21 -0
- package/README.md +194 -2
- package/condense/chunk.mjs +258 -0
- package/condense/ivr.mjs +104 -0
- package/contextinjector-webui/README.md +43 -0
- package/contextinjector-webui/lib/client.js +3034 -0
- package/contextinjector-webui/lib/index.mjs +1565 -0
- package/contextinjector-webui/lib/trace-read.mjs +180 -0
- package/contextinjector-webui/package.json +21 -0
- package/cordis.patch.yml +25 -0
- package/package.json +66 -4
- package/toolfold/toolfold.mjs +513 -0
- package/web/contextinjector.mjs +3139 -0
- package/web/extreason.mjs +1107 -0
- package/web/ledger-online.mjs +229 -0
- package/web/logcompiler.mjs +369 -0
- package/web/memory-durability.mjs +431 -0
- package/web/memory-janitor.mjs +551 -0
- package/web/memory-ledger-core.mjs +1331 -0
- package/web/memory-ledger.mjs +836 -0
- package/web/memory-observations.mjs +430 -0
- package/web/memory-online-bridge.mjs +688 -0
- package/web/memory-proposal-store.mjs +1054 -0
- package/web/memory-proposals.mjs +2306 -0
- package/web/memory-trace.mjs +533 -0
- package/web/memory-vec.mjs +241 -0
- package/web/memory.mjs +5801 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
// Deterministic durability policy for Cycle 2 proposals.
|
|
2
|
+
//
|
|
3
|
+
// The model is good at copying text and bad at deciding whether a user message is
|
|
4
|
+
// a standing project rule or a one-off task/question. That decision is therefore
|
|
5
|
+
// made here, from the archived text alone, before any model call and again inside
|
|
6
|
+
// the evidence validator. The model still chooses the exact quote; this module
|
|
7
|
+
// only bounds where a quote may come from and how an overlapping predicate is
|
|
8
|
+
// named.
|
|
9
|
+
//
|
|
10
|
+
// Nothing here reads a clock, a model, or mutable state. Every result is a pure
|
|
11
|
+
// function of the input text, so a replay produces the same spans and predicates.
|
|
12
|
+
|
|
13
|
+
export const DURABILITY_POLICY_VERSION = 'durable-v2'
|
|
14
|
+
|
|
15
|
+
const WHITESPACE_CHAR = /\s/
|
|
16
|
+
|
|
17
|
+
const RE = {
|
|
18
|
+
// Prompt-injection / role confusion. Explicit instructions to rewrite memory are
|
|
19
|
+
// never evidence, even when the text uses a durable-sounding label.
|
|
20
|
+
injection: [
|
|
21
|
+
/^\s*(SYSTEM|SYSTEM\s+OVERRIDE|系统指令|系统提示|SYSTEM\s+PROMPT)\s*[::]/i,
|
|
22
|
+
/(忽略|无视|忘记).{0,8}(之前|以上|前面|原有).{0,6}(规则|指令|约束|设定)/,
|
|
23
|
+
/(把|将)(这条|这句话|上面这句|此条).{0,12}(写成|记为|记录为|设为|当作).{0,8}(约束|规则|偏好|决策|事实)/,
|
|
24
|
+
],
|
|
25
|
+
assistantProse: [
|
|
26
|
+
/^\s*(我会记住|我已记住|已经记住|已为你|已完成|已处理|总结如下|根据以上|以下是)/,
|
|
27
|
+
/(我(会|将)(记住|记录)|已(经)?(记录|记住)|根据(你|您)的(要求|偏好))/,
|
|
28
|
+
],
|
|
29
|
+
codeDump: [
|
|
30
|
+
/^\s*```/,
|
|
31
|
+
/^\s*(const|let|var|function|class|import|export)\s/m,
|
|
32
|
+
],
|
|
33
|
+
question: [
|
|
34
|
+
/[??]\s*$/,
|
|
35
|
+
/(吗|呢)\s*[。.]?\s*$/,
|
|
36
|
+
// A request for the current turn ("简单些好吗", "这样可以吗") is not a standing
|
|
37
|
+
// fact even when it carries a modal like 不要. The marker can sit mid-sentence.
|
|
38
|
+
/(好吗|行吗|可以吗|好不好|成吗|行不行|可不可以)/,
|
|
39
|
+
// A choice or a direct question is not a rule even without a question mark.
|
|
40
|
+
/^\s*(?:要不要|是否要|是否应该|为什么|为何|哪种|哪个|哪一个)/,
|
|
41
|
+
/(?:用|选|采用|使用|走)[^。!?;]{0,12}还是[^。!?;]{0,16}/,
|
|
42
|
+
/(?:有何|有哪些)[^。!?;]{0,12}[??]/,
|
|
43
|
+
/(?:是什么|为什么)[^。!?;]{0,12}[??]/,
|
|
44
|
+
/(?:是否(?:还)?(?:需要|要|应该|可以|能))/,
|
|
45
|
+
/(?:不是|难道)[^。!?;]{0,20}吗/,
|
|
46
|
+
],
|
|
47
|
+
uncertain: [
|
|
48
|
+
/(我(还)?没(想好|定|确定)|还没想好|还没定|未定|待定|等有结论|再说吧|回头再说|具体方案还没定|拿不准|不确定(要|是否|用|选|改|做)|你(觉得|看)呢)/,
|
|
49
|
+
],
|
|
50
|
+
transient: [
|
|
51
|
+
/(这次|本次|临时|暂时|先试|试试看|试一下|跑一下|抓一次|测试完|先这样)/,
|
|
52
|
+
// "这条回复/这个任务" scopes an otherwise durable-looking preference or rule
|
|
53
|
+
// to the current request.
|
|
54
|
+
/(?:这|本)(?:条|轮|次|个)(?:回复|回答|任务|请求|消息)/,
|
|
55
|
+
],
|
|
56
|
+
plan: [
|
|
57
|
+
/(打算|计划|准备(?!好)|回头|下次再|以后再说|等有结论)/,
|
|
58
|
+
/(如果|假如).{0,24}(发布|上线|发版|合并|关掉|恢复原设置)/,
|
|
59
|
+
],
|
|
60
|
+
transientError: [
|
|
61
|
+
/(刚才|刚刚|这次|本次).{0,24}(报错|报了|错误|失败|超时|异常).{0,12}(重试|再试|试试|一下|再跑|看下)/,
|
|
62
|
+
/(构建|测试|运行|安装).{0,8}(失败|报错).{0,12}(重试|再跑|再试)/,
|
|
63
|
+
],
|
|
64
|
+
fileObservation: [
|
|
65
|
+
/(里写着|上写着|里已经写着|历史记录|只是历史|已经有.{0,16}(脚本|配置|文件|依赖))/,
|
|
66
|
+
/(README|package\.json|源码|文档)(里|中)\s*(写|说|提到|记录|配置)/i,
|
|
67
|
+
],
|
|
68
|
+
// A tool or runtime that is unavailable, paired in the same sentence with the
|
|
69
|
+
// substitute to use. The substitute is required: "bash 不可用" alone is a
|
|
70
|
+
// status report, and "bash 不可用,试试 pwsh" is a one-off suggestion.
|
|
71
|
+
environment: [
|
|
72
|
+
/(?:shell|终端|命令行|解释器|运行时|运行环境|工具链|pwsh|powershell|bash|cmd(?:\.exe)?|python\d*(?:\.\d+)?|node(?:\.js)?)[^。!?;;]{0,40}?(?:不可用|用不了|不能用|没装|无法使用|不支持|不适配)[^。!?;;]{0,40}?(?:得|必须|只能|应当|应该|改用|统一|一律|用)\s*(?:用|使用|走)?\s*[A-Za-z]/i,
|
|
73
|
+
],
|
|
74
|
+
assignmentOpen: [
|
|
75
|
+
/^\s*(你负责|你来做|你来|请你|请|帮我|麻烦|先|现在|这次|本次|临时|实现|修复|补足|运行|读取|写入|检查|启动|关闭|把|将|给我|重试|再试)/,
|
|
76
|
+
],
|
|
77
|
+
// A command verb at the start of the message.
|
|
78
|
+
startImperative: [
|
|
79
|
+
/^\s*(?:改成|改为|调成|换成|部署|安装|卸载|创建|新建|添加|删除|移动|重命名|读取|写入|运行|执行|检查|启动|关闭|拉取|提交(?!前|后|信息|说明)|推送(?!前|后)|合并(?!前|后)|升级|降级|提升|达到|优化|降低|减少|增加|实现|修复)/,
|
|
80
|
+
],
|
|
81
|
+
// A work chain ("…,然后重新部署") is request-scoped even when its opening clause
|
|
82
|
+
// looks like a decision ("端口改成 8080").
|
|
83
|
+
actionChain: [
|
|
84
|
+
/(然后|再|完成后|做完|之后|最后|接着)/,
|
|
85
|
+
/并(?:把|将|告诉|输出|贴|汇报|确认|运行|执行|检查|部署|安装|创建|添加|删除|修改|修复|重命名|读取|写入|提交|推送|合并|升级)/,
|
|
86
|
+
],
|
|
87
|
+
directive: [
|
|
88
|
+
/(改成|改为|调成|换成|转为|部署|安装|卸载|创建|新建|添加|删除|移动|重命名|读取|写入|运行|执行|检查|启动|关闭|拉取|提交|推送|合并|升级|降级|提升|达到|优化|降低|减少|增加|实现|修复|清理|去除|制造|写|建|搭|明确|设置|配置|监测|打包|审验|传递|解决|调集|整理|修正|跑|做|看|改|加)/,
|
|
89
|
+
],
|
|
90
|
+
// A first/second-person request frame anywhere in the message. It is only a task
|
|
91
|
+
// when paired with a directive and no standing-rule signal.
|
|
92
|
+
requestFrame: [
|
|
93
|
+
/(?:我(?:需要|要|想|希望)|你(?:需要|要|得|应该|可以)|你的任务|你要做的|我说(?:把|将|要)?|我们|请你|帮我|麻烦|给我|替我|可以做|再审|重新)/,
|
|
94
|
+
],
|
|
95
|
+
// A status report, observation, pasted log, or acknowledgement. These carry
|
|
96
|
+
// project nouns and modals but describe what happened, not what must hold.
|
|
97
|
+
statusFrame: [
|
|
98
|
+
/^\s*(?:我注意到|我发现|我的理解|从全局来看|我不得不|已经|刚刚|目前|我在|我向|啊[,,]|都没有用|没有用|还是不行|已验证|已修复|已解决|已完成|成功了|搞定了|NO[,,])/i,
|
|
99
|
+
/^\s*(?:🧠|🟢|📋|✅|\[当前任务\]|Context Inject)/i,
|
|
100
|
+
/^(?:A|B|C)[,,]\s*我们(?:已经|摸清|知道)/,
|
|
101
|
+
],
|
|
102
|
+
strongDurable: [
|
|
103
|
+
/(规则|约定|规范|标准|原则|约束)/,
|
|
104
|
+
/(记下|记住|记录一下|请记录|写进记忆|加入记忆)/,
|
|
105
|
+
/(以后|今后|往后|未来|长期|永久|始终|永远|每次|下一次)/,
|
|
106
|
+
/\b(?:add|put|keep)\s+(?:this|it|that)\s+(?:in|into|to)\s+(?:your\s+)?memory\b/i,
|
|
107
|
+
/\bremember\s+(?:that|this)\b/i,
|
|
108
|
+
/\bkeep\s+in\s+mind\b/i,
|
|
109
|
+
],
|
|
110
|
+
explicitRule: [
|
|
111
|
+
/(规则|约定|规范|标准|原则|约束)\s*[::]/,
|
|
112
|
+
/(项目|业务|接口|部署|测试|命名|结算|开发|团队|代码|提交|评测|安全|数据|协作|发布|架构|设计)[^,。;:]{0,6}(规则|约定|规范|标准|原则|约束)/,
|
|
113
|
+
/(这是|那是|作为|按照|根据|遵循)[^。;]{0,6}(规则|约定|规范|标准|原则|约束)/,
|
|
114
|
+
],
|
|
115
|
+
designConstraint: [
|
|
116
|
+
/(?:不能|不得|不可|必须|应当|应该)(?:丢失|丢|破坏|省略|跳过|绕过|混用|忽略|覆盖)/,
|
|
117
|
+
/(?:是必要的|是危险的|必不可少|必须保留|不可替代)/,
|
|
118
|
+
],
|
|
119
|
+
memo: [
|
|
120
|
+
/(记下|记住|记录一下|请记录|写进记忆|加入记忆)/,
|
|
121
|
+
/\b(?:add|put|keep)\s+(?:this|it|that)\s+(?:in|into|to)\s+(?:your\s+)?memory\b/i,
|
|
122
|
+
/\bremember\s+(?:that|this)\b/i,
|
|
123
|
+
/\bkeep\s+in\s+mind\b/i,
|
|
124
|
+
],
|
|
125
|
+
future: [
|
|
126
|
+
/(以后|今后|往后|未来|长期|永久|始终|永远|每次|下一次)/,
|
|
127
|
+
],
|
|
128
|
+
universal: [
|
|
129
|
+
/(一律|统一|总是|所有|任何|全部|默认|优先|从不|绝不)/,
|
|
130
|
+
],
|
|
131
|
+
modal: [
|
|
132
|
+
/(必须|不得|不允许|禁止|不能|(?<!要)不要|别(?!人)|只允许|只能|最多|至少|应当|应该|可回滚|全绿|不得超过|保持|视为|固定为|需要固定)/,
|
|
133
|
+
],
|
|
134
|
+
decision: [
|
|
135
|
+
/(决定|确定|定版|拍板|采用|改用|选用|迁移到|改为|改成|为准|最终方案|而不是|不引入|不再接受|不再适用|作废|撤回|取消|废弃|停用|撤销|回退|改回|不再使用)/,
|
|
136
|
+
],
|
|
137
|
+
preference: [
|
|
138
|
+
/(偏好|更喜欢|喜欢|习惯|倾向|希望|不喜欢|prefer|would rather)/i,
|
|
139
|
+
],
|
|
140
|
+
interaction: [
|
|
141
|
+
/(回答|回复|答复|沟通|先给结论|先给细节|先说明|先提问|提问|不要猜|命名|格式|风格|行文|报告|绝对路径|相对路径)/,
|
|
142
|
+
],
|
|
143
|
+
retraction: [
|
|
144
|
+
/(作废|撤回|取消|废弃|停用|撤销|不再适用|不再使用|回退|改回|不用了|不要了)/,
|
|
145
|
+
],
|
|
146
|
+
english: [
|
|
147
|
+
/\b(decided|decision|prefer|preference|must|never|always|should|requires?|required|avoid|convention|rule|constraint|from now on)\b/i,
|
|
148
|
+
],
|
|
149
|
+
domain: [
|
|
150
|
+
/(金额|折扣|优惠券|结算|测试|依赖|接口|部署|生产|字段|单位|四舍五入|端口|目录|路径|文件|版本|编码|日志|数据库|缓存|并发|安全|权限|响应|请求|API|JSON|错误|时间|时区|精度|上限|下限|类型|编号|序号|schema)/i,
|
|
151
|
+
],
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function any(patterns, text) {
|
|
155
|
+
for (const pattern of patterns) {
|
|
156
|
+
if (pattern.test(text)) return pattern
|
|
157
|
+
}
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function describe(pattern) {
|
|
162
|
+
return pattern?.source ?? null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// A period followed by whitespace and an uppercase letter/quote starts a new
|
|
166
|
+
// English sentence. A period inside a decimal, version, path, or URL is followed
|
|
167
|
+
// by a non-space character and therefore stays part of the token.
|
|
168
|
+
function asciiSentenceEnd(text, index) {
|
|
169
|
+
if (index + 1 >= text.length || !WHITESPACE_CHAR.test(text[index + 1])) return false
|
|
170
|
+
let next = index + 1
|
|
171
|
+
while (next < text.length && WHITESPACE_CHAR.test(text[next])) next++
|
|
172
|
+
if (next >= text.length) return true
|
|
173
|
+
return /[A-Z"“「(]/.test(text[next])
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Sentence spans that keep their offsets. Chinese terminators, line breaks, and
|
|
177
|
+
// unambiguous English sentence endings end a sentence; other ASCII periods do not,
|
|
178
|
+
// because a decimal/version/path can contain one.
|
|
179
|
+
export function sentenceSpans(text) {
|
|
180
|
+
if (typeof text !== 'string' || text === '') return []
|
|
181
|
+
const spans = []
|
|
182
|
+
let start = 0
|
|
183
|
+
for (let index = 0; index < text.length; index++) {
|
|
184
|
+
const char = text[index]
|
|
185
|
+
const ends = char === '\n' || char === '。' || char === '!' || char === '?' ||
|
|
186
|
+
char === ';' || char === ';' || (char === '.' && asciiSentenceEnd(text, index))
|
|
187
|
+
if (ends) {
|
|
188
|
+
const end = index + 1
|
|
189
|
+
if (text.slice(start, end).trim() !== '') spans.push({ start, end })
|
|
190
|
+
start = end
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (start < text.length && text.slice(start).trim() !== '') {
|
|
194
|
+
spans.push({ start, end: text.length })
|
|
195
|
+
}
|
|
196
|
+
return spans
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function sentenceSignals(text) {
|
|
200
|
+
return {
|
|
201
|
+
explicitRule: any(RE.explicitRule, text),
|
|
202
|
+
designConstraint: any(RE.designConstraint, text),
|
|
203
|
+
memo: any(RE.memo, text),
|
|
204
|
+
future: any(RE.future, text),
|
|
205
|
+
universal: any(RE.universal, text),
|
|
206
|
+
modal: any(RE.modal, text),
|
|
207
|
+
decision: any(RE.decision, text),
|
|
208
|
+
preference: any(RE.preference, text),
|
|
209
|
+
interaction: any(RE.interaction, text),
|
|
210
|
+
retraction: any(RE.retraction, text),
|
|
211
|
+
english: any(RE.english, text),
|
|
212
|
+
environment: any(RE.environment, text),
|
|
213
|
+
domain: any(RE.domain, text),
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function hasSignal(signals) {
|
|
218
|
+
return Object.values(signals).some(Boolean)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function strongSignal(signals) {
|
|
222
|
+
return Boolean(signals.explicitRule || signals.memo || signals.future)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function mergeSpans(units) {
|
|
226
|
+
const merged = []
|
|
227
|
+
for (const unit of units) {
|
|
228
|
+
const last = merged[merged.length - 1]
|
|
229
|
+
// Adjacent sentences must stay separate: merging them lets a predicate cue from
|
|
230
|
+
// one sentence authorize a quote from the next ("回答先给结论" becoming a
|
|
231
|
+
// constraint because the previous sentence said "项目约定").
|
|
232
|
+
if (last && last.end > unit.start) {
|
|
233
|
+
last.end = Math.max(last.end, unit.end)
|
|
234
|
+
last.reasons.push(...unit.reasons)
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
merged.push({ ...unit, reasons: [...unit.reasons] })
|
|
238
|
+
}
|
|
239
|
+
return merged
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function signalsOf(signals) {
|
|
243
|
+
return Object.entries(signals)
|
|
244
|
+
.filter(([, pattern]) => Boolean(pattern))
|
|
245
|
+
.map(([name]) => name)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Pure classification of one archived human message.
|
|
249
|
+
//
|
|
250
|
+
// `eligible` means a proposal may be generated from it. `spans` are the only text
|
|
251
|
+
// ranges a quote may occupy; a quote that crosses an ineligible sentence is
|
|
252
|
+
// refused by the validator even when the message as a whole is eligible.
|
|
253
|
+
export function analyzeDurableUtterance(text) {
|
|
254
|
+
if (typeof text !== 'string' || text.trim() === '') {
|
|
255
|
+
return {
|
|
256
|
+
eligible: false, reason: 'empty', reasons: ['empty'], signals: [], spans: [],
|
|
257
|
+
policyVersion: DURABILITY_POLICY_VERSION,
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Keep the original offsets: callers compare these spans with an indexOf result
|
|
261
|
+
// on the archived text, so trimming here would shift every unit.
|
|
262
|
+
const raw = text
|
|
263
|
+
const match = (patterns) => any(patterns, raw)
|
|
264
|
+
const injection = match(RE.injection)
|
|
265
|
+
const assistantProse = match(RE.assistantProse)
|
|
266
|
+
const codeDump = match(RE.codeDump)
|
|
267
|
+
const question = match(RE.question)
|
|
268
|
+
const uncertain = match(RE.uncertain)
|
|
269
|
+
const transient = match(RE.transient)
|
|
270
|
+
const plan = match(RE.plan)
|
|
271
|
+
const transientError = match(RE.transientError)
|
|
272
|
+
const fileObservation = match(RE.fileObservation)
|
|
273
|
+
const environment = match(RE.environment)
|
|
274
|
+
const assignmentOpen = match(RE.assignmentOpen)
|
|
275
|
+
const startImperative = match(RE.startImperative)
|
|
276
|
+
const actionChain = match(RE.actionChain)
|
|
277
|
+
const requestFrame = match(RE.requestFrame)
|
|
278
|
+
const directive = match(RE.directive)
|
|
279
|
+
const status = match(RE.statusFrame)
|
|
280
|
+
const strong = match(RE.strongDurable)
|
|
281
|
+
const future = match(RE.future)
|
|
282
|
+
const explicitRule = match(RE.explicitRule)
|
|
283
|
+
const universal = match(RE.universal)
|
|
284
|
+
const decision = match(RE.decision)
|
|
285
|
+
const interaction = match(RE.interaction)
|
|
286
|
+
const preference = match(RE.preference)
|
|
287
|
+
// A commitment outranks an action verb: "定版为 p1,不再接受" and "改用 worktree"
|
|
288
|
+
// are decisions even though they contain 提交/执行.
|
|
289
|
+
const commitment = /(决定|定版|拍板|采用|选用|选定|不再接受|不再适用|不再使用|作废|撤回|废弃|弃用|放弃|改用|迁移到|最终方案|为准)/.test(raw)
|
|
290
|
+
|
|
291
|
+
// Precedence is intentional. A transient or reported message must not become
|
|
292
|
+
// durable just because it contains a decision verb ("临时决定…", "README 里写着
|
|
293
|
+
// 决定…"), and a question must not become a fact because it contains "以后".
|
|
294
|
+
let reason = null
|
|
295
|
+
if (injection) reason = 'injection'
|
|
296
|
+
else if (assistantProse) reason = 'assistant-prose'
|
|
297
|
+
else if (codeDump) reason = 'code-dump'
|
|
298
|
+
else if (question) reason = 'question'
|
|
299
|
+
else if (uncertain) reason = 'uncertain'
|
|
300
|
+
else if (transient && !future) reason = 'transient'
|
|
301
|
+
else if (plan) reason = 'plan'
|
|
302
|
+
else if (transientError) reason = 'transient-error'
|
|
303
|
+
else if (fileObservation) reason = 'file-observation'
|
|
304
|
+
else if (status && !strong && !explicitRule && !commitment) {
|
|
305
|
+
reason = 'status-report'
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// An imperative frame is request-scoped unless the same message also carries a
|
|
309
|
+
// standing-rule signal. This is the scope criterion, not a list of task words:
|
|
310
|
+
// "帮我加一个测试文件" is work; "先给结论" is a preference; "把金额一律…" is a rule.
|
|
311
|
+
// A commitment always outranks the action frame; a standing-rule signal exempts a
|
|
312
|
+
// request frame. A bare setting change ("接口端口改为 9000") has neither an opener
|
|
313
|
+
// nor an action chain, so it stays a decision rather than becoming a task.
|
|
314
|
+
// "所有文件" is an object, not a standing scope: a universal word exempts an
|
|
315
|
+
// imperative opener ("把金额一律…") but not a first-person request ("我需要你清理
|
|
316
|
+
// 所有文件").
|
|
317
|
+
const openerFrame = Boolean(assignmentOpen || startImperative || (actionChain && directive))
|
|
318
|
+
const requestFrameTask = Boolean(requestFrame && directive)
|
|
319
|
+
const assignment = Boolean(
|
|
320
|
+
(openerFrame && !strong && !explicitRule && !universal && !interaction && !preference &&
|
|
321
|
+
!environment && !commitment) ||
|
|
322
|
+
(requestFrameTask && !strong && !explicitRule && !preference &&
|
|
323
|
+
!environment && !commitment),
|
|
324
|
+
)
|
|
325
|
+
if (reason === null && assignment) reason = 'task-assignment'
|
|
326
|
+
|
|
327
|
+
const spans = sentenceSpans(raw)
|
|
328
|
+
const units = []
|
|
329
|
+
for (const span of spans) {
|
|
330
|
+
const sentence = raw.slice(span.start, span.end)
|
|
331
|
+
const signals = sentenceSignals(sentence)
|
|
332
|
+
if (!hasSignal(signals)) continue
|
|
333
|
+
if (assignment && !strongSignal(signals)) continue
|
|
334
|
+
units.push({
|
|
335
|
+
start: span.start,
|
|
336
|
+
end: span.end,
|
|
337
|
+
reasons: signalsOf(signals),
|
|
338
|
+
text: sentence,
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// A mixed message is judged by its rule clause, not by a question/plan in another
|
|
343
|
+
// sentence. The override is limited to question/uncertain/plan; a reported file or
|
|
344
|
+
// an injection stays excluded because its authority is wrong.
|
|
345
|
+
const strongSentences = units.some(({ reasons }) => reasons.some((name) =>
|
|
346
|
+
name === 'explicitRule' || name === 'designConstraint' || name === 'memo' ||
|
|
347
|
+
name === 'future' || name === 'decision' ||
|
|
348
|
+
(name === 'modal' && reasons.includes('domain'))))
|
|
349
|
+
// A universal word in a status report ("不显示任何内容") is an object quantifier,
|
|
350
|
+
// not a standing rule; only an explicit label, a design invariant, a memo, or a
|
|
351
|
+
// future commitment lifts the status frame.
|
|
352
|
+
const ruleSentences = units.some(({ reasons }) => reasons.some((name) =>
|
|
353
|
+
name === 'explicitRule' || name === 'designConstraint' || name === 'memo' ||
|
|
354
|
+
name === 'future'))
|
|
355
|
+
// A question only yields to a sentence that states a rule outright. "我们是不是
|
|
356
|
+
// 应该改用 Postgres?" contains a decision verb but is still a question; a plain
|
|
357
|
+
// decision/future cue must not turn it into a fact.
|
|
358
|
+
const questionRule = units.some(({ reasons }) => reasons.some((name) =>
|
|
359
|
+
name === 'explicitRule' || name === 'designConstraint' || name === 'memo'))
|
|
360
|
+
if (ruleSentences && reason === 'status-report') reason = null
|
|
361
|
+
if (questionRule && reason === 'question') reason = null
|
|
362
|
+
if (strongSentences && (reason === 'uncertain' || reason === 'plan')) reason = null
|
|
363
|
+
|
|
364
|
+
if (reason === null && units.length === 0) reason = 'no-durable-signal'
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
eligible: reason === null,
|
|
368
|
+
reason,
|
|
369
|
+
reasons: reason === null ? [] : [reason],
|
|
370
|
+
signals: {
|
|
371
|
+
injection: describe(injection),
|
|
372
|
+
assistantProse: describe(assistantProse),
|
|
373
|
+
codeDump: describe(codeDump),
|
|
374
|
+
question: describe(question),
|
|
375
|
+
uncertain: describe(uncertain),
|
|
376
|
+
transient: describe(transient),
|
|
377
|
+
plan: describe(plan),
|
|
378
|
+
transientError: describe(transientError),
|
|
379
|
+
fileObservation: describe(fileObservation),
|
|
380
|
+
environment: describe(environment),
|
|
381
|
+
requestFrame: describe(requestFrame),
|
|
382
|
+
status: describe(status),
|
|
383
|
+
startImperative: describe(startImperative),
|
|
384
|
+
actionChain: describe(actionChain),
|
|
385
|
+
directive: describe(directive),
|
|
386
|
+
assignment: assignment ? 'assignment' : null,
|
|
387
|
+
strongDurable: describe(strong),
|
|
388
|
+
future: describe(future),
|
|
389
|
+
},
|
|
390
|
+
spans: mergeSpans(units).map(({ start, end, reasons }) => ({ start, end, reasons })),
|
|
391
|
+
policyVersion: DURABILITY_POLICY_VERSION,
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// The taxonomy is defined by these cues, in this order. The model's predicate is
|
|
396
|
+
// advisory: when a cue is present the deterministic label wins, because an
|
|
397
|
+
// undefined taxonomy is what produced content-correct/predicate-wrong candidates.
|
|
398
|
+
export function resolveQuotePredicate(text) {
|
|
399
|
+
if (typeof text !== 'string') return null
|
|
400
|
+
const signals = sentenceSignals(text)
|
|
401
|
+
if (signals.explicitRule) return { predicate: 'project.constraint', rule: 'explicit-rule-noun' }
|
|
402
|
+
if (signals.designConstraint) return { predicate: 'project.constraint', rule: 'design-constraint' }
|
|
403
|
+
if (signals.environment) return { predicate: 'project.constraint', rule: 'environment-cue' }
|
|
404
|
+
if (signals.preference || signals.interaction) {
|
|
405
|
+
return { predicate: 'project.preference', rule: signals.preference ? 'preference-cue' : 'interaction-cue' }
|
|
406
|
+
}
|
|
407
|
+
if (signals.decision) return { predicate: 'project.decision', rule: 'decision-cue' }
|
|
408
|
+
if (signals.english) {
|
|
409
|
+
if (/\b(prefer|preference|would rather)\b/i.test(text)) {
|
|
410
|
+
return { predicate: 'project.preference', rule: 'english-preference' }
|
|
411
|
+
}
|
|
412
|
+
if (/\b(decided|decision|chose|instead of)\b/i.test(text)) {
|
|
413
|
+
return { predicate: 'project.decision', rule: 'english-decision' }
|
|
414
|
+
}
|
|
415
|
+
return { predicate: 'project.constraint', rule: 'english-deontic' }
|
|
416
|
+
}
|
|
417
|
+
if (signals.universal || signals.modal || signals.retraction) {
|
|
418
|
+
return { predicate: 'project.constraint', rule: 'invariant-cue' }
|
|
419
|
+
}
|
|
420
|
+
// A domain noun alone ("数据库使用 SQLite") says what the statement is about,
|
|
421
|
+
// not which taxonomy bucket it belongs to. Keep the model's label rather than
|
|
422
|
+
// turning every unlabelled domain sentence into a constraint.
|
|
423
|
+
return null
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function quoteWithinSpans(spans, start, end) {
|
|
427
|
+
if (!Array.isArray(spans)) return false
|
|
428
|
+
return spans.some((span) =>
|
|
429
|
+
Number.isInteger(span.start) && Number.isInteger(span.end) &&
|
|
430
|
+
start >= span.start && end <= span.end)
|
|
431
|
+
}
|