@leonhardthust/dsh-decision-engine 0.0.0-stage → 1.9.1

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 (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -2
  3. package/README.zh.md +57 -0
  4. package/config.example.yml +436 -0
  5. package/cordis.patch.yml +14 -0
  6. package/docs/assets/config-page.png +0 -0
  7. package/docs/assets/decisions-tab.png +0 -0
  8. package/docs/assets/hero.jpeg +0 -0
  9. package/docs/assets/stats-charts-preview.png +0 -0
  10. package/docs/assets/stats-donut-toggle.png +0 -0
  11. package/docs/assets/stats-interactive-charts.png +0 -0
  12. package/docs/assets/stats-point-donut.png +0 -0
  13. package/docs/assets/stats-ranking-bars.png +0 -0
  14. package/docs/auto-review-hotfix.md +33 -0
  15. package/docs/lessons/cloudflare-builds-root-directory.md +51 -0
  16. package/docs/lessons/cold-start-timeout.md +134 -0
  17. package/docs/lessons/endpoint-path-vs-model.md +26 -0
  18. package/docs/lessons/model-field-required-by-provider.md +31 -0
  19. package/docs/local-dev-link.md +54 -0
  20. package/docs/releasing.md +69 -0
  21. package/docs/security-revision.md +130 -0
  22. package/icon.svg +6 -0
  23. package/lib/adapters/agent.js +314 -0
  24. package/lib/adapters/route.js +144 -0
  25. package/lib/adapters/tools.js +186 -0
  26. package/lib/adapters/translate.js +143 -0
  27. package/lib/approval-detail.js +325 -0
  28. package/lib/client.charts.js +36 -0
  29. package/lib/client.js +8715 -0
  30. package/lib/config-file.js +136 -0
  31. package/lib/credentials.js +187 -0
  32. package/lib/decision-cache.js +188 -0
  33. package/lib/decision-points.js +205 -0
  34. package/lib/decision-store.js +774 -0
  35. package/lib/endpoint-fallback.js +143 -0
  36. package/lib/engine.js +312 -0
  37. package/lib/guard/advisory.js +100 -0
  38. package/lib/guard/audit.js +81 -0
  39. package/lib/guard/context.js +128 -0
  40. package/lib/guard/effects.js +104 -0
  41. package/lib/guard/fallback.js +139 -0
  42. package/lib/guard/listener.js +214 -0
  43. package/lib/guard/messages.js +31 -0
  44. package/lib/guard/policy.js +28 -0
  45. package/lib/home-paths.js +86 -0
  46. package/lib/index.js +3823 -0
  47. package/lib/outcomes.js +169 -0
  48. package/lib/points/spec.js +645 -0
  49. package/lib/providers.js +958 -0
  50. package/lib/resilience.js +308 -0
  51. package/lib/scope.js +67 -0
  52. package/lib/server-config.js +351 -0
  53. package/lib/session-events.js +75 -0
  54. package/lib/stats-store.js +916 -0
  55. package/lib/stats.js +1293 -0
  56. package/lib/telemetry.js +465 -0
  57. package/lib/tokenizer.js +201 -0
  58. package/lib/tolerance.js +498 -0
  59. package/lib/vendor/microcharts-charts.css +47 -0
  60. package/lib/vendor-src/entry.js +13 -0
  61. package/lib/vendor-src/jsx-shim.js +34 -0
  62. package/locale/en.json +6 -0
  63. package/locale/zh.json +6 -0
  64. package/package.json +101 -3
  65. package/scripts/migrate-profile.mjs +102 -0
@@ -0,0 +1,498 @@
1
+ /**
2
+ * 「人工审核比例」(tolerance)——**模型无关**的打扰频率设定。
3
+ *
4
+ * ## 为什么需要这一层
5
+ *
6
+ * 现有的授权裁决靠**固定阈值**(`guardConfidenceMin` / `guardIrreversibleCeiling` /
7
+ * `guardTaskRelevanceFloor` …)。这些阈值是在**某一家决策模型**的输出分布上
8
+ * 校准出来的,换一个模型就失去意义:
9
+ *
10
+ * - 同一个 0.4,在 A 模型上是「中风险」,在 B 模型上可能只是「基线噪声」;
11
+ * - 同一个 0.9,在 A 模型上几乎不出现,在 B 模型上是家常便饭。
12
+ *
13
+ * 而用户真正想表达的不是「阈值是多少」,而是「**大概每多少次调用来问一次**」。
14
+ * 这两件事之间隔着一层「模型归一化」,本模块就是那层归一化。
15
+ *
16
+ * ## 归一化怎么做:滚动分位数,而不是固定阈值
17
+ *
18
+ * 每次授权裁决都算出一个 **severity**(严重度排序分数,0–1,不是经校准的危险概率),
19
+ * 把它塞进一个**滚动窗口**(按 provider × model 分开)。窗口里存的是
20
+ * 「这个模型最近怎么说的」,于是:
21
+ *
22
+ * - 「最严重的 1%」= 窗口里的 **P99 分位**,而不是某个固定的绝对分数;
23
+ * - 换模型时窗口是空的,阈值重新学——**这正是模型无关的含义**。
24
+ *
25
+ * 窗口按 provider × model 分桶:两个模型的分数分布没有可比性,混在一个
26
+ * 窗口里算分位数会得到一个两边都不对的阈值。
27
+ *
28
+ * ## 四档语义
29
+ *
30
+ * 自治 autonomous 消除软询问;mandatory deny 与必须人工批准的操作不受影响。
31
+ * 必须审批但审批不可用时直接拒绝,不能用模型替代许可。
32
+ * 轻松 relaxed 只有严重度排在最前的 1% 才询问用户。
33
+ * 默认 default 按决策模型自己的判断提问(**当前的默认行为**,逐字不变)。
34
+ * 谨慎 cautious 在默认判断之外,再把严重度靠前的 25% 也拿来询问。
35
+ *
36
+ * 三档之间是**单调**的(校准完成后):1% ≤ 默认 ≤ 默认 ∪ 25%。自治是例外——
37
+ * 它是「不打扰」,不是「按分位数」。
38
+ *
39
+ * ## 校准不足时怎么办
40
+ *
41
+ * 窗口里还没攒够样本时**算不出可靠的分位数**。此时不猜分位数,改用
42
+ * `fallbackSeverity`——这几个绝对值是**在真实数据上量出来的典型分位数**
43
+ * (见 DEC-035 的实测表),因此冷启动的行为与校准后的行为量级一致,不会
44
+ * 出现「一开始问很多、攒够数据后突然变安静」这种跳变。
45
+ *
46
+ * 校准状态会显示在界面上(校准中 n/50),用户不会以为设置没生效。
47
+ *
48
+ * @module dsh-decision-engine/tolerance
49
+ */
50
+
51
+ /** 四档的固定顺序(界面上的滑块从左到右也是这个顺序)。 */
52
+ export const TOLERANCE_LEVELS = ["autonomous", "relaxed", "default", "cautious"];
53
+
54
+ /** 默认档:`default` = 当前的默认行为(升级不改变行为)。 */
55
+ export const DEFAULT_TOLERANCE = "default";
56
+
57
+ /**
58
+ * 滚动窗口容量(每个 provider × model 一份)。
59
+ *
60
+ * 取 200 的理由:分位数要稳,至少要几十个样本;200 个样本能让 P99 落在
61
+ * 「最���的那一两个」之外(插值后是一个连续阈值),同时窗口不必大到让
62
+ * 「最近」失去意义——用户改用模型后,几十次调用之内就该反映新分布。
63
+ */
64
+ export const TOLERANCE_WINDOW_SIZE = 200;
65
+
66
+ /**
67
+ * 校准「就绪」所需的最小样本数。
68
+ *
69
+ * 50 是折中:样本太少时 P99 只是「最大的那一条」,噪声大到会随机打扰用户;
70
+ * 50 个样本时 P99 插值出来已经落在真实分布的尾部附近。
71
+ */
72
+ export const TOLERANCE_MIN_SAMPLES = 50;
73
+
74
+ /**
75
+ * 严重度的构成常量。
76
+ *
77
+ * ## 风险分:把三档的 choice 连续化
78
+ *
79
+ * 决策模型的 `choice` 只有 low/medium/high 三档,是**粗糙**的量化;同一次
80
+ * 调用里,模型给出的 `probabilities`(high/medium/low 各多少)比 choice 细
81
+ * 得多。用概率合成一个连续分数(`high + medium/2`),让同档内部也能分出
82
+ * 轻重——这正是「按比例挑」所需要的信息:分位数只能区分**连续**分数。
83
+ *
84
+ * 没有 `probabilities` 时(部分自建服务不返回该字段)退回分类兜底值。
85
+ *
86
+ * ## 安全下限:保证最危险的那几条一定排在最前面
87
+ *
88
+ * 分位数是**相对**的:如果模型整体校准很差(所有分数都挤在 0.2 附近),
89
+ * 「P99」可能落在一条平平无奇的调用上,而真正危险的那条反而在中间。
90
+ *
91
+ * 因此对两类**不容置疑**的情况设下限:
92
+ *
93
+ * - 注入嫌疑达线(`injectionP ≥ 0.8`)→ 至少 0.90;
94
+ * - 判定为高风险(`choice === "high"`)→ 至少 0.70。
95
+ *
96
+ * 这两个下限让「最该被人看一眼的那几条」在任何分位数口径下都进得了最前面
97
+ * 的那 1%。下限的值是刻意挑的:0.90 高于实测 P99(≈0.50),0.70 也高于它,
98
+ * 于是它们必然落在最前 1% 内(实测 5/5 条危险样本全部命中)。
99
+ */
100
+ const CATEGORICAL_RISK = { low: 0.1, medium: 0.5, high: 0.9 };
101
+
102
+ /** 注入嫌疑达到这个值就算「安全下限」档(与 `guardInjectionAskThreshold` 默认值一致)。 */
103
+ export const INJECTION_FLOOR_THRESHOLD = 0.8;
104
+
105
+ /** 注入嫌疑达线时的严重度下限。 */
106
+ export const INJECTION_FLOOR_SCORE = 0.9;
107
+
108
+ /** 判定为高风险时的严重度下限。 */
109
+ export const HIGH_RISK_FLOOR_SCORE = 0.7;
110
+
111
+ /**
112
+ * 四档预设。
113
+ *
114
+ * `percentile` = 该档在窗口里的分位数(null = 不按分位数走)。
115
+ * `fallbackSeverity` = 校准不足时用的绝对阈值。
116
+ */
117
+ export const TOLERANCE_PRESETS = {
118
+ autonomous: {
119
+ id: "autonomous",
120
+ label: "自治",
121
+ labelEn: "Autonomous",
122
+ summary: "免除软询问;保留强制审批与拒绝,无人审批时阻止未授权操作",
123
+ summaryEn: "Skips soft questions; preserves mandatory approval and denial, blocking unapproved actions when approval is unavailable",
124
+ percentile: null,
125
+ // 自治档不走分位数路径,这里的值只是「让通用分支也不会触发」的占位。
126
+ fallbackSeverity: Number.POSITIVE_INFINITY,
127
+ },
128
+ relaxed: {
129
+ id: "relaxed",
130
+ label: "轻松",
131
+ labelEn: "Relaxed",
132
+ summary: "只有严重度排在最前的 1% 才询问用户",
133
+ summaryEn: "Only the top 1% by severity asks the user",
134
+ percentile: 0.99,
135
+ // 实测(1421 条真实授权决策,见 DEC-035):severity ≥ 0.60 覆盖 0.47%
136
+ // 的调用,与 P99 分位(≈0.50,覆盖 1.1%)在同一量级。冷启动时用它,
137
+ // 行为与校准后一致,不会出现「一开始问一堆、攒够数据后突然变安静」。
138
+ fallbackSeverity: 0.6,
139
+ },
140
+ default: {
141
+ id: "default",
142
+ label: "默认",
143
+ labelEn: "Default",
144
+ summary: "按决策模型自己的判断提问(当前的默认行为)",
145
+ summaryEn: "Ask according to the decision model's own judgement (current behaviour)",
146
+ percentile: null,
147
+ fallbackSeverity: null,
148
+ },
149
+ cautious: {
150
+ id: "cautious",
151
+ label: "谨慎",
152
+ labelEn: "Cautious",
153
+ summary: "在默认判断之外,再把严重度靠前的 25% 也拿来询问",
154
+ summaryEn: "Beyond the default judgement, also ask for the top 25% by severity",
155
+ percentile: 0.75,
156
+ // 实测:severity ≥ 0.30 覆盖 19.3% 的调用;P75 分位(≈0.28)覆盖 25.1%。
157
+ // 兜底值取实测的中位口径,量级一致。
158
+ fallbackSeverity: 0.3,
159
+ },
160
+ };
161
+
162
+ /** 把任意值夹到 [0, 1]。 */
163
+ function clamp01(value) {
164
+ const n = Number(value);
165
+ if (!Number.isFinite(n)) return 0;
166
+ if (n < 0) return 0;
167
+ if (n > 1) return 1;
168
+ return n;
169
+ }
170
+
171
+ /**
172
+ * 算一次授权决策的**严重度**(0–1,越大越严重)。
173
+ *
174
+ * 三个分量取**最大**而不是加权求和:三个分量各自都是「独立成立的严重性」
175
+ * (风险高 / 可能不可逆且与任务无关 / 疑似注入),其中任何一条成立都足以
176
+ * 让人工审核。加权求和会让「注入 0.9 + 风险 0.05」被平均值稀释掉——
177
+ * 而那恰恰是最该拦下来的一次。
178
+ *
179
+ * @param answers - 已通过 `extractGuardAnswers` 校验的答案。
180
+ * @param options.injectionFloorThreshold - 注入嫌疑达线值;缺省 0.8。
181
+ * @returns 严重度;输入不可用时返回 0。
182
+ */
183
+ export function severityOf(answers, options = {}) {
184
+ if (answers === null || typeof answers !== "object") return 0;
185
+ const risk = answers.risk ?? {};
186
+ const probabilities = risk.probabilities;
187
+ let riskScore;
188
+ if (
189
+ probabilities !== null &&
190
+ typeof probabilities === "object" &&
191
+ Number.isFinite(probabilities.high) &&
192
+ Number.isFinite(probabilities.medium)
193
+ ) {
194
+ riskScore = probabilities.high + 0.5 * probabilities.medium;
195
+ } else {
196
+ riskScore = CATEGORICAL_RISK[risk.choice] ?? 0.5;
197
+ }
198
+ // 「可能不可逆」只有在**与当前任务无关**时才值得打断人:模型自己都确信
199
+ // 这个调用服务于任务时,它对不可逆的顾虑多半是模型对某类工具的惯性谨慎
200
+ // (实测:普通 bash 命令的 irreversible 输出基线在 0.2–0.4)。
201
+ const irrelevance = clamp01(answers.irreversibleP) * (1 - clamp01(answers.taskP));
202
+ const injection = clamp01(answers.injectionP);
203
+ let severity = Math.max(riskScore, irrelevance, injection);
204
+ // 安全下限:让最危险的那几条在任何分位数口径下都排在最前面。
205
+ const injectionFloor =
206
+ Number.isFinite(options.injectionFloorThreshold)
207
+ ? options.injectionFloorThreshold
208
+ : INJECTION_FLOOR_THRESHOLD;
209
+ if (injection >= injectionFloor) severity = Math.max(severity, INJECTION_FLOOR_SCORE);
210
+ if (risk.choice === "high") severity = Math.max(severity, HIGH_RISK_FLOOR_SCORE);
211
+ return clamp01(severity);
212
+ }
213
+
214
+ /**
215
+ * 线性插值分位数(与 numpy 默认口径一致)。
216
+ *
217
+ * 为什么要插值而不是直接取第 k 个:P99 在 200 个样本上落在倒数第 2 与
218
+ * 倒数第 1 之间,取整会让阈值在两次调用之间跳一整步,表现为「突然多问一次
219
+ * 或突然不问」。插值把它变成连续阈值。
220
+ *
221
+ * @param sorted - **已排序**的数值数组。
222
+ * @param q - 分位数(0–1)。
223
+ * @returns 分位数值;数组为空时返回 null。
224
+ */
225
+ export function quantileOf(sorted, q) {
226
+ if (!Array.isArray(sorted) || sorted.length === 0) return null;
227
+ const position = (sorted.length - 1) * Math.min(1, Math.max(0, q));
228
+ const low = Math.floor(position);
229
+ const high = Math.ceil(position);
230
+ if (low === high) return sorted[low];
231
+ return sorted[low] + (sorted[high] - sorted[low]) * (position - low);
232
+ }
233
+
234
+ /**
235
+ * 滚动校准器:按 provider × model 记住最近若干次严重度。
236
+ *
237
+ * 为什么分桶:两个决策模型的分数分布没有可比性。混在一个窗口里算分位数,
238
+ * 得到的阈值对两个模型都不对(对高分模型偏松,对低分模型偏紧)。
239
+ *
240
+ * 纯内存:不落盘。理由是窗口的语义是「**最近**怎么说的」,落盘会把几天前
241
+ * 的分布混进来,而模型与调用场景都可能已经变了。
242
+ */
243
+ export class ToleranceCalibrator {
244
+ /**
245
+ * @param options.windowSize - 每个分桶保留多少个样本。
246
+ * @param options.minSamples - 校准就绪所需的最小样本数。
247
+ */
248
+ constructor(options = {}) {
249
+ this.windowSize = Number.isFinite(options.windowSize)
250
+ ? Math.max(10, Math.floor(options.windowSize))
251
+ : TOLERANCE_WINDOW_SIZE;
252
+ this.minSamples = Number.isFinite(options.minSamples)
253
+ ? Math.max(5, Math.floor(options.minSamples))
254
+ : TOLERANCE_MIN_SAMPLES;
255
+ /** `@type {Map<string, number[]>}` 分桶:键 = `provider::model`。 */
256
+ this.buckets = new Map();
257
+ /** 最近一次观察到的分桶键(状态回显用)。 */
258
+ this.lastKey = "";
259
+ /**
260
+ * 分桶 → 有序副本的缓存,供 `threshold()` 复用(见 `sortedOf`)。
261
+ * 键是分桶键,值是 `{ source, length, sorted }`;`observe()` 会删掉对应项。
262
+ */
263
+ this.sortedCache = new Map();
264
+ }
265
+
266
+ /** 分桶键。 */
267
+ static keyOf(provider, model) {
268
+ return `${String(provider ?? "unknown")}::${String(model ?? "unknown")}`;
269
+ }
270
+
271
+ /**
272
+ * 记一次严重度。
273
+ *
274
+ * @param provider - 服务商 id。
275
+ * @param model - 模型 id。
276
+ * @param severity - 严重度(0–1)。
277
+ */
278
+ observe(provider, model, severity) {
279
+ const value = clamp01(severity);
280
+ const key = ToleranceCalibrator.keyOf(provider, model);
281
+ let bucket = this.buckets.get(key);
282
+ if (bucket === undefined) {
283
+ bucket = [];
284
+ this.buckets.set(key, bucket);
285
+ }
286
+ bucket.push(value);
287
+ // 窗口是「最近」的:超量就从最旧的开始丢。
288
+ while (bucket.length > this.windowSize) bucket.shift();
289
+ // 窗口变了,有序副本作废(见 `sortedOf`)。
290
+ this.sortedCache.delete(key);
291
+ this.lastKey = key;
292
+ }
293
+
294
+ /**
295
+ * 某分桶的校准状态。
296
+ *
297
+ * @param provider - 服务商 id。
298
+ * @param model - 模型 id。
299
+ * @returns `{ samples, ready }`;分桶不存在时 samples = 0。
300
+ */
301
+ stats(provider, model) {
302
+ const key = ToleranceCalibrator.keyOf(provider, model);
303
+ const bucket = this.buckets.get(key);
304
+ const samples = bucket === undefined ? 0 : bucket.length;
305
+ return { samples, ready: samples >= this.minSamples };
306
+ }
307
+
308
+ /**
309
+ * 某分桶在某分位数上的阈值。
310
+ *
311
+ * @param provider - 服务商 id。
312
+ * @param model - 模型 id。
313
+ * @param q - 分位数(0–1)。
314
+ * @returns 阈值;样本不足时返回 null(调用方应改用 `fallbackSeverity`)。
315
+ */
316
+ threshold(provider, model, q) {
317
+ const key = ToleranceCalibrator.keyOf(provider, model);
318
+ const bucket = this.buckets.get(key);
319
+ if (bucket === undefined || bucket.length < this.minSamples) return null;
320
+ return quantileOf(this.sortedOf(key, bucket), q);
321
+ }
322
+
323
+ /**
324
+ * 取某分桶的**有序**副本。
325
+ *
326
+ * 为什么需要缓存:`threshold()` 在**每一次受守卫的决策**上都会被问一次
327
+ * (算「这一档要问多频繁」),而它过去每次都要复制整个窗口再排序——
328
+ * 窗口 200 条就是每次决策一次 200 元素排序。窗口只在 `observe()` 时变化,
329
+ * 所以把有序副本连同「当时的长度」一起记住,`observe()` 一来就作废。
330
+ *
331
+ * 为什么用「长度 + 数组身份」当失效依据而不是脏标记:`buckets` 是公开字段
332
+ * (测试与界面会直接读),外部可能整体替换或 push。用长度比对能覆盖 push
333
+ * 与出窗,用数组身份比对能覆盖替换;两者都不依赖调用方守规矩。
334
+ *
335
+ * @param key - 分桶键。
336
+ * @param bucket - 当前分桶数组。
337
+ * @returns 升序数组(内部缓存,调用方只读)。
338
+ */
339
+ sortedOf(key, bucket) {
340
+ const cached = this.sortedCache.get(key);
341
+ if (cached !== undefined && cached.source === bucket && cached.length === bucket.length) {
342
+ return cached.sorted;
343
+ }
344
+ const sorted = [...bucket].sort((a, b) => a - b);
345
+ this.sortedCache.set(key, { source: bucket, length: bucket.length, sorted });
346
+ return sorted;
347
+ }
348
+
349
+ /** 清空(配置变更或测试用)。 */
350
+ reset() {
351
+ this.buckets.clear();
352
+ this.sortedCache.clear();
353
+ this.lastKey = "";
354
+ }
355
+ }
356
+
357
+ /**
358
+ * 归一化一档配置。
359
+ *
360
+ * @param level - 档位 id。
361
+ * @returns 预设对象;未知档位退回 `default`。
362
+ */
363
+ export function tolerancePreset(level) {
364
+ const id = TOLERANCE_LEVELS.includes(level) ? level : DEFAULT_TOLERANCE;
365
+ return TOLERANCE_PRESETS[id];
366
+ }
367
+
368
+ /**
369
+ * 按档位决定「这次要不要问用户」。
370
+ *
371
+ * 这个函数是**纯函数**:`cutoff` 由调用方算好传进来(校准器是有状态的,
372
+ * 而裁决必须可测)。
373
+ *
374
+ * @param options.level - 档位 id。
375
+ * @param options.legacyKind - 现有阈值算出的裁决(`delegate` / `ask` / `deny`)。
376
+ * @param options.severity - 本次的严重度(0–1)。
377
+ * @param options.cutoff - 该档的严重度阈值;null = 还没校准好。
378
+ * @returns `{ kind, changed, severity, cutoff, note }`:
379
+ * - `kind`:最终裁决(`delegate` / `ask` / `deny`);
380
+ * - `changed`:是否与 `legacyKind` 不同(用于审计与界面提示);
381
+ * - `note`:改动原因(`changed` 为 true 时才有意义)。
382
+ */
383
+ export function resolveTolerance(options = {}) {
384
+ const preset = tolerancePreset(options.level);
385
+ const severity = clamp01(options.severity);
386
+ const legacyKind = options.legacyKind === "ask" || options.legacyKind === "deny"
387
+ ? options.legacyKind
388
+ : "delegate";
389
+
390
+ // 拦截(deny)不是「打扰用户」,四档一律保留。
391
+ //
392
+ // 这条边界必须守住:自治档的语义是「不问我」,不是「什么都放行」。
393
+ // deny 不会打断用户(它直接告诉模型「这次调用被拒了」),而把它一并
394
+ // 取消等于让自治档变成「关掉安全检查」——那与用户说的「0% 询问」是两件事。
395
+ if (legacyKind === "deny") {
396
+ return { kind: "deny", changed: false, severity, cutoff: null, note: "" };
397
+ }
398
+
399
+ const mandatoryStatus = typeof options.mandatory === "string" ? options.mandatory : options.mandatory?.status;
400
+ if (mandatoryStatus === "deny") {
401
+ return { kind: "deny", changed: legacyKind !== "deny", severity, cutoff: null, note: "mandatory=deny" };
402
+ }
403
+ if (mandatoryStatus === "requires_approval") {
404
+ const approvalAvailable = options.approvalAvailable ?? options.mandatory?.approvalAvailable;
405
+ const kind = approvalAvailable === true ? "ask" : "deny";
406
+ return { kind, changed: kind !== legacyKind, severity, cutoff: null, note: approvalAvailable === true ? "mandatory=requires_approval" : "mandatory=requires_approval; approval-unavailable" };
407
+ }
408
+
409
+ if (preset.id === "autonomous") {
410
+ return legacyKind === "ask"
411
+ ? {
412
+ kind: "delegate",
413
+ changed: true,
414
+ severity,
415
+ cutoff: null,
416
+ note: `tolerance=autonomous(自治:不询问用户,severity=${severity.toFixed(3)})`,
417
+ }
418
+ : { kind: "delegate", changed: false, severity, cutoff: null, note: "" };
419
+ }
420
+
421
+ // 默认档:完全走现有阈值,逐字不变。
422
+ if (preset.id === "default") {
423
+ return { kind: legacyKind, changed: false, severity, cutoff: null, note: "" };
424
+ }
425
+
426
+ // 轻松 / 谨慎:按分位数带。
427
+ const cutoff = Number.isFinite(options.cutoff)
428
+ ? clamp01(options.cutoff)
429
+ : preset.fallbackSeverity;
430
+
431
+ if (preset.id === "relaxed") {
432
+ // 轻松档**替换**默认判断,而不是叠加在它上面——否则「top 1%」会变成
433
+ // 「top 1% ∪ 默认判断」,实测那是 23% 的打扰率,与档位语义不符。
434
+ if (severity >= cutoff) {
435
+ return {
436
+ kind: "ask",
437
+ changed: legacyKind !== "ask",
438
+ severity,
439
+ cutoff,
440
+ note: `tolerance=relaxed(严重度 ${severity.toFixed(3)} ≥ ${cutoff.toFixed(3)},属最前的 1%)`,
441
+ };
442
+ }
443
+ return legacyKind === "ask"
444
+ ? {
445
+ kind: "delegate",
446
+ changed: true,
447
+ severity,
448
+ cutoff,
449
+ note: `tolerance=relaxed(严重度 ${severity.toFixed(3)} < ${cutoff.toFixed(3)},不在最前的 1%)`,
450
+ }
451
+ : { kind: "delegate", changed: false, severity, cutoff, note: "" };
452
+ }
453
+
454
+ // 谨慎档:默认判断 ∪ 严重度靠前的 25%(**叠加**,因此一定 ≥ 默认)。
455
+ if (legacyKind === "ask" || severity >= cutoff) {
456
+ return {
457
+ kind: "ask",
458
+ changed: legacyKind !== "ask",
459
+ severity,
460
+ cutoff,
461
+ note:
462
+ legacyKind === "ask"
463
+ ? `tolerance=cautious(默认判断已升级询问)`
464
+ : `tolerance=cautious(严重度 ${severity.toFixed(3)} ≥ ${cutoff.toFixed(3)},属最前的 25%)`,
465
+ };
466
+ }
467
+ return { kind: "delegate", changed: false, severity, cutoff, note: "" };
468
+ }
469
+
470
+ /**
471
+ * 算一档该用的严重度阈值。
472
+ *
473
+ * @param options.level - 档位 id。
474
+ * @param options.calibrator - 校准器(可为 undefined)。
475
+ * @param options.provider - 服务商 id。
476
+ * @param options.model - 模型 id。
477
+ * @returns `{ cutoff, ready, samples }`:`cutoff` 为 null 表示「该档不按分位数走」
478
+ * 或「还没校准好」(调用方据此改用 `fallbackSeverity`)。
479
+ */
480
+ export function toleranceCutoff(options = {}) {
481
+ const preset = tolerancePreset(options.level);
482
+ if (preset.percentile === null) {
483
+ return { cutoff: null, ready: true, samples: 0 };
484
+ }
485
+ const stats = options.calibrator?.stats(options.provider, options.model) ?? {
486
+ samples: 0,
487
+ ready: false,
488
+ };
489
+ if (!stats.ready) {
490
+ return { cutoff: null, ready: false, samples: stats.samples };
491
+ }
492
+ const threshold = options.calibrator.threshold(options.provider, options.model, preset.percentile);
493
+ return {
494
+ cutoff: Number.isFinite(threshold) ? threshold : null,
495
+ ready: true,
496
+ samples: stats.samples,
497
+ };
498
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * microcharts@0.19.1 的统计页样式(**构建期生成**,勿手改)。
3
+ *
4
+ * 上半部分是 @microcharts/react 的 core.css + sparkline.css,未做删改——
5
+ * 图型的可见性依赖 `[data-mc-ink]` 的 fill/stroke 规则,删一条就可能整张图空白。
6
+ *
7
+ * 下半部分是「主题覆盖」:把库的硬编码调色板接到 DSH 的语义变量上。
8
+ */
9
+
10
+ @layer microcharts.tokens,microcharts.base,microcharts.charts,microcharts.motion;@property --mc-seat{syntax: "<number>"; inherits: false; initial-value: 0;}@property --mc-seat-mid{syntax: "<number>"; inherits: false; initial-value: 0;}@layer microcharts.tokens{:where(:root){--mc-stroke: #1a1917;--mc-positive: #0e7a5f;--mc-negative: #bd4b2d;--mc-neutral: #8a8986;--mc-accent: #1f6091;--mc-band: color-mix(in oklab, var(--mc-stroke) 8%, transparent);--mc-moon: #c1922f;--mc-cat-1: #d2982f;--mc-cat-2: #5b9fd4;--mc-cat-3: #2e8c66;--mc-cat-4: #285788;--mc-cat-5: #bc5138;--mc-cat-6: #a55a89;--mc-on-cat-1: rgba(0, 0, 0, .9);--mc-on-cat-2: rgba(0, 0, 0, .9);--mc-on-cat-3: rgba(0, 0, 0, .9);--mc-on-cat-4: rgba(255, 255, 255, .96);--mc-on-cat-5: rgba(255, 255, 255, .96);--mc-on-cat-6: rgba(255, 255, 255, .96);--mc-stroke-width: 1.5;--mc-gap: .25em;--mc-density: 1;--mc-font: inherit;--mc-font-numeric: var(--mc-font);--mc-label-size: .75em;--mc-label-weight: 400;--mc-inline-nudge: 0em;--mc-glyph-nudge: 0em;--mc-surface: Canvas;--mc-surface-ink: CanvasText;--mc-surface-edge: color-mix(in oklab, CanvasText 16%, transparent);--mc-surface-shadow: color-mix(in oklab, #000 28%, transparent);--mc-on-fill: rgba(255, 255, 255, .96);--mc-on-cat: rgba(0, 0, 0, .9);--mc-on-fill-dim: var(--mc-stroke);--mc-duration: .3s;--mc-easing: cubic-bezier(.22, 1, .36, 1)}@media(prefers-color-scheme:dark){:where(:root){--mc-stroke: #eae9e6;--mc-positive: #45a385;--mc-negative: #df7856;--mc-accent: #5ea1cf;--mc-neutral: #9a9a97;--mc-moon: #e0be6f;--mc-cat-1: #e2b45c;--mc-cat-2: #6fb0e0;--mc-cat-3: #4fb08d;--mc-cat-4: #6e9bd1;--mc-cat-5: #e07e5e;--mc-cat-6: #c486b0;--mc-on-cat-1: rgba(0, 0, 0, .9);--mc-on-cat-2: rgba(0, 0, 0, .9);--mc-on-cat-3: rgba(0, 0, 0, .9);--mc-on-cat-4: rgba(0, 0, 0, .9);--mc-on-cat-5: rgba(0, 0, 0, .9);--mc-on-cat-6: rgba(0, 0, 0, .9);--mc-on-fill: rgba(0, 0, 0, .9)}}:where([data-mc-theme=light]){--mc-stroke: #1a1917;--mc-positive: #0e7a5f;--mc-negative: #bd4b2d;--mc-accent: #1f6091;--mc-neutral: #8a8986;--mc-moon: #c1922f;--mc-cat-1: #d2982f;--mc-cat-2: #5b9fd4;--mc-cat-3: #2e8c66;--mc-cat-4: #285788;--mc-cat-5: #bc5138;--mc-cat-6: #a55a89;--mc-on-cat-1: rgba(0, 0, 0, .9);--mc-on-cat-2: rgba(0, 0, 0, .9);--mc-on-cat-3: rgba(0, 0, 0, .9);--mc-on-cat-4: rgba(255, 255, 255, .96);--mc-on-cat-5: rgba(255, 255, 255, .96);--mc-on-cat-6: rgba(255, 255, 255, .96);--mc-on-fill: rgba(255, 255, 255, .96)}:where([data-mc-theme=dark]){--mc-stroke: #eae9e6;--mc-positive: #45a385;--mc-negative: #df7856;--mc-accent: #5ea1cf;--mc-neutral: #9a9a97;--mc-moon: #e0be6f;--mc-cat-1: #e2b45c;--mc-cat-2: #6fb0e0;--mc-cat-3: #4fb08d;--mc-cat-4: #6e9bd1;--mc-cat-5: #e07e5e;--mc-cat-6: #c486b0;--mc-on-cat-1: rgba(0, 0, 0, .9);--mc-on-cat-2: rgba(0, 0, 0, .9);--mc-on-cat-3: rgba(0, 0, 0, .9);--mc-on-cat-4: rgba(0, 0, 0, .9);--mc-on-cat-5: rgba(0, 0, 0, .9);--mc-on-cat-6: rgba(0, 0, 0, .9);--mc-on-fill: rgba(0, 0, 0, .9)}:where([data-mc-theme=editorial],[data-mc-preset=editorial]){--mc-accent: #a32236;--mc-stroke-width: 1}:where([data-mc-theme=mono],[data-mc-preset=mono]){--mc-positive: var(--mc-stroke);--mc-negative: var(--mc-stroke);--mc-accent: var(--mc-stroke);--mc-neutral: var(--mc-stroke);--mc-moon: var(--mc-stroke)}:where([data-mc-theme=vivid],[data-mc-preset=vivid]){--mc-positive: #0f9e78;--mc-negative: #e24d2e;--mc-stroke-width: 2}:where([data-mc-theme=print],[data-mc-preset=print]){--mc-stroke: #1a1a1a;--mc-neutral: #666666;--mc-positive: #0c6249;--mc-negative: #a33f22;--mc-accent: #14507a;--mc-moon: #7a5a12;--mc-band: color-mix(in oklab, #1a1a1a 9%, transparent);--mc-stroke-width: 1.25}:where([data-mc-theme=eink],[data-mc-preset=eink]){--mc-stroke: #000000;--mc-positive: #000000;--mc-negative: #595959;--mc-neutral: #8c8c8c;--mc-accent: #000000;--mc-moon: #000000;--mc-band: color-mix(in oklab, #000000 14%, transparent);--mc-stroke-width: 2}:where([data-mc-theme=mono],[data-mc-preset=mono],[data-mc-theme=eink],[data-mc-preset=eink]){--mc-cat-1: color-mix(in oklab, var(--mc-stroke) 16%, transparent);--mc-cat-2: color-mix(in oklab, var(--mc-stroke) 13%, transparent);--mc-cat-3: color-mix(in oklab, var(--mc-stroke) 11%, transparent);--mc-cat-4: color-mix(in oklab, var(--mc-stroke) 9%, transparent);--mc-cat-5: color-mix(in oklab, var(--mc-stroke) 7%, transparent);--mc-cat-6: color-mix(in oklab, var(--mc-stroke) 5%, transparent);--mc-on-cat: var(--mc-stroke)}@media(prefers-color-scheme:dark){:where([data-mc-theme=print],[data-mc-preset=print]){--mc-stroke: #e8e6e1;--mc-neutral: #9a958c;--mc-positive: #4aa588;--mc-negative: #e08a63;--mc-accent: #6aa9d3;--mc-moon: #e0be6f;--mc-band: color-mix(in oklab, #e8e6e1 9%, transparent)}:where([data-mc-theme=eink],[data-mc-preset=eink]){--mc-stroke: #ffffff;--mc-positive: #ffffff;--mc-negative: #b0b0b0;--mc-neutral: #808080;--mc-accent: #ffffff;--mc-moon: #ffffff;--mc-band: color-mix(in oklab, #ffffff 14%, transparent)}}:where([data-mc-theme=dark]) :where([data-mc-theme=print],[data-mc-preset=print]),:where([data-mc-theme=dark][data-mc-preset=print]){--mc-stroke: #e8e6e1;--mc-neutral: #9a958c;--mc-positive: #4aa588;--mc-negative: #e08a63;--mc-accent: #6aa9d3;--mc-moon: #e0be6f;--mc-band: color-mix(in oklab, #e8e6e1 9%, transparent)}:where([data-mc-theme=dark]) :where([data-mc-theme=eink],[data-mc-preset=eink]),:where([data-mc-theme=dark][data-mc-preset=eink]){--mc-stroke: #ffffff;--mc-positive: #ffffff;--mc-negative: #b0b0b0;--mc-neutral: #808080;--mc-accent: #ffffff;--mc-moon: #ffffff;--mc-band: color-mix(in oklab, #ffffff 14%, transparent)}}@layer microcharts.base{:where(.mc-root){display:inline-block;vertical-align:middle;overflow:visible;font:var(--mc-font);height:auto;max-width:100%;--mc-sw: calc(var(--mc-stroke-width) * var(--mc-density))}:where(td:has(>.mc-root:only-child)),:where(td:has(>[data-mc-host]:only-child:not(.mc-delta-live,.mc-token-confidence))),:where(td:has(>[class$=-live]:only-child:not(.mc-delta-live,.mc-tc-live))),:where(td:has(>[class$=-interactive]:only-child)){line-height:0}:where(.mc-root text){font-variant-numeric:tabular-nums;font-family:var(--mc-font-numeric);font-weight:var(--mc-label-weight);font-size:calc(var(--mc-label-px, var(--mc-label-size)) * min(1.25,var(--mc-density)));fill:var(--mc-stroke)}:where(.mc-root text[data-mc-on-fill]){fill:var(--mc-on-fill)}:where(.mc-group){display:inline-flex;flex-wrap:wrap;gap:calc(var(--mc-gap) * var(--mc-density));align-items:center}:where(.mc-inline){display:inline-flex;align-items:center;vertical-align:baseline;margin-inline:.2em;translate:0 var(--mc-inline-nudge)}:where(.mc-inline .mc-root){translate:0 calc(var(--mc-seat, 0) * 100% + var(--mc-seat-mid, 0) * (var(--mc-glyph-nudge) - .36em))}@supports (height: 1cap){:where(.mc-inline .mc-root){translate:0 calc(var(--mc-seat, 0) * 100% + var(--mc-seat-mid, 0) * (var(--mc-glyph-nudge) - .5cap))}}:where(.mc-inline [data-mc-seated]){translate:0 calc(var(--mc-seat, 0) * 100% + var(--mc-seat-mid, 0) * (var(--mc-glyph-nudge) - .36em))}:where(.mc-inline [data-mc-seated]>.mc-root){translate:none}@supports (height: 1cap){:where(.mc-inline [data-mc-seated]){translate:0 calc(var(--mc-seat, 0) * 100% + var(--mc-seat-mid, 0) * (var(--mc-glyph-nudge) - .5cap))}}:where(.mc-inline .mc-status){align-self:center}:where(.mc-inline .mc-trend){width:.95em;height:.95em}:where([data-mc-host]:focus-visible,.mc-root:focus-visible){outline:2px solid var(--mc-accent);outline-offset:2px;border-radius:.2em}:where(.mc-delta){display:inline-flex;align-items:baseline;gap:var(--mc-gap);font:var(--mc-font);font-family:var(--mc-font-numeric);font-variant-numeric:tabular-nums;color:var(--mc-neutral);vertical-align:baseline;line-height:1}:where(.mc-delta[data-mc-valence=pos]){color:var(--mc-positive)}:where(.mc-delta[data-mc-valence=neg]){color:var(--mc-negative)}:where(.mc-delta .mc-delta-glyph){fill:currentColor;flex:none;align-self:center}:where(.mc-delta .mc-delta-num){font-weight:600}:where(.mc-delta-live){position:relative;display:inline-flex;vertical-align:baseline}:where(.mc-trend-live){position:relative;display:inline-flex;vertical-align:middle}:where(.mc-status-live){position:relative;display:inline-flex;vertical-align:middle}:where([data-mc-host] .mc-root :is([data-mc-ink],[data-mc-cat],[data-mc-cone]):not([data-mc-ui],[data-mc-ui] *)){transition-property:x,y,width,height,cx,cy,r;transition-duration:.2s;transition-timing-function:var(--mc-easing)}}@layer microcharts.charts{:where(.mc-root [data-mc-ink=data]){fill:none;stroke:var(--mc-stroke);stroke-width:var(--mc-sw);stroke-linejoin:round;stroke-linecap:round}:where(.mc-root [data-mc-ink=accent]){fill:var(--mc-accent);stroke:none}:where(.mc-root :is(path,line,polyline)[data-mc-ink=accent]){fill:none;stroke:var(--mc-accent)}:where(.mc-root [data-mc-w=full]){stroke-width:var(--mc-sw)}:where(.mc-root [data-mc-w=support]){stroke-width:calc(var(--mc-sw) * 2 / 3)}:where(.mc-root [data-mc-w=tick]){stroke-width:calc(var(--mc-sw) / 2)}:where(.mc-root [data-mc-w=hair]){stroke-width:calc(var(--mc-sw) / 3)}:where(.mc-root [data-mc-w=heavy]){stroke-width:calc(var(--mc-sw) * 4 / 3)}:where(.mc-root [data-mc-w=anchor]){stroke-width:calc(var(--mc-sw) * 3 / 2)}:where(.mc-root [data-mc-w],.mc-root [data-mc-ink=data],.mc-root [data-mc-ink=muted],.mc-root [data-mc-ink=unit-off],.mc-root [data-mc-ink=gap],.mc-root [data-mc-ink=accent],.mc-root [data-mc-ink=positive],.mc-root [data-mc-ink=negative],.mc-root [data-mc-ink=ghost],.mc-root [data-mc-ink=flag]){vector-effect:non-scaling-stroke}:where(.mc-donut-wedge,.mc-ring path[data-mc-ink=accent],.mc-ring-halo){vector-effect:none}:where(.mc-root [data-mc-active]){stroke:var(--mc-active-stroke, var(--mc-accent))}:where(.mc-root :is(rect,circle,ellipse,polygon)[data-mc-active]){fill:var(--mc-active-fill, var(--mc-on-fill));fill-opacity:var(--mc-active-fill-opacity, .2)}:where(.mc-root:has([data-mc-active]) :is([data-mc-ink],[data-mc-cat]):not([data-mc-active],[data-mc-ui],[data-mc-ui] *,[opacity])){opacity:var(--mc-rest-opacity, 1)}:where(.mc-root [data-mc-ink=muted]){fill:none;stroke:var(--mc-neutral)}:where(.mc-root text[data-mc-ink=label]){fill:var(--mc-neutral);stroke:none}:where(.mc-root [data-mc-ink=point]){fill:var(--mc-stroke);stroke:none}:where(.mc-root [data-mc-ink=bar]){fill:var(--mc-stroke);stroke:none}:where(.mc-root [data-mc-ink=positive]){fill:var(--mc-positive);stroke:none}:where(.mc-root [data-mc-ink=negative]){fill:var(--mc-negative);stroke:none}:where(.mc-root :is(line,polyline)[data-mc-ink=positive]){fill:none;stroke:var(--mc-positive)}:where(.mc-root :is(line,polyline)[data-mc-ink=negative]){fill:none;stroke:var(--mc-negative)}:where(.mc-root path[data-mc-ink=positive][fill=none]){stroke:var(--mc-positive)}:where(.mc-root path[data-mc-ink=negative][fill=none]){stroke:var(--mc-negative)}:where(.mc-root [data-mc-ink=neutral]){fill:var(--mc-neutral);stroke:none}:where(.mc-root [data-mc-ink=band]){fill:var(--mc-band);stroke:none}:where(.mc-root [data-mc-ink=fill]){fill:var(--mc-stroke);fill-opacity:.12;stroke:none}:where(.mc-root [data-mc-ink=cell]){fill:var(--mc-accent);stroke:none}:where(.mc-root [data-mc-cell-mix]){fill:color-mix(in oklab,var(--mc-cell-color, var(--mc-accent)) calc(var(--mc-cell-mix, 100) * 1%),var(--mc-band))}:where(.mc-root [data-mc-cone]){fill:var(--mc-cone-color, var(--mc-accent));fill-opacity:var(--mc-cone-opacity, .13);stroke:none}:where(.mc-root [data-mc-ink=unit-off]){fill:var(--mc-band);stroke:var(--mc-neutral);stroke-opacity:.4;stroke-width:calc(var(--mc-sw) / 3)}:where(.mc-root [data-mc-ink=gap]){fill:var(--mc-band);stroke:var(--mc-neutral);stroke-opacity:.35;stroke-width:calc(var(--mc-sw) / 3)}:where(.mc-root :is(rect,circle,ellipse,polygon)[data-mc-ink=ghost]){fill:var(--mc-neutral);fill-opacity:.18;stroke:none}:where(.mc-root :is(path,line,polyline)[data-mc-ink=ghost]){fill:none;stroke:var(--mc-neutral);stroke-opacity:.5}:where(.mc-root [data-mc-ink=region]){fill:var(--mc-accent);fill-opacity:.08;stroke:none}:where(.mc-root line[data-mc-ink=flag]){stroke:var(--mc-accent)}:where(.mc-root circle[data-mc-ink=flag]){fill:var(--mc-accent)}:where(.mc-root [data-mc-cat="1"]){fill:var(--mc-cat-1);stroke:none}:where(.mc-root [data-mc-cat="2"]){fill:var(--mc-cat-2);stroke:none}:where(.mc-root [data-mc-cat="3"]){fill:var(--mc-cat-3);stroke:none}:where(.mc-root [data-mc-cat="4"]){fill:var(--mc-cat-4);stroke:none}:where(.mc-root [data-mc-cat="5"]){fill:var(--mc-cat-5);stroke:none}:where(.mc-root [data-mc-cat="6"]){fill:var(--mc-cat-6);stroke:none}:where(.mc-trace,.mc-partition) :where(rect){shape-rendering:crispEdges;rx:.5}:where(.mc-root rect:not([rx])){shape-rendering:crispEdges}:where(.mc-root rect[width^="0."],.mc-root rect[height^="0."]){shape-rendering:auto}:where(.mc-trace,.mc-partition) :where(text){font-weight:600;fill:var(--mc-on-fill)}:where(.mc-partition,.mc-segbar) :where(text){fill:var(--mc-on-cat)}:where(.mc-partition,.mc-segbar) :where(rect[data-mc-cat="1"]+text){fill:var(--mc-on-cat-1, var(--mc-on-cat))}:where(.mc-partition,.mc-segbar) :where(rect[data-mc-cat="2"]+text){fill:var(--mc-on-cat-2, var(--mc-on-cat))}:where(.mc-partition,.mc-segbar) :where(rect[data-mc-cat="3"]+text){fill:var(--mc-on-cat-3, var(--mc-on-cat))}:where(.mc-partition,.mc-segbar) :where(rect[data-mc-cat="4"]+text){fill:var(--mc-on-cat-4, var(--mc-on-cat))}:where(.mc-partition,.mc-segbar) :where(rect[data-mc-cat="5"]+text){fill:var(--mc-on-cat-5, var(--mc-on-cat))}:where(.mc-partition,.mc-segbar) :where(rect[data-mc-cat="6"]+text){fill:var(--mc-on-cat-6, var(--mc-on-cat))}:where(.mc-partition,.mc-segbar) :where(rect[data-mc-ink=accent]+text){fill:var(--mc-on-fill)}:where(.mc-segbar) :where(rect[data-mc-ink=neutral]+text){fill:var(--mc-on-cat)}:where(.mc-trace) :where(text[data-mc-dim]){fill:var(--mc-on-fill-dim);font-weight:var(--mc-label-weight)}:where([data-mc-host]){touch-action:pan-y pinch-zoom;width:fit-content;max-width:100%}:where(.mc-minimap-live){cursor:ew-resize;touch-action:none}:where(.mc-spark-readout){border:0;display:block;position:absolute;inset:auto auto 100% 50%;transform:translate(-50%);margin:0 0 .3em;padding:.05em .4em;border-radius:.4em;background:var(--mc-surface);color:var(--mc-surface-ink);box-shadow:0 0 0 1px var(--mc-surface-edge),0 2px 6px -2px var(--mc-surface-shadow);font:var(--mc-label-size, .75em) var(--mc-font, inherit);font-variant-numeric:tabular-nums;line-height:1.35;letter-spacing:.01em;white-space:nowrap;pointer-events:none;box-sizing:border-box;max-width:max(100%,14em);overflow:hidden;text-overflow:ellipsis}@supports (anchor-name: --mc) and (position-area: block-start){:where([data-mc-host]){anchor-name:--mc-chart;anchor-scope:--mc-chart}:where(.mc-spark-readout){position:fixed;position-anchor:--mc-chart;transform:none;inset:auto 2px anchor(top) 2px;justify-self:anchor-center;max-inline-size:max(anchor-size(width),14em);margin:0 0 .3em;position-try-fallbacks:--mc-readout-below;position-visibility:anchors-visible}@position-try --mc-readout-below{inset:anchor(bottom) 2px auto 2px;margin:.3em 0 0}}:where(.mc-readout-rows){display:grid;grid-template-columns:auto auto auto;inline-size:max-content;align-items:center;gap:0 .4em;text-align:start}:where(.mc-readout-rows>.mc-readout-key){inline-size:.5em;block-size:.5em;border-radius:.15em;forced-color-adjust:none}:where(.mc-readout-rows>.mc-readout-name){max-inline-size:7em;overflow:hidden;text-overflow:ellipsis}:where(.mc-readout-rows>.mc-readout-val){justify-self:end}}@layer microcharts.motion{:where(.mc-spark-readout){translate:0 0;scale:1;transition:opacity .14s var(--mc-easing),translate .14s var(--mc-easing),scale .14s var(--mc-easing);@starting-style{opacity:0;translate:0 2px;scale:.97}}:where(.mc-root [data-mc-ui] :is(circle,line,path,rect)){vector-effect:non-scaling-stroke}:where(.mc-root [data-mc-ui],.mc-root [data-mc-ui] :is(g,rect,circle,line)){transition-property:transform;transition-duration:.12s;transition-timing-function:var(--mc-easing)}@keyframes mc-pulse{0%{transform:scale(1)}30%{transform:scale(1.16)}to{transform:scale(1)}}:where(.mc-delta-live[data-pulse="1"] .mc-delta-num){display:inline-block;animation:mc-pulse var(--mc-duration) var(--mc-easing)}@keyframes mc-delta-num-in{0%{opacity:0;transform:translateY(2px) scale(.97)}to{opacity:1;transform:none}}:where(.mc-delta-live[data-enter="1"] .mc-delta-num){display:inline-block;animation:mc-delta-num-in .2s var(--mc-easing) backwards}:where(.mc-trend-live[data-pulse="1"] .mc-root){transform-origin:center;animation:mc-pulse var(--mc-duration) var(--mc-easing)}:where(.mc-hourglass-sand){transform-box:fill-box;transform-origin:center bottom}@keyframes mc-status-pulse{0%{transform:scale(1);opacity:.5}to{transform:scale(1.333);opacity:0}}:where(.mc-status .mc-status-halo){transform-box:fill-box;transform-origin:center;animation:mc-status-pulse 1.8s var(--mc-easing) infinite}@media(prefers-reduced-motion:reduce){:where(.mc-status .mc-status-halo){display:none}}@keyframes mc-burst{0%{transform:var(--mc-burst-from);opacity:0}to{transform:none;opacity:1}}:where(.mc-root .mc-celebrate){transform-box:fill-box;transform-origin:center;animation:mc-burst .6s var(--mc-easing) backwards}@media(prefers-reduced-motion:reduce){:where(.mc-root *),:where(.mc-root),:where(.mc-delta-live .mc-delta-num),:where(.mc-spark-readout){animation:none!important;transition:none!important}}@media(prefers-contrast:more){:where(.mc-root){--mc-stroke-width: 2;--mc-band: color-mix(in oklab, var(--mc-stroke) 16%, transparent)}}@media(forced-colors:active){:where(.mc-root){forced-color-adjust:none}:where(.mc-root){--mc-accent: Highlight}:where(.mc-root [data-mc-ink=data]){stroke:CanvasText}:where(.mc-root [data-mc-ink=accent]){stroke:Highlight;fill:Highlight}:where(.mc-root :is(path,line,polyline)[data-mc-ink=accent]){fill:none;stroke:Highlight}:where(.mc-root [data-mc-active]){stroke:Highlight}:where(.mc-root){--mc-rest-opacity: 1}:where(.mc-root [data-mc-ink=muted]){stroke:GrayText;fill:GrayText}:where(.mc-root [data-mc-ink=point]),:where(.mc-root [data-mc-ink=bar]){fill:CanvasText}:where(.mc-root [data-mc-ink=positive]){fill:CanvasText}:where(.mc-root [data-mc-ink=negative]){fill:GrayText}:where(.mc-root [data-mc-ink=neutral]){fill:GrayText}:where(.mc-root [data-mc-ink=band]){fill:GrayText}:where(.mc-root [data-mc-ink=fill]){fill:CanvasText;fill-opacity:.2}:where(.mc-root [data-mc-ink=cell]){forced-color-adjust:none;fill:Highlight}:where(.mc-root [data-mc-cone]){fill:Highlight;fill-opacity:calc(var(--mc-cone-opacity, .13) * 2.5)}:where(.mc-root [data-mc-cell-mix]){forced-color-adjust:none;fill:Highlight;fill-opacity:calc(var(--mc-cell-mix, 100) / 100)}:where(.mc-root [data-mc-ink=unit-off]),:where(.mc-root [data-mc-ink=gap]){fill:Canvas;stroke:GrayText;stroke-opacity:1}:where(.mc-root :is(rect,circle,ellipse,polygon)[data-mc-ink=ghost]){fill:GrayText}:where(.mc-root :is(path,line,polyline)[data-mc-ink=ghost]){stroke:GrayText}:where(.mc-root [data-mc-cat]){fill:CanvasText;stroke:Canvas;stroke-width:.5}:where(.mc-root [data-mc-cat="2"]){fill-opacity:.92}:where(.mc-root [data-mc-cat="3"]){fill-opacity:.85}:where(.mc-root [data-mc-cat="4"]){fill-opacity:.77}:where(.mc-root [data-mc-cat="5"]){fill-opacity:.7}:where(.mc-root [data-mc-cat="6"]){fill-opacity:.62}:where(.mc-root text[data-mc-on-fill]){fill:Canvas}:where(.mc-trace,.mc-partition,.mc-segbar) :where(text){fill:Canvas}:where(.mc-trace) :where(text[data-mc-dim]){fill:CanvasText}:where(.mc-root [data-mc-ink=region]){fill:transparent}:where(.mc-root text[data-mc-ink=label]){fill:CanvasText}:where(.mc-root line[data-mc-ink=flag]){stroke:Highlight}:where(.mc-root circle[data-mc-ink=flag]){fill:Highlight}:where([data-mc-host]:focus-visible,.mc-root:focus-visible){outline-color:Highlight}:where(.mc-delta){color:CanvasText}:where(.mc-root [data-mc-status]){fill:CanvasText}:where(.mc-root circle[data-mc-status=ring]),:where(.mc-root circle[data-mc-status=half]){fill:none;stroke:CanvasText}:where(.mc-root [fill=none]){fill:none}}}
11
+ @layer microcharts.charts{:where(.mc-spark-interactive [data-mc-ink=muted]){stroke-dasharray:1.5 2}}
12
+
13
+ /* ============================================================
14
+ * 主题覆盖:把 microcharts 的 token 接到 DSH 的语义变量上
15
+ * ============================================================ */
16
+ :where(.mc-theme) {
17
+ /* 兜底用 currentColor 而不是一个固定的深色:固定深色在深色宿主里
18
+ 是不可见的黑,而 currentColor 永远等于它身边的文字色——宿主改了
19
+ 变量名时图表只是「跟正文同色」,不是「消失」。 */
20
+ --mc-stroke: var(--dsw-alias-label-primary, currentColor);
21
+ --mc-neutral: var(--dsw-alias-label-tertiary, color-mix(in srgb, currentColor 55%, transparent));
22
+ --mc-accent: var(--dsw-alias-state-business-primary, #1f6091);
23
+ --mc-positive: var(--dsw-alias-state-success-primary, #0e7a5f);
24
+ --mc-negative: #e8797f;
25
+ --mc-cat-1: var(--dsw-alias-state-success-primary, #2e8c66);
26
+ --mc-cat-2: var(--dsw-alias-state-warn-primary, #d2982f);
27
+ --mc-cat-3: #e8797f;
28
+ --mc-cat-4: var(--dsw-alias-label-tertiary, #8a8986);
29
+ --mc-cat-5: var(--dsw-alias-state-business-primary, #285788);
30
+ --mc-cat-6: var(--dsw-alias-label-secondary, #a55a89);
31
+ --mc-on-cat: var(--dsw-alias-bg-base, rgba(0, 0, 0, .9));
32
+ --mc-on-cat-1: var(--dsw-alias-bg-base, rgba(0, 0, 0, .9));
33
+ --mc-on-cat-2: var(--dsw-alias-bg-base, rgba(0, 0, 0, .9));
34
+ --mc-on-cat-3: var(--dsw-alias-bg-base, rgba(255, 255, 255, .96));
35
+ --mc-on-cat-4: var(--dsw-alias-bg-base, rgba(0, 0, 0, .9));
36
+ --mc-on-cat-5: var(--dsw-alias-bg-base, rgba(255, 255, 255, .96));
37
+ --mc-on-cat-6: var(--dsw-alias-bg-base, rgba(255, 255, 255, .96));
38
+ --mc-font: inherit;
39
+ }
40
+
41
+ /* 图表与数字并排时的容器:垂直居中,行高归零免得撑高卡片。 */
42
+ :where(.de-chart) {
43
+ display: inline-flex;
44
+ align-items: center;
45
+ vertical-align: middle;
46
+ line-height: 0;
47
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * microcharts 的**按需子集**(只导出统计页真正用到的图型)。
3
+ *
4
+ * 为什么不导出整个目录:本插件的硬约束之一是首屏体积。整包 106 种图型
5
+ * 即使 tree-shaking 也带着共享几何,而统计页只用得到「比较大小」「占比」
6
+ * 「目标达成」这三类。少一种图型就少一份要审的第三方代码。
7
+ */
8
+ export { Sparkline } from "@microcharts/react/sparkline";
9
+ export { SparkBar } from "@microcharts/react/sparkbar";
10
+ export { Bullet } from "@microcharts/react/bullet";
11
+ export { SegmentedBar } from "@microcharts/react/segmented-bar";
12
+ export { Delta } from "@microcharts/react/delta";
13
+ export { MiniBar } from "@microcharts/react/mini-bar";
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `react/jsx-runtime` 的最小垫片:把 JSX 产物接到宿主给的 `react` 上。
3
+ *
4
+ * 为什么需要这个文件:microcharts 的构建产物内部 import
5
+ * `react/jsx-runtime`(自动 JSX 运行时),而宿主 factory 只保证
6
+ * `require("react")` 可用——`react/jsx-runtime` 不在平台种子表里。
7
+ * 与其赌宿主会暴露第二个种子词,不如把 jsx 调用翻成
8
+ * `createElement`,让整份产物**只依赖 react**(唯一的 peer)。
9
+ *
10
+ * 语义要点:
11
+ * - `children` 是 props 的一个键,不是独立参数,必须拆出来当第三参;
12
+ * - key 在 createElement 里走第三参的位置(这里直接赋值给元素),
13
+ * 不能塞进 props(否则 React 会当成普通属性警告)。
14
+ *
15
+ * 这是构建期私有垫片,不对外发布。
16
+ */
17
+ import { createElement, Fragment } from "react";
18
+
19
+ /** 统一的 jsx/jsxs/jsxDEV 实现:三者在本库产物里的行为一致。 */
20
+ function jsx(type, props, key) {
21
+ const { children, ...rest } = props ?? {};
22
+ const args =
23
+ children === undefined
24
+ ? [type, rest]
25
+ : [type, rest, ...(Array.isArray(children) ? children : [children])];
26
+ const element = createElement(...args);
27
+ if (key !== undefined && element !== null && typeof element === "object") element.key = key;
28
+ return element;
29
+ }
30
+
31
+ const jsxs = jsx;
32
+ const jsxDEV = jsx;
33
+
34
+ export { jsx, jsxs, jsxDEV, Fragment };
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Decision Engine",
4
+ "description": "A cheap judgement layer: before the main model acts, a small decision model answers structured questions (risk, reversibility, task relevance, injection suspicion) and the plugin allows, asks or blocks — or switches model, or skips an expensive call entirely. Works against any service exposing a decision endpoint (built-in system one / bamboonode / SiliconFlow presets, or a custom base URL). Shadow-first by default, fail-open on any error."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Decision Engine",
4
+ "description": "便宜的决策层:在主模型动手前用决策小模型做结构化判断(风险、可逆性、任务相关性、注入嫌疑),决定放行、询问或拦截,也可换模型或省掉一次昂贵调用。可接任意提供决策端点的服务(内置 system one / bamboonode / 硅基流动 / Perplexity Decider / OpenRouter 预设,或自定义 base URL)。默认只观察不拦截,任何异常都自动放行。"
5
+ }
6
+ }