dsh-context-compression-improved 0.5.2 → 0.5.3

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.
Files changed (28) hide show
  1. package/.gitattributes +1 -0
  2. package/CHANGELOG.ja.md +144 -119
  3. package/CHANGELOG.ko.md +143 -118
  4. package/CHANGELOG.md +278 -250
  5. package/CHANGELOG.zh.md +131 -109
  6. package/docs/installation.md +103 -103
  7. package/docs/installation.zh.md +100 -100
  8. package/package.json +1 -1
  9. package/packages/selector/cordis.patch.yml +5 -6
  10. package/packages/selector/src/client/EstimatorControls.tsx +277 -277
  11. package/packages/selector/src/client/locales.ts +196 -196
  12. package/packages/selector/src/index.ts +463 -463
  13. package/packages/selector/src/pruner/state.ts +50 -50
  14. package/packages/selector/src/pruner.ts +2402 -2402
  15. package/packages/selector/src/runtime/tokenpilot/advisor-prompt.ts +188 -188
  16. package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +149 -149
  17. package/packages/selector/src/runtime/tokenpilot/advisor.ts +419 -419
  18. package/packages/selector/src/runtime/tokenpilot/benefit.ts +200 -200
  19. package/packages/selector/tests/advisor-report.host.spec.ts +223 -223
  20. package/packages/selector/tests/public/package-contract.client.spec.ts +20 -0
  21. package/packages/selector/tests/runtime/advice-never-withholds.host.spec.ts +232 -232
  22. package/packages/selector/tests/runtime/advisor-invariant.spec.ts +272 -272
  23. package/packages/selector/tests/runtime/advisor.spec.ts +226 -226
  24. package/packages/selector/tests/runtime/char-basis.spec.ts +30 -30
  25. package/packages/selector/tests/runtime/deprecated-preset-options.spec.ts +96 -96
  26. package/packages/selector/tests/runtime/tokenpilot/benefit.spec.ts +217 -217
  27. package/packages/selector/tests/settings-seat.client.spec.ts +29 -4
  28. package/scripts/toolclass-corpus-replay.mjs +281 -281
@@ -1,96 +1,96 @@
1
- /**
2
- * Upgrade safety for the retired review gate's settings keys.
3
- *
4
- * The gate is gone, but its keys are PERSISTED: a live settings document still
5
- * carries `reviewMode: false`, and both the runtime parser and the browser
6
- * decoder reject unknown keys. Removing the keys from the accepted set would
7
- * therefore make the plugin fail to load (runtime) or report the whole settings
8
- * card as unreadable (client). This spec pins the accept-and-ignore contract in
9
- * both directions, and pins that the keys never reach the resolved policy.
10
- */
11
- import { describe, expect, it } from 'vitest'
12
- import {
13
- parsePresetOptionsSettings,
14
- resolveConfig,
15
- resolvePolicy,
16
- } from '../../src/runtime/config.ts'
17
- import { decodePresetOptionsSettings } from '../../src/profiles.ts'
18
-
19
- /** The exact legacy section a pre-0.5.2 install has on disk. */
20
- const LEGACY_SECTION = {
21
- estimatorMode: 'host',
22
- estimatorProvider: 'local-35b',
23
- estimatorModel: 'Qwen3.6-35B-A3B',
24
- estimatorBaseUrl: 'http://192.168.100.242:8200/v1',
25
- reviewMode: false,
26
- }
27
-
28
- describe('retired review-gate keys are accepted and ignored (runtime)', () => {
29
- it('parses the legacy section without throwing and keeps the live keys', () => {
30
- const parsed = parsePresetOptionsSettings(LEGACY_SECTION)
31
- expect(parsed).toEqual({
32
- estimatorMode: 'host',
33
- estimatorProvider: 'local-35b',
34
- estimatorModel: 'Qwen3.6-35B-A3B',
35
- estimatorBaseUrl: 'http://192.168.100.242:8200/v1',
36
- })
37
- })
38
-
39
- it('drops every retired key, even with values the old schema rejected', () => {
40
- // The retired keys are not validated any more — a stale invalid value in an
41
- // existing document must not be able to break the load.
42
- const parsed = parsePresetOptionsSettings({
43
- reviewMode: 'yes',
44
- reviewTimeoutTurns: -1,
45
- cacheHitDiscountAlpha: 'wide',
46
- reviewHighImpactTokens: null,
47
- })
48
- expect(parsed).toEqual({})
49
- })
50
-
51
- it('still rejects a genuinely unknown key', () => {
52
- expect(() => parsePresetOptionsSettings({ reviewModes: true })).toThrow(/unknown key/)
53
- })
54
-
55
- it('never lets a retired key reach the resolved policy matrix', () => {
56
- // The section arrives as `unknown` from the settings document, so the cast
57
- // mirrors the production path rather than widening the config surface.
58
- const policy = resolvePolicy(
59
- resolveConfig({
60
- presetOptions: { reviewMode: true, reviewHighImpactTokens: 1 },
61
- } as unknown as Parameters<typeof resolveConfig>[0]),
62
- 'tokenpilot-inspired',
63
- )
64
- const matrix = policy.presetOptions
65
- expect(matrix).toBeDefined()
66
- for (const gone of ['reviewMode', 'reviewTimeoutTurns', 'cacheHitDiscountAlpha', 'reviewHighImpactTokens']) {
67
- expect(Object.hasOwn(matrix as object, gone)).toBe(false)
68
- }
69
- // The surviving matrix is exactly the live capability set.
70
- expect(Object.keys(matrix as object).sort()).toEqual([
71
- 'advisor', 'dedupeToolResults', 'estimator', 'noNetSavingsGuard',
72
- 'prefixStabilizer', 'readState', 'skipReductionRecovery', 'summaryLocator',
73
- ])
74
- })
75
- })
76
-
77
- describe('retired review-gate keys are accepted and ignored (client mirror)', () => {
78
- it('decodes a legacy section instead of reporting the card unreadable', () => {
79
- const decoded = decodePresetOptionsSettings(LEGACY_SECTION)
80
- // `undefined` here would blank the settings card's presetOptions section.
81
- expect(decoded).toEqual({
82
- estimatorMode: 'host',
83
- estimatorProvider: 'local-35b',
84
- estimatorModel: 'Qwen3.6-35B-A3B',
85
- estimatorBaseUrl: 'http://192.168.100.242:8200/v1',
86
- })
87
- })
88
-
89
- it('tolerates legacy values the old decoder rejected', () => {
90
- expect(decodePresetOptionsSettings({ reviewMode: 'yes', reviewHighImpactTokens: -5 })).toEqual({})
91
- })
92
-
93
- it('still refuses an unknown key', () => {
94
- expect(decodePresetOptionsSettings({ reviewModes: true })).toBeUndefined()
95
- })
96
- })
1
+ /**
2
+ * Upgrade safety for the retired review gate's settings keys.
3
+ *
4
+ * The gate is gone, but its keys are PERSISTED: a live settings document still
5
+ * carries `reviewMode: false`, and both the runtime parser and the browser
6
+ * decoder reject unknown keys. Removing the keys from the accepted set would
7
+ * therefore make the plugin fail to load (runtime) or report the whole settings
8
+ * card as unreadable (client). This spec pins the accept-and-ignore contract in
9
+ * both directions, and pins that the keys never reach the resolved policy.
10
+ */
11
+ import { describe, expect, it } from 'vitest'
12
+ import {
13
+ parsePresetOptionsSettings,
14
+ resolveConfig,
15
+ resolvePolicy,
16
+ } from '../../src/runtime/config.ts'
17
+ import { decodePresetOptionsSettings } from '../../src/profiles.ts'
18
+
19
+ /** The exact legacy section a pre-0.5.2 install has on disk. */
20
+ const LEGACY_SECTION = {
21
+ estimatorMode: 'host',
22
+ estimatorProvider: 'local-35b',
23
+ estimatorModel: 'Qwen3.6-35B-A3B',
24
+ estimatorBaseUrl: 'http://192.168.100.242:8200/v1',
25
+ reviewMode: false,
26
+ }
27
+
28
+ describe('retired review-gate keys are accepted and ignored (runtime)', () => {
29
+ it('parses the legacy section without throwing and keeps the live keys', () => {
30
+ const parsed = parsePresetOptionsSettings(LEGACY_SECTION)
31
+ expect(parsed).toEqual({
32
+ estimatorMode: 'host',
33
+ estimatorProvider: 'local-35b',
34
+ estimatorModel: 'Qwen3.6-35B-A3B',
35
+ estimatorBaseUrl: 'http://192.168.100.242:8200/v1',
36
+ })
37
+ })
38
+
39
+ it('drops every retired key, even with values the old schema rejected', () => {
40
+ // The retired keys are not validated any more — a stale invalid value in an
41
+ // existing document must not be able to break the load.
42
+ const parsed = parsePresetOptionsSettings({
43
+ reviewMode: 'yes',
44
+ reviewTimeoutTurns: -1,
45
+ cacheHitDiscountAlpha: 'wide',
46
+ reviewHighImpactTokens: null,
47
+ })
48
+ expect(parsed).toEqual({})
49
+ })
50
+
51
+ it('still rejects a genuinely unknown key', () => {
52
+ expect(() => parsePresetOptionsSettings({ reviewModes: true })).toThrow(/unknown key/)
53
+ })
54
+
55
+ it('never lets a retired key reach the resolved policy matrix', () => {
56
+ // The section arrives as `unknown` from the settings document, so the cast
57
+ // mirrors the production path rather than widening the config surface.
58
+ const policy = resolvePolicy(
59
+ resolveConfig({
60
+ presetOptions: { reviewMode: true, reviewHighImpactTokens: 1 },
61
+ } as unknown as Parameters<typeof resolveConfig>[0]),
62
+ 'tokenpilot-inspired',
63
+ )
64
+ const matrix = policy.presetOptions
65
+ expect(matrix).toBeDefined()
66
+ for (const gone of ['reviewMode', 'reviewTimeoutTurns', 'cacheHitDiscountAlpha', 'reviewHighImpactTokens']) {
67
+ expect(Object.hasOwn(matrix as object, gone)).toBe(false)
68
+ }
69
+ // The surviving matrix is exactly the live capability set.
70
+ expect(Object.keys(matrix as object).sort()).toEqual([
71
+ 'advisor', 'dedupeToolResults', 'estimator', 'noNetSavingsGuard',
72
+ 'prefixStabilizer', 'readState', 'skipReductionRecovery', 'summaryLocator',
73
+ ])
74
+ })
75
+ })
76
+
77
+ describe('retired review-gate keys are accepted and ignored (client mirror)', () => {
78
+ it('decodes a legacy section instead of reporting the card unreadable', () => {
79
+ const decoded = decodePresetOptionsSettings(LEGACY_SECTION)
80
+ // `undefined` here would blank the settings card's presetOptions section.
81
+ expect(decoded).toEqual({
82
+ estimatorMode: 'host',
83
+ estimatorProvider: 'local-35b',
84
+ estimatorModel: 'Qwen3.6-35B-A3B',
85
+ estimatorBaseUrl: 'http://192.168.100.242:8200/v1',
86
+ })
87
+ })
88
+
89
+ it('tolerates legacy values the old decoder rejected', () => {
90
+ expect(decodePresetOptionsSettings({ reviewMode: 'yes', reviewHighImpactTokens: -5 })).toEqual({})
91
+ })
92
+
93
+ it('still refuses an unknown key', () => {
94
+ expect(decodePresetOptionsSettings({ reviewModes: true })).toBeUndefined()
95
+ })
96
+ })
@@ -1,217 +1,217 @@
1
- import { describe, expect, it } from 'vitest'
2
- import {
3
- adviseCandidates,
4
- computeBenefit,
5
- DEFAULT_ADVICE_ALPHA,
6
- DEFAULT_ADVICE_HIGH_IMPACT_TOKENS,
7
- } from '../../../src/runtime/tokenpilot/benefit.ts'
8
-
9
- /** Reference values follow the spec formulas exactly:
10
- * R = Σ(before − after); penalty = (1−α)·tail; payback = penalty / (α·R);
11
- * expectedSaving = α·R·max(0, Ŝ − payback). */
12
-
13
- describe('computeBenefit', () => {
14
- it('returns zero recovery and no payback for an empty batch', () => {
15
- const result = computeBenefit([], { alpha: 0.1, tailTokens: 4000 })
16
- expect(result.recoveredTokens).toBe(0)
17
- expect(result.penaltyTokens).toBe(3600)
18
- expect(result.paybackTurns).toBeUndefined()
19
- expect(result.expectedSaving).toBeUndefined()
20
- })
21
-
22
- it('computes the single-candidate benefit with a known remaining-turn estimate', () => {
23
- // R = 4000 − 400 = 3600; penalty = 0.9·4000 = 3600; per-turn = 0.1·3600 = 360
24
- const result = computeBenefit(
25
- [{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
26
- { alpha: 0.1, tailTokens: 4000, remainingTurns: 12 },
27
- )
28
- expect(result.recoveredTokens).toBe(3600)
29
- expect(result.penaltyTokens).toBeCloseTo(3600)
30
- expect(result.paybackTurns).toBeCloseTo(10)
31
- // α·R·max(0, 12 − 10) = 360·2
32
- expect(result.expectedSaving).toBeCloseTo(720)
33
- })
34
-
35
- it('omits expectedSaving while Ŝ is unknown', () => {
36
- const result = computeBenefit(
37
- [{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
38
- { alpha: 0.1, tailTokens: 4000 },
39
- )
40
- expect(result.paybackTurns).toBeCloseTo(10)
41
- expect(result.expectedSaving).toBeUndefined()
42
- })
43
-
44
- it('merges candidates into one batch so the penalty is paid once', () => {
45
- const result = computeBenefit(
46
- [
47
- { sourceSeq: 1, tokensBefore: 2000, tokensAfter: 500 },
48
- { sourceSeq: 2, tokensBefore: 3000, tokensAfter: 1000 },
49
- ],
50
- { alpha: 0.1, tailTokens: 4000, remainingTurns: 20 },
51
- )
52
- expect(result.recoveredTokens).toBe(3500)
53
- expect(result.penaltyTokens).toBeCloseTo(3600)
54
- expect(result.paybackTurns).toBeCloseTo(3600 / 350)
55
- expect(result.expectedSaving).toBeCloseTo(350 * (20 - 3600 / 350))
56
- })
57
-
58
- it('clamps a growing candidate to zero recovery instead of negative batch credit', () => {
59
- const result = computeBenefit(
60
- [{ sourceSeq: 3, tokensBefore: 100, tokensAfter: 400 }],
61
- { alpha: 0.1, tailTokens: 4000 },
62
- )
63
- expect(result.recoveredTokens).toBe(0)
64
- expect(result.paybackTurns).toBeUndefined()
65
- })
66
-
67
- it('stays finite as α → 0: no division, payback undefined, expectedSaving negative', () => {
68
- const result = computeBenefit(
69
- [{ sourceSeq: 4, tokensBefore: 4000, tokensAfter: 400 }],
70
- { alpha: 0, tailTokens: 4000, remainingTurns: 12 },
71
- )
72
- expect(result.recoveredTokens).toBe(3600)
73
- expect(result.paybackTurns).toBeUndefined()
74
- // With zero per-turn saving the batch can only lose the refill penalty.
75
- expect(result.expectedSaving).toBeCloseTo(-4000)
76
- })
77
-
78
- it('never reports a positive expectedSaving once Ŝ is inside the payback window', () => {
79
- // payback = 10 turns; Ŝ = 5 → max(0, Ŝ − payback) = 0
80
- const result = computeBenefit(
81
- [{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
82
- { alpha: 0.1, tailTokens: 4000, remainingTurns: 5 },
83
- )
84
- expect(result.expectedSaving).toBeCloseTo(0)
85
- })
86
-
87
- it('waives the refill penalty for a refill-exempt batch: payback 0, saving α·R·Ŝ', () => {
88
- // Fresh-stage shaping: the content never entered the KV cache, so no
89
- // refill penalty applies — the whole discounted recovery is pure gain.
90
- const result = computeBenefit(
91
- [{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
92
- { alpha: 0.1, tailTokens: 4000, remainingTurns: 12, refillPenaltyExempt: true },
93
- )
94
- expect(result.recoveredTokens).toBe(3600)
95
- expect(result.penaltyTokens).toBe(0)
96
- expect(result.paybackTurns).toBe(0)
97
- // α·R·max(0, Ŝ − 0) = 360·12
98
- expect(result.expectedSaving).toBeCloseTo(4320)
99
- })
100
-
101
- it('keeps the refill penalty when the exemption flag is absent', () => {
102
- const result = computeBenefit(
103
- [{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
104
- { alpha: 0.1, tailTokens: 4000 },
105
- )
106
- expect(result.penaltyTokens).toBeCloseTo(3600)
107
- })
108
- })
109
-
110
- /** The shipped advice defaults are part of the contract: the audit and the
111
- * report quote them, so a silent drift would change what the label means. */
112
- describe('advice defaults', () => {
113
- it('pins the shipped α and high-impact threshold', () => {
114
- expect(DEFAULT_ADVICE_ALPHA).toBe(0.1)
115
- expect(DEFAULT_ADVICE_HIGH_IMPACT_TOKENS).toBe(4_000)
116
- })
117
- })
118
-
119
- describe('adviseCandidates', () => {
120
- // The shipped threshold is exercised by its own case; every other case lifts
121
- // it out of the way so one band can be asserted at a time.
122
- const base = {
123
- alpha: 0.1,
124
- tailTokens: 4000,
125
- highImpactTokens: 1_000_000,
126
- }
127
-
128
- it('returns undefined when no candidate carries a positive recovery', () => {
129
- expect(adviseCandidates([], base)).toBeUndefined()
130
- expect(adviseCandidates([{ sourceSeq: 1, tokensBefore: 100, tokensAfter: 400 }], base)).toBeUndefined()
131
- })
132
-
133
- it('labels a fast-payback batch profitable', () => {
134
- // R = 3600, penalty = 3600, α·R = 360 → payback 10 ≤ 0.25·Ŝ requires Ŝ ≥ 40.
135
- const advice = adviseCandidates(
136
- [{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
137
- { ...base, remainingTurns: 60 },
138
- )
139
- expect(advice?.band).toBe('profitable')
140
- expect(advice?.priced).toBe(1)
141
- expect(advice?.maxTokensBefore).toBe(4_000)
142
- })
143
-
144
- it('labels an unpriceable batch when α prices no recovery', () => {
145
- const advice = adviseCandidates(
146
- [{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
147
- { ...base, alpha: 0, remainingTurns: 12 },
148
- )
149
- expect(advice?.band).toBe('unpriceable')
150
- expect(advice?.benefit.expectedSaving).toBeCloseTo(-4000)
151
- })
152
-
153
- it('lets one high-impact candidate label the whole batch, ahead of a good payback', () => {
154
- // The 8k candidate alone reaches the shipped threshold; the small sibling
155
- // would be profitable on its own. One label per batch, high impact wins.
156
- const advice = adviseCandidates(
157
- [
158
- { sourceSeq: 1, tokensBefore: 200, tokensAfter: 100 },
159
- { sourceSeq: 2, tokensBefore: 8_192, tokensAfter: 1_000 },
160
- ],
161
- { ...base, highImpactTokens: DEFAULT_ADVICE_HIGH_IMPACT_TOKENS, alpha: 1, remainingTurns: 60 },
162
- )
163
- expect(advice?.band).toBe('high-impact')
164
- expect(advice?.maxTokensBefore).toBe(8_192)
165
- })
166
-
167
- it('labels a slowly paying batch slow-payback when Ŝ is known', () => {
168
- // R = 12000 → penalty 3600, α·R = 1200 → payback 3. Ŝ = 11 keeps
169
- // 0.25·Ŝ = 2.75 strictly below the payback, so the batch is not profitable.
170
- const advice = adviseCandidates(
171
- [{ sourceSeq: 7, tokensBefore: 12_000, tokensAfter: 0 }],
172
- { ...base, remainingTurns: 11 },
173
- )
174
- expect(advice?.band).toBe('slow-payback')
175
- })
176
-
177
- it('labels a batch that never pays back not-worth-it, and still returns its benefit', () => {
178
- const advice = adviseCandidates(
179
- [{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
180
- { ...base, remainingTurns: 2 },
181
- )
182
- expect(advice?.band).toBe('not-worth-it')
183
- expect(advice?.benefit.recoveredTokens).toBe(3600)
184
- })
185
-
186
- it('never closes a band on Ŝ: an unknown remaining-turn count keeps the batch priceable', () => {
187
- const advice = adviseCandidates(
188
- [{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
189
- base,
190
- )
191
- // payback 10 > 1 and Ŝ is unknown → the profit bands cannot be argued, so
192
- // the batch is labelled as not worth the cache break rather than fabricated.
193
- expect(advice?.band).toBe('not-worth-it')
194
- expect(advice?.benefit.expectedSaving).toBeUndefined()
195
- })
196
-
197
- it('prices a fresh-stage batch without the refill penalty', () => {
198
- const advice = adviseCandidates(
199
- [{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
200
- { ...base, highImpactTokens: 1_000_000, stage: 'fresh', remainingTurns: 12 },
201
- )
202
- expect(advice?.benefit.penaltyTokens).toBe(0)
203
- expect(advice?.band).toBe('profitable')
204
- })
205
-
206
- it('counts only the positively-recovering candidates it priced', () => {
207
- const advice = adviseCandidates(
208
- [
209
- { sourceSeq: 1, tokensBefore: 100, tokensAfter: 400 },
210
- { sourceSeq: 2, tokensBefore: 4_000, tokensAfter: 400 },
211
- ],
212
- { ...base, remainingTurns: 60 },
213
- )
214
- expect(advice?.priced).toBe(1)
215
- expect(advice?.maxTokensBefore).toBe(4_000)
216
- })
217
- })
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ adviseCandidates,
4
+ computeBenefit,
5
+ DEFAULT_ADVICE_ALPHA,
6
+ DEFAULT_ADVICE_HIGH_IMPACT_TOKENS,
7
+ } from '../../../src/runtime/tokenpilot/benefit.ts'
8
+
9
+ /** Reference values follow the spec formulas exactly:
10
+ * R = Σ(before − after); penalty = (1−α)·tail; payback = penalty / (α·R);
11
+ * expectedSaving = α·R·max(0, Ŝ − payback). */
12
+
13
+ describe('computeBenefit', () => {
14
+ it('returns zero recovery and no payback for an empty batch', () => {
15
+ const result = computeBenefit([], { alpha: 0.1, tailTokens: 4000 })
16
+ expect(result.recoveredTokens).toBe(0)
17
+ expect(result.penaltyTokens).toBe(3600)
18
+ expect(result.paybackTurns).toBeUndefined()
19
+ expect(result.expectedSaving).toBeUndefined()
20
+ })
21
+
22
+ it('computes the single-candidate benefit with a known remaining-turn estimate', () => {
23
+ // R = 4000 − 400 = 3600; penalty = 0.9·4000 = 3600; per-turn = 0.1·3600 = 360
24
+ const result = computeBenefit(
25
+ [{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
26
+ { alpha: 0.1, tailTokens: 4000, remainingTurns: 12 },
27
+ )
28
+ expect(result.recoveredTokens).toBe(3600)
29
+ expect(result.penaltyTokens).toBeCloseTo(3600)
30
+ expect(result.paybackTurns).toBeCloseTo(10)
31
+ // α·R·max(0, 12 − 10) = 360·2
32
+ expect(result.expectedSaving).toBeCloseTo(720)
33
+ })
34
+
35
+ it('omits expectedSaving while Ŝ is unknown', () => {
36
+ const result = computeBenefit(
37
+ [{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
38
+ { alpha: 0.1, tailTokens: 4000 },
39
+ )
40
+ expect(result.paybackTurns).toBeCloseTo(10)
41
+ expect(result.expectedSaving).toBeUndefined()
42
+ })
43
+
44
+ it('merges candidates into one batch so the penalty is paid once', () => {
45
+ const result = computeBenefit(
46
+ [
47
+ { sourceSeq: 1, tokensBefore: 2000, tokensAfter: 500 },
48
+ { sourceSeq: 2, tokensBefore: 3000, tokensAfter: 1000 },
49
+ ],
50
+ { alpha: 0.1, tailTokens: 4000, remainingTurns: 20 },
51
+ )
52
+ expect(result.recoveredTokens).toBe(3500)
53
+ expect(result.penaltyTokens).toBeCloseTo(3600)
54
+ expect(result.paybackTurns).toBeCloseTo(3600 / 350)
55
+ expect(result.expectedSaving).toBeCloseTo(350 * (20 - 3600 / 350))
56
+ })
57
+
58
+ it('clamps a growing candidate to zero recovery instead of negative batch credit', () => {
59
+ const result = computeBenefit(
60
+ [{ sourceSeq: 3, tokensBefore: 100, tokensAfter: 400 }],
61
+ { alpha: 0.1, tailTokens: 4000 },
62
+ )
63
+ expect(result.recoveredTokens).toBe(0)
64
+ expect(result.paybackTurns).toBeUndefined()
65
+ })
66
+
67
+ it('stays finite as α → 0: no division, payback undefined, expectedSaving negative', () => {
68
+ const result = computeBenefit(
69
+ [{ sourceSeq: 4, tokensBefore: 4000, tokensAfter: 400 }],
70
+ { alpha: 0, tailTokens: 4000, remainingTurns: 12 },
71
+ )
72
+ expect(result.recoveredTokens).toBe(3600)
73
+ expect(result.paybackTurns).toBeUndefined()
74
+ // With zero per-turn saving the batch can only lose the refill penalty.
75
+ expect(result.expectedSaving).toBeCloseTo(-4000)
76
+ })
77
+
78
+ it('never reports a positive expectedSaving once Ŝ is inside the payback window', () => {
79
+ // payback = 10 turns; Ŝ = 5 → max(0, Ŝ − payback) = 0
80
+ const result = computeBenefit(
81
+ [{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
82
+ { alpha: 0.1, tailTokens: 4000, remainingTurns: 5 },
83
+ )
84
+ expect(result.expectedSaving).toBeCloseTo(0)
85
+ })
86
+
87
+ it('waives the refill penalty for a refill-exempt batch: payback 0, saving α·R·Ŝ', () => {
88
+ // Fresh-stage shaping: the content never entered the KV cache, so no
89
+ // refill penalty applies — the whole discounted recovery is pure gain.
90
+ const result = computeBenefit(
91
+ [{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
92
+ { alpha: 0.1, tailTokens: 4000, remainingTurns: 12, refillPenaltyExempt: true },
93
+ )
94
+ expect(result.recoveredTokens).toBe(3600)
95
+ expect(result.penaltyTokens).toBe(0)
96
+ expect(result.paybackTurns).toBe(0)
97
+ // α·R·max(0, Ŝ − 0) = 360·12
98
+ expect(result.expectedSaving).toBeCloseTo(4320)
99
+ })
100
+
101
+ it('keeps the refill penalty when the exemption flag is absent', () => {
102
+ const result = computeBenefit(
103
+ [{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
104
+ { alpha: 0.1, tailTokens: 4000 },
105
+ )
106
+ expect(result.penaltyTokens).toBeCloseTo(3600)
107
+ })
108
+ })
109
+
110
+ /** The shipped advice defaults are part of the contract: the audit and the
111
+ * report quote them, so a silent drift would change what the label means. */
112
+ describe('advice defaults', () => {
113
+ it('pins the shipped α and high-impact threshold', () => {
114
+ expect(DEFAULT_ADVICE_ALPHA).toBe(0.1)
115
+ expect(DEFAULT_ADVICE_HIGH_IMPACT_TOKENS).toBe(4_000)
116
+ })
117
+ })
118
+
119
+ describe('adviseCandidates', () => {
120
+ // The shipped threshold is exercised by its own case; every other case lifts
121
+ // it out of the way so one band can be asserted at a time.
122
+ const base = {
123
+ alpha: 0.1,
124
+ tailTokens: 4000,
125
+ highImpactTokens: 1_000_000,
126
+ }
127
+
128
+ it('returns undefined when no candidate carries a positive recovery', () => {
129
+ expect(adviseCandidates([], base)).toBeUndefined()
130
+ expect(adviseCandidates([{ sourceSeq: 1, tokensBefore: 100, tokensAfter: 400 }], base)).toBeUndefined()
131
+ })
132
+
133
+ it('labels a fast-payback batch profitable', () => {
134
+ // R = 3600, penalty = 3600, α·R = 360 → payback 10 ≤ 0.25·Ŝ requires Ŝ ≥ 40.
135
+ const advice = adviseCandidates(
136
+ [{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
137
+ { ...base, remainingTurns: 60 },
138
+ )
139
+ expect(advice?.band).toBe('profitable')
140
+ expect(advice?.priced).toBe(1)
141
+ expect(advice?.maxTokensBefore).toBe(4_000)
142
+ })
143
+
144
+ it('labels an unpriceable batch when α prices no recovery', () => {
145
+ const advice = adviseCandidates(
146
+ [{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
147
+ { ...base, alpha: 0, remainingTurns: 12 },
148
+ )
149
+ expect(advice?.band).toBe('unpriceable')
150
+ expect(advice?.benefit.expectedSaving).toBeCloseTo(-4000)
151
+ })
152
+
153
+ it('lets one high-impact candidate label the whole batch, ahead of a good payback', () => {
154
+ // The 8k candidate alone reaches the shipped threshold; the small sibling
155
+ // would be profitable on its own. One label per batch, high impact wins.
156
+ const advice = adviseCandidates(
157
+ [
158
+ { sourceSeq: 1, tokensBefore: 200, tokensAfter: 100 },
159
+ { sourceSeq: 2, tokensBefore: 8_192, tokensAfter: 1_000 },
160
+ ],
161
+ { ...base, highImpactTokens: DEFAULT_ADVICE_HIGH_IMPACT_TOKENS, alpha: 1, remainingTurns: 60 },
162
+ )
163
+ expect(advice?.band).toBe('high-impact')
164
+ expect(advice?.maxTokensBefore).toBe(8_192)
165
+ })
166
+
167
+ it('labels a slowly paying batch slow-payback when Ŝ is known', () => {
168
+ // R = 12000 → penalty 3600, α·R = 1200 → payback 3. Ŝ = 11 keeps
169
+ // 0.25·Ŝ = 2.75 strictly below the payback, so the batch is not profitable.
170
+ const advice = adviseCandidates(
171
+ [{ sourceSeq: 7, tokensBefore: 12_000, tokensAfter: 0 }],
172
+ { ...base, remainingTurns: 11 },
173
+ )
174
+ expect(advice?.band).toBe('slow-payback')
175
+ })
176
+
177
+ it('labels a batch that never pays back not-worth-it, and still returns its benefit', () => {
178
+ const advice = adviseCandidates(
179
+ [{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
180
+ { ...base, remainingTurns: 2 },
181
+ )
182
+ expect(advice?.band).toBe('not-worth-it')
183
+ expect(advice?.benefit.recoveredTokens).toBe(3600)
184
+ })
185
+
186
+ it('never closes a band on Ŝ: an unknown remaining-turn count keeps the batch priceable', () => {
187
+ const advice = adviseCandidates(
188
+ [{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
189
+ base,
190
+ )
191
+ // payback 10 > 1 and Ŝ is unknown → the profit bands cannot be argued, so
192
+ // the batch is labelled as not worth the cache break rather than fabricated.
193
+ expect(advice?.band).toBe('not-worth-it')
194
+ expect(advice?.benefit.expectedSaving).toBeUndefined()
195
+ })
196
+
197
+ it('prices a fresh-stage batch without the refill penalty', () => {
198
+ const advice = adviseCandidates(
199
+ [{ sourceSeq: 7, tokensBefore: 4_000, tokensAfter: 400 }],
200
+ { ...base, highImpactTokens: 1_000_000, stage: 'fresh', remainingTurns: 12 },
201
+ )
202
+ expect(advice?.benefit.penaltyTokens).toBe(0)
203
+ expect(advice?.band).toBe('profitable')
204
+ })
205
+
206
+ it('counts only the positively-recovering candidates it priced', () => {
207
+ const advice = adviseCandidates(
208
+ [
209
+ { sourceSeq: 1, tokensBefore: 100, tokensAfter: 400 },
210
+ { sourceSeq: 2, tokensBefore: 4_000, tokensAfter: 400 },
211
+ ],
212
+ { ...base, remainingTurns: 60 },
213
+ )
214
+ expect(advice?.priced).toBe(1)
215
+ expect(advice?.maxTokensBefore).toBe(4_000)
216
+ })
217
+ })