@hegemonart/get-design-done 1.27.1 → 1.27.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.
@@ -0,0 +1,309 @@
1
+ /**
2
+ * scripts/lib/bandit-router/integration.cjs — Plan 27.5-01
3
+ *
4
+ * Production-integration shim for the Phase 23.5 bandit posterior +
5
+ * Phase 27-07 delegate dimension. Hides the `pull` vs `pullWithDelegate`
6
+ * + `update` vs `updateWithDelegate` choice from callers.
7
+ *
8
+ * Two functions:
9
+ * consultBandit({agent, bin, delegate, agentFrontmatter, adaptiveMode, baseDir?, posteriorPath?})
10
+ * → {tier, decision_log}
11
+ * recordOutcome({agent, bin, delegate, tier, status, costUsd, adaptiveMode, baseDir?, posteriorPath?})
12
+ * → void (best-effort write per D-04)
13
+ *
14
+ * Routing rules (D-05 + D-07):
15
+ * 1. agentFrontmatter.tier_override is set → bypass bandit, return tier_override
16
+ * 2. adaptiveMode !== 'full' → bandit silent, return frontmatter.default_tier
17
+ * (covers 'static' and 'hedge' per D-07)
18
+ * 3. adaptiveMode === 'full' && delegate is 'none' / undefined
19
+ * → call pull()
20
+ * 4. adaptiveMode === 'full' && delegate is a peer name
21
+ * → call pullWithDelegate({delegates:[delegate]})
22
+ *
23
+ * recordOutcome is symmetric on the adaptive_mode gate:
24
+ * - non-'full' → no-op
25
+ * - 'full' && delegate 'none'/undefined → update()
26
+ * - 'full' && delegate is a peer → updateWithDelegate()
27
+ *
28
+ * Reward function = Phase 23.5's computeReward unchanged (D-08).
29
+ *
30
+ * Posterior writes are best-effort — all throws are swallowed. The
31
+ * shim's job is to plumb the call; telemetry resilience is downstream.
32
+ */
33
+
34
+ 'use strict';
35
+
36
+ const banditRouter = require('../bandit-router.cjs');
37
+ const adaptiveModeLib = require('../adaptive-mode.cjs');
38
+
39
+ const DELEGATE_NONE = banditRouter.DELEGATE_NONE; // 'none'
40
+ const VALID_DELEGATES = banditRouter.DEFAULT_DELEGATES; // ['none','gemini','codex','cursor','copilot','qwen']
41
+
42
+ /**
43
+ * Validate that `delegate` is either undefined, DELEGATE_NONE, or a
44
+ * member of VALID_DELEGATES. Returns the canonical delegate string
45
+ * (undefined → 'none').
46
+ *
47
+ * @param {string|undefined} delegate
48
+ * @param {string} fnName — for error message context
49
+ * @returns {string}
50
+ */
51
+ function resolveDelegate(delegate, fnName) {
52
+ if (delegate === undefined || delegate === null) return DELEGATE_NONE;
53
+ if (typeof delegate !== 'string') {
54
+ throw new TypeError(
55
+ `integration.${fnName}: delegate must be a string when provided, got ${typeof delegate}`,
56
+ );
57
+ }
58
+ if (!VALID_DELEGATES.includes(delegate)) {
59
+ throw new RangeError(
60
+ `integration.${fnName}: unknown delegate '${delegate}'; expected one of ${VALID_DELEGATES.join(',')}`,
61
+ );
62
+ }
63
+ return delegate;
64
+ }
65
+
66
+ /**
67
+ * Resolve the adaptive_mode for a call. If the caller passed it
68
+ * explicitly we use that; otherwise we read it from disk via
69
+ * adaptive-mode.getMode (D-07: single gating surface).
70
+ *
71
+ * @param {string|undefined} adaptiveMode
72
+ * @param {{baseDir?: string}} opts
73
+ * @returns {'static'|'hedge'|'full'}
74
+ */
75
+ function resolveAdaptiveMode(adaptiveMode, opts) {
76
+ if (typeof adaptiveMode === 'string' && adaptiveMode.length > 0) {
77
+ return /** @type {'static'|'hedge'|'full'} */ (adaptiveMode);
78
+ }
79
+ return adaptiveModeLib.getMode({ baseDir: opts && opts.baseDir, quiet: true });
80
+ }
81
+
82
+ /**
83
+ * consultBandit — single canonical lookup that returns a tier + a
84
+ * decision_log explaining how the tier was chosen. Five paths:
85
+ *
86
+ * Path 1 — static mode → frontmatter.default_tier (or 'sonnet' fallback)
87
+ * Path 2 — tier_override set on frontmatter → bypass bandit
88
+ * Path 3 — full mode + delegate='none' (or undefined) → pull()
89
+ * Path 4 — full mode + delegate=<peer> → pullWithDelegate()
90
+ * Path 5 — hedge mode → frontmatter.default_tier (bandit silent)
91
+ *
92
+ * Path 2 takes precedence over Path 1 / 3 / 4 / 5 (tier_override is the
93
+ * explicit operator override per D-05).
94
+ *
95
+ * @param {{
96
+ * agent: string,
97
+ * bin: string,
98
+ * delegate?: string,
99
+ * agentFrontmatter?: {tier_override?: string, default_tier?: string},
100
+ * adaptiveMode?: 'static'|'hedge'|'full',
101
+ * baseDir?: string,
102
+ * posteriorPath?: string,
103
+ * }} input
104
+ * @returns {{
105
+ * tier: string,
106
+ * decision_log: {
107
+ * source: 'frontmatter'|'tier_override_bypass'|'bandit_pull'|'bandit_pull_with_delegate',
108
+ * samples?: object,
109
+ * delegate?: string,
110
+ * adaptive_mode: 'static'|'hedge'|'full',
111
+ * reason?: string,
112
+ * }
113
+ * }}
114
+ */
115
+ function consultBandit(input) {
116
+ if (!input || typeof input !== 'object') {
117
+ throw new TypeError('integration.consultBandit: input object required');
118
+ }
119
+ if (typeof input.agent !== 'string' || input.agent.length === 0) {
120
+ throw new TypeError('integration.consultBandit: agent (string) required');
121
+ }
122
+ if (typeof input.bin !== 'string' || input.bin.length === 0) {
123
+ throw new TypeError('integration.consultBandit: bin (string) required');
124
+ }
125
+
126
+ const agentFrontmatter = input.agentFrontmatter && typeof input.agentFrontmatter === 'object'
127
+ ? input.agentFrontmatter
128
+ : {};
129
+ const adaptiveMode = resolveAdaptiveMode(input.adaptiveMode, input);
130
+ const delegate = resolveDelegate(input.delegate, 'consultBandit');
131
+
132
+ // Step 1 — tier_override bypass (D-05). Highest priority; beats both
133
+ // bandit consultation and static/hedge frontmatter.default_tier.
134
+ if (typeof agentFrontmatter.tier_override === 'string' && agentFrontmatter.tier_override.length > 0) {
135
+ return {
136
+ tier: agentFrontmatter.tier_override,
137
+ decision_log: {
138
+ source: 'tier_override_bypass',
139
+ adaptive_mode: adaptiveMode,
140
+ reason: 'frontmatter_tier_override_set',
141
+ },
142
+ };
143
+ }
144
+
145
+ // Step 2 — non-full short-circuit (D-07). Static and hedge are both
146
+ // "bandit silent"; frontmatter.default_tier (or 'sonnet' fallback)
147
+ // is authoritative. No posterior read or write.
148
+ if (adaptiveMode !== 'full') {
149
+ const fallbackTier = (typeof agentFrontmatter.default_tier === 'string' && agentFrontmatter.default_tier.length > 0)
150
+ ? agentFrontmatter.default_tier
151
+ : 'sonnet';
152
+ return {
153
+ tier: fallbackTier,
154
+ decision_log: {
155
+ source: 'frontmatter',
156
+ adaptive_mode: adaptiveMode,
157
+ reason: adaptiveMode === 'hedge' ? 'hedge_mode_skips_bandit' : 'static_mode_authoritative',
158
+ },
159
+ };
160
+ }
161
+
162
+ // Step 3/4 — full mode → consult the bandit. Choice of pull vs
163
+ // pullWithDelegate is driven by `delegate`:
164
+ // delegate === 'none' (or undefined → 'none') → pull()
165
+ // delegate ∈ {gemini,codex,cursor,copilot,qwen} → pullWithDelegate
166
+ if (delegate === DELEGATE_NONE) {
167
+ const result = banditRouter.pull({
168
+ agent: input.agent,
169
+ bin: input.bin,
170
+ baseDir: input.baseDir,
171
+ posteriorPath: input.posteriorPath,
172
+ });
173
+ return {
174
+ tier: result.tier,
175
+ decision_log: {
176
+ source: 'bandit_pull',
177
+ samples: result.samples,
178
+ delegate: DELEGATE_NONE,
179
+ adaptive_mode: 'full',
180
+ },
181
+ };
182
+ }
183
+
184
+ // Path 4 — peer delegate. Constrain the delegate axis to the single
185
+ // requested peer so the bandit samples the (tier × delegate) joint
186
+ // restricted to {delegate}. Same posterior file, same arm shape; the
187
+ // arm's `delegate` field is set so the slice is distinct from local.
188
+ const result = banditRouter.pullWithDelegate({
189
+ agent: input.agent,
190
+ bin: input.bin,
191
+ delegates: [delegate],
192
+ baseDir: input.baseDir,
193
+ posteriorPath: input.posteriorPath,
194
+ });
195
+ return {
196
+ tier: result.tier,
197
+ decision_log: {
198
+ source: 'bandit_pull_with_delegate',
199
+ samples: result.samples,
200
+ delegate: result.delegate,
201
+ adaptive_mode: 'full',
202
+ },
203
+ };
204
+ }
205
+
206
+ /**
207
+ * recordOutcome — post-spawn telemetry update. Computes a reward via
208
+ * computeReward (Phase 23.5 D-08, unchanged) and writes the posterior
209
+ * arm. Best-effort: all errors swallowed so telemetry can never break
210
+ * a session (D-04).
211
+ *
212
+ * No-op when adaptive_mode is not 'full' (D-07).
213
+ *
214
+ * @param {{
215
+ * agent: string,
216
+ * bin: string,
217
+ * delegate?: string,
218
+ * tier: string,
219
+ * status: string,
220
+ * costUsd?: number,
221
+ * adaptiveMode?: 'static'|'hedge'|'full',
222
+ * baseDir?: string,
223
+ * posteriorPath?: string,
224
+ * }} input
225
+ * @returns {void}
226
+ */
227
+ function recordOutcome(input) {
228
+ if (!input || typeof input !== 'object') {
229
+ throw new TypeError('integration.recordOutcome: input object required');
230
+ }
231
+ if (typeof input.agent !== 'string' || input.agent.length === 0) {
232
+ throw new TypeError('integration.recordOutcome: agent (string) required');
233
+ }
234
+ if (typeof input.bin !== 'string' || input.bin.length === 0) {
235
+ throw new TypeError('integration.recordOutcome: bin (string) required');
236
+ }
237
+ if (typeof input.tier !== 'string' || input.tier.length === 0) {
238
+ throw new TypeError('integration.recordOutcome: tier (string) required');
239
+ }
240
+ if (typeof input.status !== 'string') {
241
+ throw new TypeError('integration.recordOutcome: status (string) required');
242
+ }
243
+
244
+ const adaptiveMode = resolveAdaptiveMode(input.adaptiveMode, input);
245
+
246
+ // D-07 + D-04: posterior is silent in static/hedge. No-op early.
247
+ if (adaptiveMode !== 'full') {
248
+ return undefined;
249
+ }
250
+
251
+ const delegate = resolveDelegate(input.delegate, 'recordOutcome');
252
+
253
+ // D-08: reward function unchanged. wall_time_ms always 0 per
254
+ // Phase 23.5 / 27.5 — the wall-time tiebreaker is not used at the
255
+ // recordOutcome boundary; correctness + cost are the only signals.
256
+ const reward = banditRouter.computeReward({
257
+ solidify_pass: input.status === 'completed',
258
+ cost_usd: typeof input.costUsd === 'number' ? input.costUsd : 0,
259
+ wall_time_ms: 0,
260
+ });
261
+
262
+ // D-04: best-effort write. Swallow ALL exceptions so a broken
263
+ // posterior file never breaks a session.
264
+ try {
265
+ if (delegate === DELEGATE_NONE) {
266
+ banditRouter.update({
267
+ agent: input.agent,
268
+ bin: input.bin,
269
+ tier: input.tier,
270
+ reward,
271
+ baseDir: input.baseDir,
272
+ posteriorPath: input.posteriorPath,
273
+ });
274
+ } else {
275
+ banditRouter.updateWithDelegate({
276
+ agent: input.agent,
277
+ bin: input.bin,
278
+ tier: input.tier,
279
+ delegate,
280
+ reward,
281
+ baseDir: input.baseDir,
282
+ posteriorPath: input.posteriorPath,
283
+ });
284
+ }
285
+ } catch (err) {
286
+ // Live-tail breadcrumb opt-in via env var. Inner try/catch around
287
+ // the stderr write itself keeps the swallow guarantee even when
288
+ // stderr is closed/unavailable.
289
+ if (process.env.GDD_BANDIT_DEBUG === '1') {
290
+ try {
291
+ process.stderr.write(
292
+ '[bandit-integration] recordOutcome swallowed: ' +
293
+ (err && err.message ? err.message : String(err)) +
294
+ '\n',
295
+ );
296
+ } catch {
297
+ /* swallow */
298
+ }
299
+ }
300
+ }
301
+
302
+ return undefined;
303
+ }
304
+
305
+ module.exports = {
306
+ consultBandit,
307
+ recordOutcome,
308
+ DELEGATE_NONE,
309
+ };
@@ -0,0 +1,292 @@
1
+ /**
2
+ * scripts/lib/cache/gdd-cache-manager.cjs — Plan 27.6-03
3
+ *
4
+ * Cache-warming heuristic (Phase 10.1 cost-governance refinement,
5
+ * Phase 27.6 D-06): score each candidate entry as the multiplicative
6
+ * product of three [0,1]-normalized components — recency, frequency,
7
+ * cost — and warm the top-N entries per cycle.
8
+ *
9
+ * Eviction policy: LRU within the warmed set. When a new top-rank
10
+ * candidate arrives, it displaces the oldest-touched entry from the
11
+ * warmed slot (D-06 wording).
12
+ *
13
+ * Telemetry: each per-cycle decision emits a `cache.warm_decision`
14
+ * event via Phase 22 event-stream so the Phase 27.6-01 perf-analyzer
15
+ * can surface "false-positive rate exceeds threshold" proposals
16
+ * (D-02: 20% default; configurable via
17
+ * `.design/budget.json#cache_warming_falsepositive_threshold`).
18
+ *
19
+ * Pure functions + side-effects-via-appendEvent. No external deps
20
+ * beyond `node:` builtins and the lazy event-stream require.
21
+ *
22
+ * @module scripts/lib/cache/gdd-cache-manager
23
+ */
24
+ 'use strict';
25
+
26
+ // `node:fs` and `node:path` are required by the contract (lazy
27
+ // future-extension surface for budget.json reads); event-stream is
28
+ // loaded lazily via try/catch so this library is consumable from pure
29
+ // CommonJS runtimes that cannot strip TypeScript on the fly.
30
+ const path = require('node:path'); // eslint-disable-line no-unused-vars
31
+ const fs = require('node:fs'); // eslint-disable-line no-unused-vars
32
+
33
+ /**
34
+ * Default top-N candidates warmed per cycle. Override per-call via
35
+ * `rankWarmCandidates({ entries, topN })` or via
36
+ * `.design/budget.json#cache_warm_topn` at the caller level.
37
+ *
38
+ * @type {number}
39
+ */
40
+ const DEFAULT_TOPN = 10;
41
+
42
+ /**
43
+ * Default false-positive tolerance threshold percentage (D-02).
44
+ * When more than this percent of warmed entries are evicted before
45
+ * being read in a single cycle, the heuristic emits a per-cycle
46
+ * `cache.warm_decision` summary event so the perf-analyzer can flag
47
+ * the heuristic as mis-tuned.
48
+ *
49
+ * Configurable per-call via the `falsePositiveThresholdPct` argument
50
+ * to `summarizeFalsePositiveRate`, or at the project level via
51
+ * `.design/budget.json#cache_warming_falsepositive_threshold`.
52
+ *
53
+ * @type {number}
54
+ */
55
+ const DEFAULT_FALSE_POSITIVE_THRESHOLD_PCT = 20;
56
+
57
+ /**
58
+ * Clamp a number to the [0, 1] range. Non-finite / non-numeric inputs
59
+ * collapse to 0 — by design, since a non-numeric component cannot
60
+ * meaningfully participate in the multiplicative score.
61
+ *
62
+ * @param {unknown} n
63
+ * @returns {number}
64
+ */
65
+ function clamp01(n) {
66
+ if (typeof n !== 'number' || !Number.isFinite(n)) return 0;
67
+ if (n < 0) return 0;
68
+ if (n > 1) return 1;
69
+ return n;
70
+ }
71
+
72
+ /**
73
+ * Lazily resolve the Phase 22 event-stream `appendEvent` function.
74
+ * The event-stream is shipped as `index.ts` (TypeScript); under a
75
+ * pure-CommonJS runtime that cannot strip TS on the fly, the require
76
+ * will throw. We swallow that and return a no-op so this library
77
+ * stays usable in any caller — tests, CJS scripts, or the
78
+ * cache-manager slash skill — without forcing them to install a
79
+ * TS loader.
80
+ *
81
+ * @returns {(ev: object) => void}
82
+ */
83
+ function getAppendEvent() {
84
+ try {
85
+ // Resolved relative to this file: `scripts/lib/cache/` → `../event-stream`.
86
+ const m = require('../event-stream');
87
+ if (m && typeof m.appendEvent === 'function') return m.appendEvent;
88
+ } catch {
89
+ // Swallow — fall through to the no-op below. The event is
90
+ // best-effort telemetry; losing one decision is acceptable.
91
+ }
92
+ return function noopAppend(_ev) { /* event-stream unavailable */ };
93
+ }
94
+
95
+ /**
96
+ * Compute the multiplicative warming score for a single candidate
97
+ * (D-06). Each component is clamped to [0, 1] before multiplication;
98
+ * any zero component zeroes the entire score by design — all three
99
+ * dimensions (recency, frequency, cost) must be non-zero for an
100
+ * entry to be a warm candidate at all.
101
+ *
102
+ * @param {{recency_score:number, frequency_score:number, cost_score:number}} components
103
+ * @returns {number} score in [0, 1]
104
+ */
105
+ function computeWarmingScore({ recency_score, frequency_score, cost_score } = {}) {
106
+ const r = clamp01(recency_score);
107
+ const f = clamp01(frequency_score);
108
+ const c = clamp01(cost_score);
109
+ return r * f * c;
110
+ }
111
+
112
+ /**
113
+ * Rank a list of cache candidates by multiplicative warming score and
114
+ * return the top-N as the warmed set, with the remainder as eviction
115
+ * candidates. Pure: no I/O, no event emission.
116
+ *
117
+ * Component normalization:
118
+ * recency_score = 1 / (1 + days_since_last_use)
119
+ * frequency_score = uses_in_window / window_size (clamped)
120
+ * cost_score = est_cost_usd / max(est_cost_usd) (clamped)
121
+ *
122
+ * If all entries have `est_cost_usd === 0`, all cost_scores collapse
123
+ * to 0 (and therefore all final scores collapse to 0) — the heuristic
124
+ * never warms cost-free entries, by design.
125
+ *
126
+ * Eviction policy: entries beyond the top-N rank are returned in
127
+ * `evictionCandidates`. The caller (cache-manager skill) treats the
128
+ * warmed set as LRU internally — when a new entry beats an existing
129
+ * warmed entry, the LRU entry in the warmed set is the one displaced.
130
+ *
131
+ * @param {{
132
+ * entries?: Array<{
133
+ * key: string,
134
+ * days_since_last_use?: number,
135
+ * uses_in_window?: number,
136
+ * window_size?: number,
137
+ * est_cost_usd?: number,
138
+ * last_touched_at?: string,
139
+ * }>,
140
+ * topN?: number,
141
+ * }} args
142
+ * @returns {{warmed: Array<object>, evictionCandidates: Array<object>}}
143
+ */
144
+ function rankWarmCandidates({ entries, topN } = {}) {
145
+ const N = typeof topN === 'number' && topN > 0 ? Math.floor(topN) : DEFAULT_TOPN;
146
+ const list = Array.isArray(entries) ? entries : [];
147
+ if (list.length === 0) return { warmed: [], evictionCandidates: [] };
148
+
149
+ // Determine the per-cycle max cost for normalization. If everything
150
+ // is free, the cost dimension contributes 0 to every score (which
151
+ // zeroes the multiplicative product — that's intentional).
152
+ let maxCost = 0;
153
+ for (const e of list) {
154
+ const c = typeof e.est_cost_usd === 'number' && e.est_cost_usd > 0 ? e.est_cost_usd : 0;
155
+ if (c > maxCost) maxCost = c;
156
+ }
157
+
158
+ const scored = list.map((e) => {
159
+ const days = typeof e.days_since_last_use === 'number' && e.days_since_last_use >= 0
160
+ ? e.days_since_last_use
161
+ : Infinity;
162
+ const recency_score = days === Infinity ? 0 : 1 / (1 + days);
163
+
164
+ const usesIn = typeof e.uses_in_window === 'number' && e.uses_in_window >= 0
165
+ ? e.uses_in_window
166
+ : 0;
167
+ const win = typeof e.window_size === 'number' && e.window_size > 0
168
+ ? e.window_size
169
+ : 1;
170
+ const frequency_score = clamp01(usesIn / win);
171
+
172
+ const cost = typeof e.est_cost_usd === 'number' && e.est_cost_usd >= 0
173
+ ? e.est_cost_usd
174
+ : 0;
175
+ const cost_score = maxCost > 0 ? clamp01(cost / maxCost) : 0;
176
+
177
+ const score = computeWarmingScore({ recency_score, frequency_score, cost_score });
178
+ return { ...e, recency_score, frequency_score, cost_score, score };
179
+ });
180
+
181
+ // Sort by score descending. Stable order on ties is fine for v1.
182
+ scored.sort((a, b) => b.score - a.score);
183
+
184
+ const warmed = scored.slice(0, N);
185
+ const evictionCandidates = scored.slice(N);
186
+ return { warmed, evictionCandidates };
187
+ }
188
+
189
+ /**
190
+ * Emit one `cache.warm_decision` event recording the outcome of a
191
+ * single warmed entry: was it used before being evicted, or did the
192
+ * heuristic mis-warm (false-positive)?
193
+ *
194
+ * Called per warmed entry at eviction time by the cache layer. The
195
+ * 27.6-01 perf-analyzer aggregates these to compute per-cycle
196
+ * false-positive rates.
197
+ *
198
+ * Side effect only — returns void. The function is best-effort:
199
+ * if the event-stream is unavailable, the emission silently no-ops.
200
+ *
201
+ * @param {{
202
+ * entry: {key:string, score:number, recency_score:number, frequency_score:number, cost_score:number},
203
+ * usedBeforeEviction: boolean,
204
+ * evictionEvent?: {at?: string, reason?: string},
205
+ * sessionId?: string,
206
+ * }} args
207
+ * @returns {void}
208
+ */
209
+ function evaluateWarmingDecision({ entry, usedBeforeEviction, evictionEvent, sessionId } = {}) {
210
+ const append = getAppendEvent();
211
+ const e = entry && typeof entry === 'object' ? entry : {};
212
+ append({
213
+ type: 'cache.warm_decision',
214
+ timestamp: new Date().toISOString(),
215
+ sessionId: typeof sessionId === 'string' && sessionId.length > 0
216
+ ? sessionId
217
+ : 'cache-manager',
218
+ payload: {
219
+ entry_key: typeof e.key === 'string' ? e.key : 'unknown',
220
+ score: typeof e.score === 'number' ? e.score : 0,
221
+ recency_score: typeof e.recency_score === 'number' ? e.recency_score : 0,
222
+ frequency_score: typeof e.frequency_score === 'number' ? e.frequency_score : 0,
223
+ cost_score: typeof e.cost_score === 'number' ? e.cost_score : 0,
224
+ used_before_eviction: !!usedBeforeEviction,
225
+ evicted_at: evictionEvent && typeof evictionEvent.at === 'string'
226
+ ? evictionEvent.at
227
+ : undefined,
228
+ },
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Aggregate a cycle's worth of per-entry decisions into a single
234
+ * false-positive rate. If the rate exceeds the configured threshold
235
+ * (D-02 default 20%), emit a per-cycle summary `cache.warm_decision`
236
+ * event so the perf-analyzer can flag the heuristic as mis-tuned.
237
+ *
238
+ * Returns the computed rate + threshold context regardless of whether
239
+ * an event was emitted, so the caller can route the result into its
240
+ * own reporting surface (e.g. the cache-manager slash skill's status
241
+ * output).
242
+ *
243
+ * @param {{
244
+ * decisions?: Array<{entry_key?:string, used_before_eviction?:boolean, score?:number}>,
245
+ * falsePositiveThresholdPct?: number,
246
+ * sessionId?: string,
247
+ * }} args
248
+ * @returns {{false_positive_rate:number, count:number, threshold_pct:number, exceeds_threshold:boolean}}
249
+ */
250
+ function summarizeFalsePositiveRate({ decisions, falsePositiveThresholdPct, sessionId } = {}) {
251
+ const list = Array.isArray(decisions) ? decisions : [];
252
+ const total = list.length;
253
+ let evictedUnused = 0;
254
+ for (const d of list) {
255
+ if (d && d.used_before_eviction === false) evictedUnused++;
256
+ }
257
+ const false_positive_rate = total === 0 ? 0 : evictedUnused / total;
258
+ const threshold_pct = typeof falsePositiveThresholdPct === 'number' && Number.isFinite(falsePositiveThresholdPct)
259
+ ? falsePositiveThresholdPct
260
+ : DEFAULT_FALSE_POSITIVE_THRESHOLD_PCT;
261
+ const exceeds_threshold = false_positive_rate * 100 > threshold_pct;
262
+
263
+ if (exceeds_threshold) {
264
+ const append = getAppendEvent();
265
+ append({
266
+ type: 'cache.warm_decision',
267
+ timestamp: new Date().toISOString(),
268
+ sessionId: typeof sessionId === 'string' && sessionId.length > 0
269
+ ? sessionId
270
+ : 'cache-manager',
271
+ payload: {
272
+ entry_key: '<cycle-summary>',
273
+ score: 0,
274
+ recency_score: 0,
275
+ frequency_score: 0,
276
+ cost_score: 0,
277
+ false_positive_rate,
278
+ },
279
+ });
280
+ }
281
+
282
+ return { false_positive_rate, count: total, threshold_pct, exceeds_threshold };
283
+ }
284
+
285
+ module.exports = {
286
+ computeWarmingScore,
287
+ rankWarmCandidates,
288
+ evaluateWarmingDecision,
289
+ summarizeFalsePositiveRate,
290
+ DEFAULT_TOPN,
291
+ DEFAULT_FALSE_POSITIVE_THRESHOLD_PCT,
292
+ };
@@ -29,6 +29,7 @@
29
29
 
30
30
  import { OperationFailedError } from '../gdd-errors/index.ts';
31
31
  import { getLogger } from '../logger/index.ts';
32
+ import { resolveConcurrency } from '../parallelism-engine/concurrency-tuner.cjs';
32
33
 
33
34
  import {
34
35
  spawnAggregator,
@@ -137,9 +138,12 @@ export async function run(
137
138
  ): Promise<DiscussRunnerResult> {
138
139
  const logger = getLogger();
139
140
  const specs = opts.discussants ?? DEFAULT_DISCUSSANTS;
141
+ // Phase 27.6 D-07: data-driven concurrency default. Falls back to
142
+ // min(cpu-1, 8) when no `parallelism.verdict` events exist in
143
+ // .design/telemetry/events.jsonl. Explicit `opts.concurrency` still wins.
140
144
  const concurrency = opts.concurrency !== undefined && opts.concurrency > 0
141
145
  ? opts.concurrency
142
- : 4;
146
+ : resolveConcurrency();
143
147
  const cwd = opts.cwd ?? process.cwd();
144
148
 
145
149
  logger.info('discuss.runner.started', {
@@ -25,6 +25,7 @@
25
25
  import { resolve as resolvePath } from 'node:path';
26
26
 
27
27
  import { getLogger } from '../logger/index.ts';
28
+ import { resolveConcurrency } from '../parallelism-engine/concurrency-tuner.cjs';
28
29
 
29
30
  import {
30
31
  isParallelismSafe,
@@ -120,7 +121,10 @@ export async function run(
120
121
  ): Promise<ExploreRunnerResult> {
121
122
  const specs: readonly MapperSpec[] = opts.mappers ?? DEFAULT_MAPPERS;
122
123
  const cwd: string = opts.cwd ?? process.cwd();
123
- const concurrency: number = opts.concurrency ?? 4;
124
+ // Phase 27.6 D-07: data-driven concurrency default. Falls back to
125
+ // min(cpu-1, 8) when no `parallelism.verdict` events exist in
126
+ // .design/telemetry/events.jsonl. Explicit `opts.concurrency` still wins.
127
+ const concurrency: number = opts.concurrency ?? resolveConcurrency();
124
128
 
125
129
  const logger = getLogger().child('explore.runner');
126
130