@yolk_vat-y/dsh-project-memory 0.5.5 → 0.5.6
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/CHANGELOG.md +25 -2
- package/README.md +27 -4
- package/README.zh-CN.md +26 -3
- package/package.json +4 -2
- package/src/audit.js +87 -0
- package/src/auto-inject.js +148 -27
- package/src/insight-store.js +21 -1
- package/src/ops.js +210 -0
- package/src/readiness.js +279 -35
- package/src/tools/lesson-tools.js +29 -7
package/src/readiness.js
CHANGED
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
* @module dsh-project-memory/readiness
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { FALLBACK_OPS, WEAK_OPS, activityFromText, opForLegacyAction } from './ops.js'
|
|
20
|
+
import { CJK_RANGE, tokenize } from './util/search.js'
|
|
21
|
+
|
|
19
22
|
/**
|
|
20
23
|
* 动作词典:动作 id → 匹配模式。命令形态与人类意图词都归一成同一套 id,
|
|
21
24
|
* 于是"你顺便提交一下"与 `git commit -m x` 对 trigger 是同一件事。
|
|
@@ -80,7 +83,26 @@ export function buildReadinessContext(input = {}) {
|
|
|
80
83
|
const actionText = String(input.actionText || '')
|
|
81
84
|
const actions = new Set([...(input.actions || []), ...detectActions(humanText), ...detectActions(actionText)])
|
|
82
85
|
const paths = new Set([...(input.paths || []), ...extractPaths(actionText), ...extractPaths(humanText)])
|
|
83
|
-
|
|
86
|
+
// 动作平面(S1):结构化调用优先;没有结构化调用时对 actionText 跑一遍 shell 规则兜底。
|
|
87
|
+
const fromText = activityFromText(actionText)
|
|
88
|
+
const ops = new Set([...(input.ops || []), ...fromText.ops])
|
|
89
|
+
for (const a of actions) {
|
|
90
|
+
const op = opForLegacyAction(a)
|
|
91
|
+
if (op) ops.add(op)
|
|
92
|
+
}
|
|
93
|
+
const targets = new Set([...(input.targets || []), ...fromText.targets])
|
|
94
|
+
const hosts = new Set([...(input.hosts || []), ...fromText.hosts])
|
|
95
|
+
return {
|
|
96
|
+
humanText,
|
|
97
|
+
actionText,
|
|
98
|
+
actions: [...actions],
|
|
99
|
+
paths: [...paths],
|
|
100
|
+
ops: [...ops],
|
|
101
|
+
targets: [...targets],
|
|
102
|
+
hosts: [...hosts],
|
|
103
|
+
intent: intentText(humanText),
|
|
104
|
+
tags: Array.isArray(input.tags) ? [...input.tags] : [],
|
|
105
|
+
}
|
|
84
106
|
}
|
|
85
107
|
|
|
86
108
|
/**
|
|
@@ -145,12 +167,12 @@ export function backfillDerivedTriggers(doc) {
|
|
|
145
167
|
}
|
|
146
168
|
|
|
147
169
|
/**
|
|
148
|
-
* 提示通道的查询文本:剔除 1–
|
|
170
|
+
* 提示通道的查询文本:剔除 1–3 个字符的拉丁 token。
|
|
149
171
|
*
|
|
150
|
-
* 为什么:`PR` / `CI` / `OS`
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* authored trigger
|
|
172
|
+
* 为什么:`PR` / `CI` / `OS` / `WSL` / `npm` 这类缩写太短、歧义太大,一个巧合命中就能当上
|
|
173
|
+
* 该层最高分,于是以 `relative:1.00` 混进上下文(实测:人类消息里的 "PR" 把一条 task-tools
|
|
174
|
+
* 越权 lesson 顶到了提示位;"wsl" 又把一条 pnpm lesson 顶到了"改文件时间戳"的任务里)。
|
|
175
|
+
* 内容词(≥4 字符的拉丁标识符、CJK 词)不受影响;authored trigger 通道完全不走这里。
|
|
154
176
|
* @param {string} text - 人类消息或工具参数。
|
|
155
177
|
* @returns {string} 过滤后的查询文本。
|
|
156
178
|
*/
|
|
@@ -159,7 +181,7 @@ export function hintQueryText(text) {
|
|
|
159
181
|
.split(/\s+/)
|
|
160
182
|
// 路径形态的 token 原样保留(src/util/fs.js 里的 fs / js 是有效证据),
|
|
161
183
|
// 只对独立词做缩写剔除。
|
|
162
|
-
.map((w) => (/[/.]/.test(w) ? w : w.replace(/\b[A-Za-z0-9_]{1,
|
|
184
|
+
.map((w) => (/[/.]/.test(w) ? w : w.replace(/\b[A-Za-z0-9_]{1,3}\b/g, ' ')))
|
|
163
185
|
.join(' ')
|
|
164
186
|
.replace(/\s+/g, ' ')
|
|
165
187
|
.trim()
|
|
@@ -172,42 +194,264 @@ function globToRegExp(pattern) {
|
|
|
172
194
|
}
|
|
173
195
|
|
|
174
196
|
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
197
|
+
* 剥离**引用内容**:代码块、行内代码、引号内的路径/文件名、裸路径。
|
|
198
|
+
*
|
|
199
|
+
* 为什么必须剥:人类消息里的文件名是**数据**,不是意图。实测中"石啸天-LLM记忆方向调研.pptx"
|
|
200
|
+
* 这个文件名让一条"做调研要先扫 curated 列表"的经验在改文件时间戳的任务里被注入。
|
|
201
|
+
* @param {string} text - 人类消息原文
|
|
202
|
+
* @returns {string} 只保留意图文字的版本
|
|
179
203
|
*/
|
|
180
|
-
export function
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
204
|
+
export function intentText(text) {
|
|
205
|
+
return String(text || '')
|
|
206
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
207
|
+
.replace(/`[^`]*`/g, ' ')
|
|
208
|
+
.replace(/"[^"\n]*"/g, ' ')
|
|
209
|
+
.replace(/“[^”\n]*”/g, ' ')
|
|
210
|
+
.replace(/[A-Za-z]:\\[^\s"']*/g, ' ')
|
|
211
|
+
.replace(/(?:[A-Za-z0-9_.@-]+\/)+[A-Za-z0-9_.@-]+/g, ' ')
|
|
212
|
+
// 仓库里的裸文件名(README / CHANGELOG …)也是语料,不是意图
|
|
213
|
+
.replace(/\b(?:README|CHANGELOG|LICENSE|AGENTS|CONTRIBUTING|Dockerfile|Makefile)\b/g, ' ')
|
|
214
|
+
.replace(/\s+/g, ' ')
|
|
215
|
+
.trim()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 意图词是否值得作为触发信号(S2 的准入过滤)。
|
|
220
|
+
*
|
|
221
|
+
* 规则来自实测:`rm` 命中 `dcterms`、`ppt` 命中 `pptx`、`ms` 命中任何含 ms 的词——短拉丁词
|
|
222
|
+
* 是假阳性制造机;含 `_`/`.`/`/` 的是标识符或路径,属于语料平面,不是意图。
|
|
223
|
+
*/
|
|
224
|
+
export function isIntentWord(word) {
|
|
225
|
+
const s = String(word || '').trim()
|
|
226
|
+
if (!s) return false
|
|
227
|
+
if (CJK_RANGE.test(s)) return s.length >= 2
|
|
228
|
+
if (s.length < 5) return false
|
|
229
|
+
if (/[_./\\*]/.test(s)) return false
|
|
230
|
+
if (/\s/.test(s)) return s.length >= 8
|
|
231
|
+
return true
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const GLOB_RE = /\*/
|
|
235
|
+
/** 扩展名 glob(`*.pptx`)与泛名 glob(`README*`):只能撒谎,不能收窄。 */
|
|
236
|
+
export function isDroppableGlob(pattern) {
|
|
237
|
+
const p = String(pattern || '')
|
|
238
|
+
if (!GLOB_RE.test(p)) return false
|
|
239
|
+
if (/^\*\.\w+$/.test(p)) return true
|
|
240
|
+
if (/^[A-Za-z][A-Za-z0-9_-]*\*$/.test(p)) return true
|
|
241
|
+
return false
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 意图词命中:CJK 用包含,拉丁用词边界(避免 `ppt` 命中 `pptx`)。
|
|
246
|
+
* @param {string} word
|
|
247
|
+
* @param {string} text - 已经是 {@link intentText} 处理过的意图文本
|
|
248
|
+
*/
|
|
249
|
+
export function matchIntent(word, text) {
|
|
250
|
+
const w = String(word || '').trim()
|
|
251
|
+
const hay = String(text || '')
|
|
252
|
+
if (!w || !hay) return false
|
|
253
|
+
if (CJK_RANGE.test(w)) return w.length >= 2 && hay.includes(w)
|
|
254
|
+
if (w.length < 4) return false
|
|
255
|
+
const esc = w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
256
|
+
return new RegExp(`(^|[^A-Za-z0-9_])${esc}([^A-Za-z0-9_]|$)`, 'i').test(hay)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function matchAnyPath(pattern, targets) {
|
|
260
|
+
const p = String(pattern || '')
|
|
261
|
+
if (!p) return false
|
|
262
|
+
const re = globToRegExp(p)
|
|
263
|
+
return (targets || []).some((cand) => {
|
|
264
|
+
const c = String(cand || '')
|
|
265
|
+
if (!c) return false
|
|
266
|
+
return re.test(c) || re.test(c.split(/[\\/]/).pop() || '')
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** guard 是**收窄**条件:全部满足才放行;它自己不能触发任何东西。 */
|
|
271
|
+
function guardPass(guard, ctx) {
|
|
272
|
+
if (!guard || typeof guard !== 'object') return true
|
|
273
|
+
const targets = ctx?.targets || []
|
|
274
|
+
if (Array.isArray(guard.paths) && guard.paths.length && !guard.paths.some((p) => matchAnyPath(p, targets))) return false
|
|
275
|
+
if (Array.isArray(guard.not_paths) && guard.not_paths.some((p) => matchAnyPath(p, targets))) return false
|
|
276
|
+
if (Array.isArray(guard.hosts) && guard.hosts.length && !guard.hosts.some((h) => (ctx?.hosts || []).includes(String(h)))) return false
|
|
277
|
+
if (Array.isArray(guard.tags) && guard.tags.length) {
|
|
278
|
+
const tags = ctx?.tags || []
|
|
279
|
+
// 画像未知(tags 为空)时不过滤:这是既有语义,保持兼容。
|
|
280
|
+
if (tags.length && !guard.tags.some((t) => tags.includes(String(t)))) return false
|
|
187
281
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
282
|
+
return true
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* trigger 命中判定(确定性,无打分)——**准入化后的语义**。
|
|
287
|
+
*
|
|
288
|
+
* `when` 是唯一的触发面,三个成员按 OR:`ops`(动作)/`writes`(本次要写的文件)/
|
|
289
|
+
* `intents`(人类消息里剥离引用后的意图词)。`guard` 只能收窄。
|
|
290
|
+
* **没有 `when` 的条目不再触发任何东西**(降级为可发现 + 按需拉取)。
|
|
291
|
+
*
|
|
292
|
+
* @param {object|null} trigger
|
|
293
|
+
* @param {object} ctx - {@link buildReadinessContext} 的结果
|
|
294
|
+
* @returns {string|null} 命中原因(`op:…` / `write:…` / `intent:…`),未命中为 null
|
|
295
|
+
*/
|
|
296
|
+
export function matchTrigger(trigger, ctx) {
|
|
297
|
+
if (!trigger || typeof trigger !== 'object') return null
|
|
298
|
+
const when = trigger.when
|
|
299
|
+
if (!when || typeof when !== 'object') return null
|
|
300
|
+
if (!guardPass(trigger.guard, ctx)) return null
|
|
301
|
+
for (const op of when.ops || []) {
|
|
302
|
+
if (op && (ctx?.ops || []).includes(String(op))) return `op:${op}`
|
|
191
303
|
}
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (
|
|
304
|
+
for (const w of when.writes || []) {
|
|
305
|
+
// 扩展名/泛名 glob 在这里被硬性忽略:`*.pptx` 这类条件只能撒谎,不能收窄。
|
|
306
|
+
if (!w || isDroppableGlob(w)) continue
|
|
307
|
+
if (matchAnyPath(w, ctx?.targets)) return `write:${w}`
|
|
196
308
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (
|
|
200
|
-
const re = globToRegExp(pattern)
|
|
201
|
-
const hit = (ctx?.paths || []).some((cand) => {
|
|
202
|
-
const c = String(cand || '')
|
|
203
|
-
if (!c) return false
|
|
204
|
-
return re.test(c) || re.test(c.split(/[\\/]/).pop() || '')
|
|
205
|
-
})
|
|
206
|
-
if (hit) return `path:${pattern}`
|
|
309
|
+
const intent = ctx?.intent ?? ctx?.humanText ?? ''
|
|
310
|
+
for (const it of when.intents || []) {
|
|
311
|
+
if (it && matchIntent(it, intent)) return `intent:${it}`
|
|
207
312
|
}
|
|
208
313
|
return null
|
|
209
314
|
}
|
|
210
315
|
|
|
316
|
+
/**
|
|
317
|
+
* 旧 trigger → 新 schema(纯函数、幂等、不改原对象)。
|
|
318
|
+
*
|
|
319
|
+
* 迁移规则(都在实测里有据):
|
|
320
|
+
* - `actions` → `when.ops`(经 {@link opForLegacyAction} 映射;死值记录进 `triggerNormalized.dropped`)
|
|
321
|
+
* - 具体的 `paths` → `when.writes`("我要改这个文件");扩展名/泛名 glob 直接丢弃
|
|
322
|
+
* - `keywords` → `when.intents`,只留通过 {@link isIntentWord} 的
|
|
323
|
+
* - `scope` → 默认忽略(它的值不在项目画像 tag 空间里,实测把最相关的一条 procedure 判了死刑)
|
|
324
|
+
*
|
|
325
|
+
* @param {object} it
|
|
326
|
+
* @param {{legacyScope?: 'ignore'|'filter'}} [opts]
|
|
327
|
+
*/
|
|
328
|
+
export function normalizeTrigger(it, opts = {}) {
|
|
329
|
+
const t = it && it.trigger
|
|
330
|
+
if (!t || typeof t !== 'object') return it
|
|
331
|
+
if (t.when && typeof t.when === 'object') return it // 已是新 schema:幂等
|
|
332
|
+
const dropped = []
|
|
333
|
+
const writes = []
|
|
334
|
+
for (const p of t.paths || []) {
|
|
335
|
+
if (!p) continue
|
|
336
|
+
if (isDroppableGlob(p)) dropped.push(`path:${p}`)
|
|
337
|
+
else writes.push(String(p))
|
|
338
|
+
}
|
|
339
|
+
const ops = new Set()
|
|
340
|
+
for (const a of t.actions || []) {
|
|
341
|
+
const op = opForLegacyAction(a)
|
|
342
|
+
if (!op) {
|
|
343
|
+
dropped.push(`action:${a}`)
|
|
344
|
+
continue
|
|
345
|
+
}
|
|
346
|
+
// 有精确 writes 时丢掉弱 op:留着它只会把"改这个文件时"扩大成"任何一次提交时"。
|
|
347
|
+
if (writes.length && WEAK_OPS.has(op)) {
|
|
348
|
+
dropped.push(`weak-action:${a}`)
|
|
349
|
+
continue
|
|
350
|
+
}
|
|
351
|
+
ops.add(op)
|
|
352
|
+
}
|
|
353
|
+
// 具体路径只写进 `when.writes`,**不要**再补一个 `file-write` 到 ops:
|
|
354
|
+
// when 的成员是取或的,补进去等于让"写了任何文件"就命中,把路径条件短路掉
|
|
355
|
+
// (实测:那样会让 D 场景一次多出 6 条假阳性)。
|
|
356
|
+
const intents = []
|
|
357
|
+
for (const k of t.keywords || []) if (isIntentWord(k)) intents.push(String(k))
|
|
358
|
+
const when = {}
|
|
359
|
+
if (ops.size) when.ops = [...ops].sort()
|
|
360
|
+
if (writes.length) when.writes = [...new Set(writes)]
|
|
361
|
+
if (intents.length) when.intents = [...new Set(intents)]
|
|
362
|
+
const out = { ...t, when }
|
|
363
|
+
const scopeIgnored = Array.isArray(t.scope) && t.scope.length > 0
|
|
364
|
+
if (scopeIgnored && opts.legacyScope !== 'ignore') out.guard = { ...(t.guard || {}), tags: t.scope }
|
|
365
|
+
return { ...it, trigger: out, triggerNormalized: { dropped, scopeIgnored } }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* trigger 自检(S5):把"哪些条目其实推不动、哪些声明是死的"变成可数的事实。
|
|
370
|
+
* @param {object[]} items
|
|
371
|
+
*/
|
|
372
|
+
export function auditTriggers(items, opts = {}) {
|
|
373
|
+
const out = {
|
|
374
|
+
total: 0,
|
|
375
|
+
pushable: 0,
|
|
376
|
+
pullOnly: [],
|
|
377
|
+
deadActions: [],
|
|
378
|
+
droppedGlobs: [],
|
|
379
|
+
weakDropped: [],
|
|
380
|
+
fallbackOps: [],
|
|
381
|
+
scopeIgnored: [],
|
|
382
|
+
missingPrevents: [],
|
|
383
|
+
}
|
|
384
|
+
for (const raw of items || []) {
|
|
385
|
+
if (!raw || !raw.id) continue
|
|
386
|
+
out.total++
|
|
387
|
+
const it = normalizeTrigger(raw, opts)
|
|
388
|
+
const meta = it.triggerNormalized || {}
|
|
389
|
+
for (const d of meta.dropped || []) {
|
|
390
|
+
if (d.startsWith('action:')) out.deadActions.push({ id: it.id, value: d.slice(7) })
|
|
391
|
+
else if (d.startsWith('path:')) out.droppedGlobs.push({ id: it.id, value: d.slice(5) })
|
|
392
|
+
else if (d.startsWith('weak-action:')) out.weakDropped.push({ id: it.id, value: d.slice(12) })
|
|
393
|
+
}
|
|
394
|
+
if (meta.scopeIgnored) out.scopeIgnored.push(it.id)
|
|
395
|
+
const when = it.trigger?.when
|
|
396
|
+
for (const w of when?.writes || []) {
|
|
397
|
+
if (isDroppableGlob(w)) out.droppedGlobs.push({ id: it.id, value: w })
|
|
398
|
+
}
|
|
399
|
+
const hasWhen = Boolean(when && ((when.ops || []).length || (when.writes || []).length || (when.intents || []).length))
|
|
400
|
+
if (hasWhen) {
|
|
401
|
+
out.pushable++
|
|
402
|
+
// 兜底 op 出现在 when.ops 里:不是错误,但这条触发面比它看起来宽得多。
|
|
403
|
+
const fb = (when.ops || []).filter((o) => FALLBACK_OPS.has(o))
|
|
404
|
+
if (fb.length) out.fallbackOps.push({ id: it.id, ops: fb })
|
|
405
|
+
} else {
|
|
406
|
+
out.pullOnly.push(it.id)
|
|
407
|
+
}
|
|
408
|
+
if (!it.trigger?.prevents) out.missingPrevents.push(it.id)
|
|
409
|
+
}
|
|
410
|
+
return out
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* IDF 加权覆盖率(S4 的**绝对**门槛):条目覆盖了查询里多少**信息量**,而不是多少个 token。
|
|
415
|
+
*
|
|
416
|
+
* 为什么需要它:`relativeHits` 只看"层内最高分的比例",而最高分本身可能就是噪声——
|
|
417
|
+
* 实测 `relative:1.00` 出现在和查询毫无关系的条目上。归一在 [0,1]、有真零点,才能设下限。
|
|
418
|
+
*
|
|
419
|
+
* 返回四个量,调用方要一起看:
|
|
420
|
+
* - `coverage`:在**语料能表示**的词里,条目覆盖了多少信息量(分母不含 df=0 的词——
|
|
421
|
+
* 自然语言查询总有语料没有的词,把它们算成未命中会让任何正常查询都趋零)。
|
|
422
|
+
* - `matched`:命中的词数。单个通用词("插件")也能拿到 coverage=1.00。
|
|
423
|
+
* - `supported` / `terms`:语料里有对应词的比例。长句子里只有一两个词能在语料中找到对应时,
|
|
424
|
+
* "覆盖率 1.00"是假象(实测:一句 19 个词的改时间戳请求,只与 WSL 笔记共享一个"文件",
|
|
425
|
+
* 却拿到 cov=1.00),调用方据此让**整条通道沉默**。
|
|
426
|
+
* @returns {{coverage: number, matched: number, supported: number, terms: number}}
|
|
427
|
+
*/
|
|
428
|
+
export function idfCoverage(query, itemText, corpusTexts) {
|
|
429
|
+
const q = [...new Set(tokenize(query))]
|
|
430
|
+
if (!q.length) return { coverage: 0, matched: 0, supported: 0, terms: 0 }
|
|
431
|
+
const item = new Set(tokenize(itemText))
|
|
432
|
+
const corpus = (corpusTexts || []).map((t) => new Set(tokenize(t)))
|
|
433
|
+
const n = Math.max(corpus.length, 1)
|
|
434
|
+
let total = 0
|
|
435
|
+
let hit = 0
|
|
436
|
+
let matched = 0
|
|
437
|
+
let supported = 0
|
|
438
|
+
for (const term of q) {
|
|
439
|
+
let df = 0
|
|
440
|
+
for (const doc of corpus) if (doc.has(term)) df++
|
|
441
|
+
// df=0 的词不计入分母:它对"选哪一条"没有分辨力。但 supported 会记下有多少词是
|
|
442
|
+
// 语料根本无法表示的——那是"这条查询整体上离语料太远"的证据,交给调用方决定沉默。
|
|
443
|
+
if (df === 0) continue
|
|
444
|
+
supported++
|
|
445
|
+
const w = Math.log(1 + n / (1 + df))
|
|
446
|
+
total += w
|
|
447
|
+
if (item.has(term)) {
|
|
448
|
+
hit += w
|
|
449
|
+
matched++
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return { coverage: total > 0 ? hit / total : 0, matched, supported, terms: q.length }
|
|
453
|
+
}
|
|
454
|
+
|
|
211
455
|
/**
|
|
212
456
|
* 层内相对阈值:命中分至少达到该层最高分的 `ratioMin`。尺度无关(BM25 分数无上界),
|
|
213
457
|
* 取代"整条消息 vs 整条 insight 的集合重叠 ÷ 较大集合"这种长度敏感判据。
|
|
@@ -35,16 +35,38 @@ export function lessonTool(config) {
|
|
|
35
35
|
type: 'object',
|
|
36
36
|
additionalProperties: false,
|
|
37
37
|
properties: {
|
|
38
|
-
|
|
38
|
+
when: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
properties: {
|
|
42
|
+
ops: { type: 'array', items: { type: 'string' }, description: '归一动作 id:file-write / file-delete / shell-run / git-commit / git-push / release / npm-publish / deploy / migrate / render-doc / index-doc / read-image / fetch-web / run-bench / query-memory。' },
|
|
43
|
+
writes: { type: 'array', items: { type: 'string' }, description: '本次要【写】的项目相对路径(禁扩展名/泛名 glob)。' },
|
|
44
|
+
intents: { type: 'array', items: { type: 'string' }, description: '人类消息里剥离引用后的意图词(CJK ≥2 字、拉丁 ≥5 字符且不含 _ . /)。' },
|
|
45
|
+
},
|
|
46
|
+
description: '唯一触发面:三者取或。',
|
|
47
|
+
},
|
|
48
|
+
guard: {
|
|
49
|
+
type: 'object',
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
properties: {
|
|
52
|
+
paths: { type: 'array', items: { type: 'string' }, description: '收窄:写目标必须命中其中之一(自己不能触发)。' },
|
|
53
|
+
not_paths: { type: 'array', items: { type: 'string' } },
|
|
54
|
+
hosts: { type: 'array', items: { type: 'string' }, description: '例如 wsl(命令里出现 /mnt/* 或 powershell.exe)。' },
|
|
55
|
+
tags: { type: 'array', items: { type: 'string' }, description: '项目画像 tag 交集(画像未知时不过滤)。' },
|
|
56
|
+
},
|
|
57
|
+
description: '收窄条件:全部满足才注入。',
|
|
58
|
+
},
|
|
59
|
+
prevents: { type: 'string', description: '准入条件:不知道这条,这一步会做错什么。写不出来 → 不该进自动注入。' },
|
|
60
|
+
keywords: { type: 'array', items: { type: 'string' }, description: '【旧字段】降级为 intents 与被动召回排序。' },
|
|
39
61
|
symbols: { type: 'array', items: { type: 'string' } },
|
|
40
|
-
actions: { type: 'array', items: { type: 'string' } },
|
|
41
|
-
paths: { type: 'array', items: { type: 'string' } },
|
|
42
|
-
scope: { type: 'array', items: { type: 'string' } },
|
|
62
|
+
actions: { type: 'array', items: { type: 'string' }, description: '【旧字段】映射为 when.ops;死值丢弃并计入自检。' },
|
|
63
|
+
paths: { type: 'array', items: { type: 'string' }, description: '【旧字段】具体文件映射为 when.writes;扩展名/泛名 glob 丢弃。' },
|
|
64
|
+
scope: { type: 'array', items: { type: 'string' }, description: '【旧字段】默认忽略(值不在项目画像 tag 空间里)。' },
|
|
43
65
|
},
|
|
44
66
|
description:
|
|
45
|
-
'authored trigger
|
|
46
|
-
'
|
|
47
|
-
'
|
|
67
|
+
'authored trigger:命中即在**动手前**确定性注入。' +
|
|
68
|
+
'when = 唯一触发面(ops / writes / intents 取或);guard 只能收窄;prevents 是准入条件。' +
|
|
69
|
+
'没有 when 的条目不会自动推送,只出现在记忆目录里供按需拉取。',
|
|
48
70
|
},
|
|
49
71
|
task_id: { type: 'string', description: '目标任务 id(scope 缺省时优先于绑定任务)' },
|
|
50
72
|
files: { type: 'array', items: { type: 'string' }, description: '关联文件(项目相对路径)' },
|