dsh-experience 1.2.1 → 1.2.2
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 +23 -0
- package/index.js +49 -5
- package/lib/capture.js +59 -11
- package/lib/decay.js +8 -6
- package/lib/recall.js +10 -0
- package/package.json +1 -1
- package/tools/selftest.mjs +41 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## [1.2.2] — 2026-11-09 — 体验修复:采集可靠性、候选可读性、旧经验降权
|
|
5
|
+
|
|
6
|
+
**采集**
|
|
7
|
+
- `tools/result` 与 turn 生命周期统一用 session id 对齐(原先工具侧取 `agent.id`、轮末取 `session.id`,不一致时整轮调用被记到另一个 key,表现为 `G1: 成功工具调用 < 2` 而实际跑了很多工具)。
|
|
8
|
+
- 单 key 兜底搬运 + `trace.resultsSeen` / `trace.keyFallback`;G1 拒绝但确实收到过结果时写入 `capture.diagnostic`,不再静默。
|
|
9
|
+
|
|
10
|
+
**召回**
|
|
11
|
+
- 无 platform 标签的历史记录排序 ×0.85,注入块里标注「平台:未标注」。
|
|
12
|
+
- `score()` 收到非字符串直接抛 `TypeError`(此前把记录对象传进去会静默得 0,看起来像"不相关")。
|
|
13
|
+
|
|
14
|
+
**候选可读性**
|
|
15
|
+
- 标题带项目名:`新增 truncate(acme-app)`、`跑 run(cross-a)`。
|
|
16
|
+
- 触发条件写真实验证命令,不再三条候选共用一句模板。
|
|
17
|
+
- `stepLabel` 去掉 `cd …;` 前缀;`cd` / `Start-Sleep` 计入噪声。
|
|
18
|
+
- 新增 `/memory review discard`:一次性拒绝当前所有候选(可审计,非删除)。
|
|
19
|
+
|
|
20
|
+
**衰减**
|
|
21
|
+
- 容量未达阈值时不再输出"观察项"清单(小库时那 5 条就是全部,纯噪声)。
|
|
22
|
+
|
|
23
|
+
**测试**:`selftest 106` + `e2e 25` = 131 全部通过。
|
|
24
|
+
|
|
25
|
+
**已知限制**:采集可靠性改动只经过单测与结构验证;真实轮次的效果需要下一个长轮次的 `capture.diagnostic` 来确认。
|
|
26
|
+
|
|
4
27
|
## [1.2.1] — 2026-11-09 — 审计修复:召回语料、id 回收、模拟淘汰、恢复、G8 守卫
|
|
5
28
|
|
|
6
29
|
### 修复
|
package/index.js
CHANGED
|
@@ -35,7 +35,7 @@ const PROBE_FILE = path.join(STORE_DIR, 'probe.json')
|
|
|
35
35
|
const THRESHOLD = 0.15
|
|
36
36
|
const MAX_EXPERIENCES = 2
|
|
37
37
|
const MAX_CHARS = 1600
|
|
38
|
-
const BUILD = '
|
|
38
|
+
const BUILD = 'r24-ux-fixes'
|
|
39
39
|
|
|
40
40
|
let CONFIG = { ...C.DEFAULT_CONFIG }
|
|
41
41
|
|
|
@@ -111,8 +111,21 @@ function tokens(text) {
|
|
|
111
111
|
return R.expand(text)
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Sessions own tool calls; an agent object can lose its id between the tool
|
|
116
|
+
* lifecycle and the turn lifecycle. Keying on the session keeps both sides in
|
|
117
|
+
* agreement — the mismatch was silently dropping every call of a turn.
|
|
118
|
+
*/
|
|
119
|
+
function keyOf(agent) {
|
|
120
|
+
if (!agent) return ''
|
|
121
|
+
return (agent.session && agent.session.id) || agent.id || ''
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
function scoreExperience(experience, queryTokens, index) {
|
|
115
|
-
|
|
125
|
+
const base = R.score(R.profileText(experience), queryTokens, index)
|
|
126
|
+
// A recipe learned on another machine, or never tagged at all, is weaker evidence.
|
|
127
|
+
const factor = C.platformFactor(experience)
|
|
128
|
+
return Math.round(base * factor * 1000) / 1000
|
|
116
129
|
}
|
|
117
130
|
|
|
118
131
|
// Retrieval floor. Configurable via "/memory config set recallThreshold 0.1"
|
|
@@ -239,6 +252,7 @@ export function apply(ctx) {
|
|
|
239
252
|
const traces = new Map()
|
|
240
253
|
const bound = new Map()
|
|
241
254
|
const liveCalls = new Map()
|
|
255
|
+
const resultsSeen = new Map()
|
|
242
256
|
const probe = { done: false }
|
|
243
257
|
let currentTrace = null
|
|
244
258
|
|
|
@@ -259,6 +273,8 @@ export function apply(ctx) {
|
|
|
259
273
|
lines.push('')
|
|
260
274
|
lines.push(`## ${experience.id} · ${experience.title}`)
|
|
261
275
|
if (experience.when) lines.push(`适用:${experience.when}`)
|
|
276
|
+
// Untagged records predate platform isolation; say so rather than pretend.
|
|
277
|
+
if (!C.platformOf(experience)) lines.push('平台:未标注(旧经验,按通用配方参考)')
|
|
262
278
|
const steps = (experience.steps || []).slice(0, 6)
|
|
263
279
|
steps.forEach((step, index) => lines.push(`${index + 1}. ${step}`))
|
|
264
280
|
const pitfalls = experience.pitfalls || experience.reverse_examples || []
|
|
@@ -475,8 +491,9 @@ export function apply(ctx) {
|
|
|
475
491
|
function recordCall(exec, result) {
|
|
476
492
|
try {
|
|
477
493
|
ensureProbe(exec, result)
|
|
478
|
-
const id =
|
|
494
|
+
const id = keyOf(exec && exec.agent) || initiatorId()
|
|
479
495
|
if (!id) return
|
|
496
|
+
resultsSeen.set(id, (resultsSeen.get(id) || 0) + 1)
|
|
480
497
|
const ok = pickOk(exec, result)
|
|
481
498
|
const list = liveCalls.get(id) || []
|
|
482
499
|
if (list.length >= 80) list.shift()
|
|
@@ -732,15 +749,31 @@ export function apply(ctx) {
|
|
|
732
749
|
|
|
733
750
|
ctx.on('agent/turn-stopping', (payload) => {
|
|
734
751
|
try {
|
|
735
|
-
const id =
|
|
736
|
-
|
|
752
|
+
const id = keyOf(payload && payload.agent)
|
|
753
|
+
let calls = liveCalls.get(id) || []
|
|
737
754
|
liveCalls.delete(id)
|
|
755
|
+
let keyFallback = false
|
|
756
|
+
if (!calls.length && liveCalls.size === 1) {
|
|
757
|
+
const [otherId, otherCalls] = liveCalls.entries().next().value
|
|
758
|
+
if (otherCalls && otherCalls.length) {
|
|
759
|
+
calls = otherCalls
|
|
760
|
+
liveCalls.delete(otherId)
|
|
761
|
+
keyFallback = true
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
const seen = resultsSeen.get(id) || 0
|
|
765
|
+
resultsSeen.delete(id)
|
|
738
766
|
const trace = traces.get(id)
|
|
739
767
|
if (!trace || trace.turn !== payload.turn) return
|
|
740
768
|
trace.calls = calls.slice(-CONFIG.maxCalls)
|
|
769
|
+
trace.resultsSeen = seen
|
|
770
|
+
if (keyFallback) trace.keyFallback = true
|
|
741
771
|
trace.endedAt = new Date().toISOString()
|
|
742
772
|
trace.durationMs = Date.parse(trace.endedAt) - Date.parse(trace.startedAt)
|
|
743
773
|
const capture = runCapture(trace, payload.agent)
|
|
774
|
+
if (capture && !capture.ok && /^G1/.test(String(capture.reason)) && seen > trace.calls.length) {
|
|
775
|
+
capture.diagnostic = `采集异常:本轮收到 ${seen} 次工具结果,只记录 ${trace.calls.length} 条${keyFallback ? '(已用 keyFallback 兜底)' : ''}`
|
|
776
|
+
}
|
|
744
777
|
// Raw hints are needed while capturing (scope + fingerprint); they must not
|
|
745
778
|
// reach the durable trace, so collapse them to the redacted form now.
|
|
746
779
|
trace.calls = (trace.calls || []).map((call) => ({ ...call, hint: C.clip(call.hint, 120) }))
|
|
@@ -982,6 +1015,17 @@ export function apply(ctx) {
|
|
|
982
1015
|
}),
|
|
983
1016
|
].join('\n')
|
|
984
1017
|
}
|
|
1018
|
+
if (line === 'discard' || line === '--discard' || line === 'reject-all') {
|
|
1019
|
+
const candidates = C.reviewTargets(store.experiences)
|
|
1020
|
+
if (!candidates.length) return '没有待确认的候选经验。'
|
|
1021
|
+
const at = new Date().toISOString()
|
|
1022
|
+
for (const item of candidates) {
|
|
1023
|
+
item.status = 'rejected'
|
|
1024
|
+
item.verify = { by: 'human', at, method: 'command-reject-all' }
|
|
1025
|
+
}
|
|
1026
|
+
persist()
|
|
1027
|
+
return `已拒绝 ${candidates.length} 条候选:${candidates.map((item) => item.id).join(', ')}`
|
|
1028
|
+
}
|
|
985
1029
|
if (line === '--approve-all' || line === 'approve-all' || line === 'approve --all') {
|
|
986
1030
|
const candidates = C.reviewTargets(store.experiences)
|
|
987
1031
|
if (!candidates.length) return '没有待确认的候选经验。'
|
package/lib/capture.js
CHANGED
|
@@ -120,6 +120,18 @@ export function classify(call) {
|
|
|
120
120
|
return { kind, target, name }
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* "cd somewhere; node test.js" says nothing about the recipe beyond the command
|
|
125
|
+
* that actually ran, so keep only the trailing command.
|
|
126
|
+
*/
|
|
127
|
+
export function stripCdPrefix(command) {
|
|
128
|
+
const text = String(command || '').trim()
|
|
129
|
+
const parts = text.split(/\s*(?:;|&&|\|\|)\s*/).filter(Boolean)
|
|
130
|
+
if (parts.length <= 1) return text.replace(/;+$/, '')
|
|
131
|
+
const kept = parts.filter((part) => !/^cd\s/i.test(part) && !/^Set-Location\b/i.test(part))
|
|
132
|
+
return (kept.length ? kept.join('; ') : parts[parts.length - 1]).replace(/;+$/, '')
|
|
133
|
+
}
|
|
134
|
+
|
|
123
135
|
export function stepLabel(call) {
|
|
124
136
|
const { kind, target, name } = classify(call)
|
|
125
137
|
switch (kind) {
|
|
@@ -128,7 +140,7 @@ export function stepLabel(call) {
|
|
|
128
140
|
case 'read':
|
|
129
141
|
return target ? `读 ${target.split('/').pop()}` : '读文件'
|
|
130
142
|
case 'run':
|
|
131
|
-
return target ? `跑 ${target}` : '执行命令'
|
|
143
|
+
return target ? `跑 ${stripCdPrefix(target)}` : '执行命令'
|
|
132
144
|
case 'search':
|
|
133
145
|
return target ? `检索 ${target}` : '检索'
|
|
134
146
|
default:
|
|
@@ -172,7 +184,7 @@ export function stepPriority(call) {
|
|
|
172
184
|
* output) is transcript noise: it describes how the agent measured the world,
|
|
173
185
|
* not what a second run should do.
|
|
174
186
|
*/
|
|
175
|
-
const NOISE_PREFIX = /^(\$|New-Item|New-ItemProperty|New-Object|Test-Path|Get-ChildItem|Get-Item|Set-Location|Push-Location|Pop-Location|Write-Host|Write-Output|Copy-Item|Remove-Item|Move-Item|Rename-Item)\b/
|
|
187
|
+
const NOISE_PREFIX = /^(\$|New-Item|New-ItemProperty|New-Object|Test-Path|Get-ChildItem|Get-Item|Set-Location|Push-Location|Pop-Location|Write-Host|Write-Output|Write-Verbose|Clear-Host|Copy-Item|Remove-Item|Move-Item|Rename-Item|Start-Sleep|sleep|cd)\b/
|
|
176
188
|
const ASSIGNMENT = /^[A-Za-z_$][\w$]*\s*=[^=]/
|
|
177
189
|
|
|
178
190
|
export function isNoiseStep(label) {
|
|
@@ -541,6 +553,10 @@ const GENERIC_DIRS = new Set([
|
|
|
541
553
|
// say nothing about *which project* a record belongs to.
|
|
542
554
|
'node', 'python', 'python3', 'npm', 'pnpm', 'npx', 'yarn', 'git', 'pwsh',
|
|
543
555
|
'deno', 'bun', 'runtime', 'runtimes', 'dependencies', 'dsh', 'windows',
|
|
556
|
+
// Home-directory placeholders: a user name is not a project name.
|
|
557
|
+
'dev', 'developer', 'user', 'userprofile',
|
|
558
|
+
// Sandbox and CI roots: fixtures live here, they are not project names.
|
|
559
|
+
'work', 'projects', 'repos',
|
|
544
560
|
])
|
|
545
561
|
|
|
546
562
|
/**
|
|
@@ -636,22 +652,46 @@ export function buildCandidate(trace, info, config) {
|
|
|
636
652
|
const named = writes
|
|
637
653
|
.map((call) => artifactName(call))
|
|
638
654
|
.find((name) => /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(name) && /[A-Za-z0-9]{2,}/.test(name))
|
|
655
|
+
// Scope is computed before rendering so the title and the trigger can name the
|
|
656
|
+
// project. "新增 index" is unreadable in a review list; "新增 index(dsh-experience)"
|
|
657
|
+
// tells the human which project the recipe belongs to.
|
|
658
|
+
const scope = mentionedDirs(
|
|
659
|
+
[
|
|
660
|
+
...(calls || []).map((call) => firstString(call && (call.hint || call.argsHint || call.args))),
|
|
661
|
+
trace && trace.query,
|
|
662
|
+
].join(' '),
|
|
663
|
+
)
|
|
664
|
+
const project = projectScope({ scope })[0] || ''
|
|
665
|
+
const suffix = project ? `(${project})` : ''
|
|
639
666
|
const title = clip(
|
|
640
667
|
runOnly
|
|
641
668
|
? scriptName
|
|
642
|
-
? `跑 ${scriptName}`
|
|
669
|
+
? `跑 ${scriptName}${suffix}`
|
|
643
670
|
: primary
|
|
644
671
|
? `跑 ${primary}`
|
|
645
672
|
: verbsOf(taskType)
|
|
646
673
|
: named
|
|
647
|
-
? `新增 ${named}`
|
|
674
|
+
? `新增 ${named}${suffix}`
|
|
648
675
|
: primary
|
|
649
676
|
? `${verb} ${primary}`
|
|
650
677
|
: verbsOf(taskType),
|
|
651
678
|
110,
|
|
652
679
|
)
|
|
653
680
|
const place = workspaceName(info && info.cwd)
|
|
654
|
-
const
|
|
681
|
+
const testRun = runs.find((call) => TEST_COMMAND.test(firstString(call && (call.hint || call.argsHint || call.args))))
|
|
682
|
+
const testCommand = testRun ? classify(testRun).target : ''
|
|
683
|
+
// The trigger line must discriminate between recipes, not repeat the task type
|
|
684
|
+
// ("在 desktop 里 新增函数/模块并配用例" fits every single candidate).
|
|
685
|
+
const when = clip(
|
|
686
|
+
project
|
|
687
|
+
? `在 ${project} 里 ${verbsOf(taskType)}${testCommand ? `,验证 ${testCommand}` : ''}`
|
|
688
|
+
: testCommand
|
|
689
|
+
? `当需要${verbsOf(taskType)}时,验证 ${testCommand}`
|
|
690
|
+
: place
|
|
691
|
+
? `在 ${place} 里 ${verbsOf(taskType)}`
|
|
692
|
+
: `当需要${verbsOf(taskType)}时`,
|
|
693
|
+
160,
|
|
694
|
+
)
|
|
655
695
|
const keywords = [...new Set([taskType, ...steps.map((step) => step.split(' ')[1] || '').filter(Boolean)])]
|
|
656
696
|
.filter(Boolean)
|
|
657
697
|
.slice(0, 12)
|
|
@@ -660,13 +700,11 @@ export function buildCandidate(trace, info, config) {
|
|
|
660
700
|
const record = {
|
|
661
701
|
status: 'candidate',
|
|
662
702
|
platform: process.platform,
|
|
703
|
+
// Task shape as recorded, not as re-guessed from prose later: a write task and
|
|
704
|
+
// a "run this script" task must never merge even when they share every noun.
|
|
705
|
+
intent: runOnly ? 'run' : 'write',
|
|
663
706
|
// Scope detection needs the raw paths; every rendered field below is normalized.
|
|
664
|
-
scope
|
|
665
|
-
[
|
|
666
|
-
...(calls || []).map((call) => firstString(call && (call.hint || call.argsHint || call.args))),
|
|
667
|
-
trace && trace.query,
|
|
668
|
-
].join(' '),
|
|
669
|
-
),
|
|
707
|
+
scope,
|
|
670
708
|
fingerprint: outcomeFingerprint(calls),
|
|
671
709
|
title,
|
|
672
710
|
taskType,
|
|
@@ -703,6 +741,16 @@ export function platformOf(record) {
|
|
|
703
741
|
* darwin. Unknown platform never vetoes: records written before v1.1 carry no
|
|
704
742
|
* tag, and every one of them was learned on this machine.
|
|
705
743
|
*/
|
|
744
|
+
/**
|
|
745
|
+
* Old records carry no platform tag: they were learned before isolation existed.
|
|
746
|
+
* They stay usable, but must not outrank a record that was proven on this machine.
|
|
747
|
+
*/
|
|
748
|
+
export function platformFactor(record) {
|
|
749
|
+
const tag = platformOf(record)
|
|
750
|
+
if (!tag) return 0.85
|
|
751
|
+
return tag === process.platform ? 1 : 0
|
|
752
|
+
}
|
|
753
|
+
|
|
706
754
|
export function platformConflicts(record) {
|
|
707
755
|
const tag = platformOf(record)
|
|
708
756
|
return Boolean(tag && tag !== process.platform)
|
package/lib/decay.js
CHANGED
|
@@ -209,12 +209,14 @@ export function renderDecayReport(store, now = Date.now(), config = {}) {
|
|
|
209
209
|
`[M3.3 衰减报告] 库 ${live.length} 条 · active ${counts.active} / dormant ${counts.dormant} / pinned ${counts.pinned}`,
|
|
210
210
|
...(hiddenRejected ? [`已排除 ${hiddenRejected} 条 rejected(审计留档,不参与排序与容量;\/memory list --all 查看)`] : []),
|
|
211
211
|
`容量: ${proposal.count}/${proposal.libraryLimit}(阈值 ${proposal.threshold} · 比例 ${num(config.capacityRatio, DECAY_DEFAULTS.capacityRatio)})· ${proposal.triggered ? '已达阈值' : '未达阈值'}`,
|
|
212
|
-
proposal.triggered
|
|
213
|
-
?
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
212
|
+
...(proposal.triggered
|
|
213
|
+
? [
|
|
214
|
+
`建议归档 ${proposal.evictions.length} 条(最多 ${num(config.evictBatch, DECAY_DEFAULTS.evictBatch)} 条/次,按 rank 升序):`,
|
|
215
|
+
...(top.length
|
|
216
|
+
? top.map((entry, index) => ` ${index + 1}. ${decayLine(entry.record, now, config)}${eligible.has(entry.record.id) ? ' ← 可归档' : ''}`)
|
|
217
|
+
: [' (库为空)']),
|
|
218
|
+
]
|
|
219
|
+
: [`容量充足(${proposal.count}/${proposal.libraryLimit}),暂时不需要淘汰任何记录。`]),
|
|
218
220
|
'只列出建议,不删除任何记录。确认后运行 /memory decay --apply(移入 archive,可用 /memory restore <id> 恢复)。',
|
|
219
221
|
]
|
|
220
222
|
return lines.join('\n')
|
package/lib/recall.js
CHANGED
|
@@ -138,6 +138,11 @@ const round = (value) => Math.round(value * 1000) / 1000
|
|
|
138
138
|
* Empty means "cannot tell" and never vetoes anything.
|
|
139
139
|
*/
|
|
140
140
|
export function intentOf(experienceOrText) {
|
|
141
|
+
// Records written by buildCandidate carry the intent they were captured with.
|
|
142
|
+
// Re-deriving it from prose is only the fallback for hand-written records.
|
|
143
|
+
if (experienceOrText && typeof experienceOrText === 'object' && (experienceOrText.intent === 'run' || experienceOrText.intent === 'write')) {
|
|
144
|
+
return experienceOrText.intent
|
|
145
|
+
}
|
|
141
146
|
const text =
|
|
142
147
|
typeof experienceOrText === 'string' ? experienceOrText : profileText(experienceOrText)
|
|
143
148
|
const found = features(text)
|
|
@@ -207,6 +212,11 @@ const scriptWeight = (token) => (/^[\u4e00-\u9fff]+$/.test(token) ? 0.45 : 1)
|
|
|
207
212
|
* Both terms are bounded, so the result is always within [0, 1].
|
|
208
213
|
*/
|
|
209
214
|
export function score(profile, queryTokens, c) {
|
|
215
|
+
// Passing a record here used to score 0 silently, which looks exactly like
|
|
216
|
+
// "this experience is irrelevant" from the outside. Fail loudly instead.
|
|
217
|
+
if (typeof profile !== 'string') {
|
|
218
|
+
throw new TypeError('score() expects plain text; call profileText(record) first')
|
|
219
|
+
}
|
|
210
220
|
const expTokens = expand(profile)
|
|
211
221
|
if (!queryTokens || !queryTokens.size || !expTokens.size) return 0
|
|
212
222
|
let weightSum = 0
|
package/package.json
CHANGED
package/tools/selftest.mjs
CHANGED
|
@@ -341,7 +341,7 @@ test('buildCandidate: shape, taskType, steps, evidence', () => {
|
|
|
341
341
|
assert.equal(draft.source.auto, true)
|
|
342
342
|
assert.equal(draft.verify, null)
|
|
343
343
|
assert.equal(draft.occurrences, 1)
|
|
344
|
-
assert.equal(draft.title, '新增 truncate')
|
|
344
|
+
assert.equal(draft.title, '新增 truncate(acme-app)')
|
|
345
345
|
assert.ok(draft.when.includes('acme-app'), draft.when)
|
|
346
346
|
assert.deepEqual(draft.steps, ['写 src/<name>.js', '跑 node test/<name>.mjs'])
|
|
347
347
|
assert.equal(draft.evidence, 'session-x:turn-3 · calls=3 errors=0')
|
|
@@ -710,7 +710,9 @@ test('T30: decayLine and decay report expose rank/age and stay proposal-only', (
|
|
|
710
710
|
assert.ok(report.includes('只列出建议,不删除任何记录'), report)
|
|
711
711
|
assert.ok(report.includes('/memory decay --apply'), report)
|
|
712
712
|
assert.ok(report.includes('/memory restore <id>'), report)
|
|
713
|
-
|
|
713
|
+
// Under capacity there is nothing to rank: no observation list at all.
|
|
714
|
+
assert.ok(report.includes('容量充足'), report)
|
|
715
|
+
assert.equal(report.split('\n').filter((row) => /^ {2}\d+\. /.test(row)).length, 0, 'no observation list under capacity')
|
|
714
716
|
const many = [...records, ...Array.from({ length: 160 }, (_, i) => ({ id: `f-${i}`, status: 'candidate', lastUsedAt: ago(1) }))]
|
|
715
717
|
const hot = D.renderDecayReport({ experiences: many }, NOW)
|
|
716
718
|
assert.ok(hot.includes('已达阈值'), hot)
|
|
@@ -814,7 +816,7 @@ test('T34: a real recipe still passes G7 and gets an actionable title', () => {
|
|
|
814
816
|
const gate = C.gateStepQuality(trace, draft, C.DEFAULT_CONFIG)
|
|
815
817
|
assert.equal(gate.ok, true, gate.reason)
|
|
816
818
|
assert.equal(gate.effective, 2)
|
|
817
|
-
assert.equal(draft.title, '新增 truncate')
|
|
819
|
+
assert.equal(draft.title, '新增 truncate(acme-app)')
|
|
818
820
|
assert.equal(C.actionableTitle(draft.title), true)
|
|
819
821
|
})
|
|
820
822
|
|
|
@@ -1135,7 +1137,7 @@ test('T78 a pure run track is captured, titled and fingerprinted by its command'
|
|
|
1135
1137
|
assert.equal(quality.ok, true, quality.reason)
|
|
1136
1138
|
|
|
1137
1139
|
const draft = C.buildCandidate(trace, { runOnly: true }, C.DEFAULT_CONFIG)
|
|
1138
|
-
assert.equal(draft.title, '跑 run')
|
|
1140
|
+
assert.equal(draft.title, '跑 run(cross-a)')
|
|
1139
1141
|
assert.deepEqual(draft.fingerprint.commands, ['node'])
|
|
1140
1142
|
assert.deepEqual(C.commandHeads(calls), ['node'])
|
|
1141
1143
|
})
|
|
@@ -1287,6 +1289,41 @@ test('T91 a fresh store starts at schema version 2', () => {
|
|
|
1287
1289
|
assert.equal(S.emptyStore().version, 2)
|
|
1288
1290
|
})
|
|
1289
1291
|
|
|
1292
|
+
// ---------------------------------------------------------------- v1.2.2
|
|
1293
|
+
|
|
1294
|
+
test('score: a non-string profile fails loudly instead of scoring 0', () => {
|
|
1295
|
+
assert.throws(() => R.score({ title: 'x' }, R.expand('新增函数'), R.corpus([])), /profileText/)
|
|
1296
|
+
assert.equal(typeof R.score('新增 pad 函数', R.expand('新增 pad 函数'), R.corpus([])), 'number')
|
|
1297
|
+
})
|
|
1298
|
+
|
|
1299
|
+
test('platformFactor: untagged records are discounted, foreign ones excluded', () => {
|
|
1300
|
+
assert.equal(C.platformFactor({}), 0.85)
|
|
1301
|
+
assert.equal(C.platformFactor({ platform: process.platform }), 1)
|
|
1302
|
+
assert.equal(C.platformFactor({ platform: process.platform === 'win32' ? 'linux' : 'win32' }), 0)
|
|
1303
|
+
})
|
|
1304
|
+
|
|
1305
|
+
test('stepLabel: hangs off the command, not the scaffolding in front of it', () => {
|
|
1306
|
+
const label = C.stepLabel({ name: 'pwsh', hint: 'cd D:\\work\\acme-app; node test\\pad.test.mjs', ok: true })
|
|
1307
|
+
assert.ok(!label.includes('cd '), label)
|
|
1308
|
+
assert.ok(label.includes('node'), label)
|
|
1309
|
+
assert.ok(C.isNoiseStep('cd D:\\work'), 'a bare cd is not a step')
|
|
1310
|
+
assert.ok(C.isNoiseStep('Start-Sleep -Seconds 2'), 'waiting is not a step')
|
|
1311
|
+
})
|
|
1312
|
+
|
|
1313
|
+
test('buildCandidate: when names the project and the verification command', () => {
|
|
1314
|
+
const draft = C.buildCandidate(sampleTrace(), { cwd: 'D:\\work\\acme-app', sessionId: 'session-z' })
|
|
1315
|
+
assert.ok(draft.when.includes('acme-app'), draft.when)
|
|
1316
|
+
assert.ok(draft.when.includes('node'), draft.when)
|
|
1317
|
+
assert.notEqual(draft.when, '在 desktop 里 新增函数/模块并配用例')
|
|
1318
|
+
})
|
|
1319
|
+
|
|
1320
|
+
test('decay report stays quiet while the library is nowhere near capacity', () => {
|
|
1321
|
+
const records = [{ id: 'exp-1', status: 'verified', title: 'x', uses: 1, at: new Date(NOW).toISOString() }]
|
|
1322
|
+
const report = D.renderDecayReport({ experiences: records }, NOW, { libraryLimit: 200 })
|
|
1323
|
+
assert.ok(report.includes('容量充足'), report)
|
|
1324
|
+
assert.ok(!report.includes('观察项'), report)
|
|
1325
|
+
})
|
|
1326
|
+
|
|
1290
1327
|
// ---------------------------------------------------------------- summary
|
|
1291
1328
|
|
|
1292
1329
|
const total = pass + failures.length
|