@xiciyang/rsi-contract 0.0.0-stage → 0.1.0

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,890 @@
1
+ /**
2
+ * contract.mjs —— **声明的契约面**:能力 id、错误码、I/O schema、失败模式、能力清单。
3
+ *
4
+ * 本文件是"机读契约"的唯一来源:`CAPABILITY_MANIFEST` 由它与各能力的实现现场组合而成,
5
+ * 而 CI 可以只 import 本文件就把五个能力的 schema 与失败模式枚举出来(不需要跑任何实现)。
6
+ *
7
+ * 纪律:
8
+ * - 每一个 schema 都不写 `default`:**本库没有兜底默认值**(fail-closed 的正面要求)。
9
+ * - 每一个失败模式都带 `falsifiedBy`:说明"哪条测试会让它变红"。
10
+ * 只声明不举证的失败模式等于没声明 —— 那是文档而不是契约。
11
+ */
12
+
13
+ /** 九个错误码之外,本库还用 `E_*` 表示"拒绝"(refused)与"阻断"(blocked)。 */
14
+ export const ERROR_CODES = Object.freeze({
15
+ // ── 结构性输入错误(抛 RsiLoopError) ──
16
+ E_INPUT_SHAPE: 'E_INPUT_SHAPE',
17
+ // ── 通用 fail-closed ──
18
+ E_METRICS_ABSENT: 'E_METRICS_ABSENT',
19
+ // ── goalSetting ──
20
+ E_GOAL_UNREACHABLE: 'E_GOAL_UNREACHABLE',
21
+ E_GOAL_CONFLICT: 'E_GOAL_CONFLICT',
22
+ E_GOAL_MEANINGLESS: 'E_GOAL_MEANINGLESS',
23
+ // ── directionSelection ──
24
+ E_DIRECTION_MISMATCH: 'E_DIRECTION_MISMATCH',
25
+ E_MEANS_NOT_IN_MENU: 'E_MEANS_NOT_IN_MENU',
26
+ E_CONSTRAINT_VIOLATION: 'E_CONSTRAINT_VIOLATION',
27
+ E_NO_MEANS: 'E_NO_MEANS',
28
+ E_NO_ADMISSIBLE_MEANS: 'E_NO_ADMISSIBLE_MEANS',
29
+ E_MEANS_UNMEASURABLE: 'E_MEANS_UNMEASURABLE',
30
+ E_RISK_MISSING: 'E_RISK_MISSING',
31
+ // ── effectJudgment ──
32
+ E_INTEGRITY_MISSING: 'E_INTEGRITY_MISSING',
33
+ E_MEASUREMENT_INCOMPLETE: 'E_MEASUREMENT_INCOMPLETE',
34
+ E_DATASET_IDENTITY_MISSING: 'E_DATASET_IDENTITY_MISSING',
35
+ E_DATASET_MISMATCH: 'E_DATASET_MISMATCH',
36
+ E_METRIC_MISMATCH: 'E_METRIC_MISMATCH',
37
+ E_METRIC_VALUE_MISSING: 'E_METRIC_VALUE_MISSING',
38
+ E_NO_SAMPLES: 'E_NO_SAMPLES',
39
+ E_THRESHOLD_NOT_PROVIDED: 'E_THRESHOLD_NOT_PROVIDED',
40
+ E_WITHIN_NOISE: 'E_WITHIN_NOISE',
41
+ E_BELOW_THRESHOLD: 'E_BELOW_THRESHOLD',
42
+ E_SELF_JUDGING: 'E_SELF_JUDGING',
43
+ // ── riskAssessment ──
44
+ E_RISK_UNCLASSIFIED: 'E_RISK_UNCLASSIFIED',
45
+ E_RISK_UNDERESTIMATE: 'E_RISK_UNDERESTIMATE',
46
+ E_RISK_OVERESTIMATE: 'E_RISK_OVERESTIMATE',
47
+ // ── rollbackDecision ──
48
+ E_NO_ROLLBACK_TARGET: 'E_NO_ROLLBACK_TARGET',
49
+ E_TARGET_NOT_VERIFIED_GOOD: 'E_TARGET_NOT_VERIFIED_GOOD',
50
+ E_ARTIFACT_SET_MISMATCH: 'E_ARTIFACT_SET_MISMATCH',
51
+ E_ROLLBACK_NOOP: 'E_ROLLBACK_NOOP',
52
+ E_PROCESS_ONLY_VERIFICATION: 'E_PROCESS_ONLY_VERIFICATION',
53
+ E_ROLLBACK_INCOMPLETE: 'E_ROLLBACK_INCOMPLETE',
54
+ E_ROLLBACK_WRONG_VERSION: 'E_ROLLBACK_WRONG_VERSION',
55
+ // ── JSON 卫生(support 层) ──
56
+ E_NOT_JSON_SAFE: 'E_NOT_JSON_SAFE',
57
+ E_CYCLIC_REFERENCE: 'E_CYCLIC_REFERENCE',
58
+ E_LIVE_OBJECT: 'E_LIVE_OBJECT',
59
+ E_NON_FINITE_NUMBER: 'E_NON_FINITE_NUMBER',
60
+ })
61
+
62
+ export const CAPABILITY_IDS = Object.freeze([
63
+ 'goalSetting',
64
+ 'directionSelection',
65
+ 'effectJudgment',
66
+ 'riskAssessment',
67
+ 'rollbackDecision',
68
+ ])
69
+
70
+ /** 值域词表(声明在契约里,实现不得私自定义第二份)。 */
71
+ export const VOCAB = Object.freeze({
72
+ direction: ['higher-is-better', 'lower-is-better'],
73
+ verdict: ['success', 'failure', 'measurement-invalid'],
74
+ riskLevel: ['low', 'medium', 'high', 'critical'],
75
+ historyOutcome: ['success', 'failure', 'blocked', 'measurement-invalid'],
76
+ })
77
+
78
+ /** 回退核验的比较算子(只允许**内容哈希**比较;没有"退出码相等"这种算子)。 */
79
+ export const VERIFICATION_COMPARATORS = Object.freeze(['sha256-equal'])
80
+
81
+ const evidenceRefsSchema = {
82
+ type: 'array',
83
+ items: { type: 'string', minLength: 1 },
84
+ description: '可复核证据引用(度量 id / 血缘键 / 轮次 id);必须能在输入内解析,否则该结论判为无证据',
85
+ }
86
+
87
+ /* ────────────────────────────── I/O SCHEMA ────────────────────────────── */
88
+ /* 手写 JSON-Schema 子集(type / properties / required / additionalProperties / enum / items /
89
+ minLength / minimum)。不引任何 schema 库 —— 零依赖,且这里的形状是封闭的。 */
90
+
91
+ const SCHEMAS = {
92
+ goalSetting: {
93
+ input: {
94
+ type: 'object',
95
+ additionalProperties: false,
96
+ required: ['state'],
97
+ properties: {
98
+ state: {
99
+ type: 'object',
100
+ additionalProperties: false,
101
+ required: ['metrics'],
102
+ properties: {
103
+ metrics: {
104
+ type: 'object',
105
+ description: '当前度量台账:键 = 度量 id。**缺失或为空 ⇒ 不发散任何自信目标(fail-closed)**',
106
+ additionalProperties: {
107
+ type: 'object',
108
+ additionalProperties: false,
109
+ required: ['value', 'direction'],
110
+ properties: {
111
+ value: { type: 'number' },
112
+ direction: { type: 'string', enum: VOCAB.direction },
113
+ evidenceRefs: evidenceRefsSchema,
114
+ expectedDelta: {
115
+ type: 'number',
116
+ description: '沿 direction 的**改进量**(正数 = 变好)。≤0 ⇒ 不可达(按当前证据不比现状更好)',
117
+ },
118
+ atBound: { type: 'boolean', description: '已顶到该度量的边界 ⇒ 无余量 ⇒ 不可达' },
119
+ unit: { type: 'string' },
120
+ },
121
+ },
122
+ },
123
+ lineage: { type: 'object', description: '血缘/来源引用:键 → 哈希或标识' },
124
+ history: {
125
+ type: 'array',
126
+ description: '既往轮次记录(期望增量的证据来源之一)',
127
+ items: {
128
+ type: 'object',
129
+ additionalProperties: false,
130
+ required: ['roundId', 'targetMetric', 'outcome'],
131
+ properties: {
132
+ roundId: { type: 'string' },
133
+ targetMetric: { type: 'string' },
134
+ outcome: { type: 'string', enum: VOCAB.historyOutcome },
135
+ delta: { type: 'number', description: '上一轮实测的改进量(正数 = 变好)' },
136
+ evidenceRefs: evidenceRefsSchema,
137
+ },
138
+ },
139
+ },
140
+ constraints: {
141
+ type: 'array',
142
+ description: '声明的目标间耦合关系(冲突只能来自**已声明**的关系,不得凭猜测)',
143
+ items: {
144
+ type: 'object',
145
+ additionalProperties: false,
146
+ required: ['id', 'kind'],
147
+ properties: {
148
+ id: { type: 'string' },
149
+ kind: { type: 'string', enum: ['tradeoff', 'mutually-exclusive'] },
150
+ between: { type: 'array', items: { type: 'string' } },
151
+ note: { type: 'string' },
152
+ },
153
+ },
154
+ },
155
+ },
156
+ },
157
+ means: {
158
+ type: 'array',
159
+ description: '**可选**:可用手段菜单(只用于可达性分析;方向选择另有一份输入)',
160
+ items: {
161
+ type: 'object',
162
+ additionalProperties: false,
163
+ required: ['id', 'targets'],
164
+ properties: {
165
+ id: { type: 'string' },
166
+ targets: { type: 'array', items: { type: 'string' } },
167
+ },
168
+ },
169
+ },
170
+ options: {
171
+ type: 'object',
172
+ additionalProperties: false,
173
+ properties: { maxGoals: { type: 'integer', minimum: 0 } },
174
+ },
175
+ },
176
+ },
177
+ output: {
178
+ type: 'object',
179
+ additionalProperties: false,
180
+ required: ['capability', 'version', 'goals', 'refused', 'code', 'reasons', 'evidenceRefs'],
181
+ properties: {
182
+ capability: { type: 'string', const: 'goalSetting' },
183
+ version: { type: 'string' },
184
+ goals: {
185
+ type: 'array',
186
+ items: {
187
+ type: 'object',
188
+ additionalProperties: false,
189
+ required: ['goalId', 'statement', 'targetMetric', 'direction', 'expectedDelta', 'rationale', 'evidenceRefs', 'priority', 'priorityBasis'],
190
+ properties: {
191
+ goalId: { type: 'string' },
192
+ statement: { type: 'string' },
193
+ targetMetric: { type: 'string' },
194
+ direction: { type: ['string', 'null'], description: '被阻断且无可测方向时为 null —— 不伪造占位值' },
195
+ expectedDelta: { type: ['number', 'null'], description: '无来源时为 null(不是 0)' },
196
+ rationale: { type: 'string' },
197
+ evidenceRefs: evidenceRefsSchema,
198
+ priority: { type: ['number', 'null'], description: '无量纲相对期望改进;无法归一 ⇒ null(不是 0)' },
199
+ priorityBasis: { type: 'string', enum: ['relative-to-current', 'unavailable'] },
200
+ blocked: {
201
+ type: 'object',
202
+ additionalProperties: false,
203
+ required: ['code', 'reason'],
204
+ properties: {
205
+ code: { type: 'string' },
206
+ reason: { type: 'string' },
207
+ conflictsWith: { type: 'array', items: { type: 'string' } },
208
+ },
209
+ },
210
+ },
211
+ },
212
+ },
213
+ refused: { type: 'boolean' },
214
+ code: { type: ['string', 'null'] },
215
+ reasons: { type: 'array', items: { type: 'string' } },
216
+ evidenceRefs: evidenceRefsSchema,
217
+ },
218
+ },
219
+ },
220
+
221
+ directionSelection: {
222
+ input: {
223
+ type: 'object',
224
+ additionalProperties: false,
225
+ required: ['goal', 'means'],
226
+ properties: {
227
+ goal: {
228
+ type: 'object',
229
+ additionalProperties: false,
230
+ required: ['goalId', 'targetMetric', 'direction'],
231
+ properties: {
232
+ goalId: { type: 'string' },
233
+ targetMetric: { type: 'string' },
234
+ direction: { type: 'string', enum: VOCAB.direction },
235
+ expectedDelta: { type: 'number' },
236
+ evidenceRefs: evidenceRefsSchema,
237
+ },
238
+ },
239
+ means: {
240
+ type: 'array',
241
+ description: '**声明的菜单**。不在其中的手段一律不可选(fail-closed)',
242
+ items: {
243
+ type: 'object',
244
+ additionalProperties: false,
245
+ required: ['id', 'targets', 'expectedGain', 'risk'],
246
+ properties: {
247
+ id: { type: 'string' },
248
+ targets: { type: 'array', items: { type: 'string' } },
249
+ expectedGain: { type: 'number' },
250
+ risk: { type: 'object', description: '**透传**:riskAssessment 的产物(本能力不自行评估风险)' },
251
+ irreversible: { type: 'boolean' },
252
+ evidenceRefs: evidenceRefsSchema,
253
+ },
254
+ },
255
+ },
256
+ constraints: {
257
+ type: 'array',
258
+ items: {
259
+ type: 'object',
260
+ additionalProperties: false,
261
+ required: ['id', 'kind'],
262
+ properties: {
263
+ id: { type: 'string' },
264
+ kind: { type: 'string', enum: ['forbid-means', 'forbid-irreversible', 'require-target'] },
265
+ meansId: { type: 'string' },
266
+ targets: { type: 'array', items: { type: 'string' } },
267
+ note: { type: 'string' },
268
+ },
269
+ },
270
+ },
271
+ preferredMeansId: { type: 'string', description: '调用方指定的手段:**若不在菜单内即拒绝**' },
272
+ },
273
+ },
274
+ output: {
275
+ type: 'object',
276
+ additionalProperties: false,
277
+ required: ['capability', 'version', 'chosen', 'rationale', 'expectedGain', 'risk', 'alternativesConsidered', 'refused', 'code', 'reasons', 'evidenceRefs', 'selectionRule'],
278
+ properties: {
279
+ capability: { type: 'string', const: 'directionSelection' },
280
+ version: { type: 'string' },
281
+ chosen: {
282
+ type: ['object', 'null'],
283
+ additionalProperties: false,
284
+ required: ['meansId', 'targetMetric', 'direction', 'expectedGain'],
285
+ properties: {
286
+ meansId: { type: 'string' },
287
+ targetMetric: { type: 'string' },
288
+ direction: { type: 'string', enum: VOCAB.direction },
289
+ expectedGain: { type: 'number' },
290
+ },
291
+ },
292
+ rationale: { type: 'string' },
293
+ expectedGain: { type: ['number', 'null'] },
294
+ risk: { type: ['object', 'null'], description: '**透传**自所选手段的 risk 字段(本能力不生成风险结论)' },
295
+ alternativesConsidered: {
296
+ type: 'array',
297
+ items: {
298
+ type: 'object',
299
+ additionalProperties: false,
300
+ required: ['meansId', 'admissible', 'code', 'reason'],
301
+ properties: {
302
+ meansId: { type: 'string' },
303
+ admissible: { type: 'boolean' },
304
+ code: { type: ['string', 'null'] },
305
+ reason: { type: ['string', 'null'] },
306
+ },
307
+ },
308
+ },
309
+ refused: { type: 'boolean' },
310
+ code: { type: ['string', 'null'] },
311
+ reasons: { type: 'array', items: { type: 'string' } },
312
+ evidenceRefs: evidenceRefsSchema,
313
+ selectionRule: { type: 'string' },
314
+ },
315
+ },
316
+ },
317
+
318
+ effectJudgment: {
319
+ input: {
320
+ type: 'object',
321
+ additionalProperties: false,
322
+ required: ['before', 'after', 'integrity', 'direction', 'threshold', 'noise'],
323
+ properties: {
324
+ before: {
325
+ type: 'object',
326
+ additionalProperties: false,
327
+ required: ['metric', 'value', 'datasetHash'],
328
+ properties: {
329
+ metric: { type: 'string' },
330
+ value: { type: 'number' },
331
+ datasetHash: { type: 'string', description: '评估集内容哈希(eval-contract §7:Δ 只在同一 datasetHash 内可比)' },
332
+ evidenceRefs: evidenceRefsSchema,
333
+ },
334
+ },
335
+ after: {
336
+ type: 'object',
337
+ additionalProperties: false,
338
+ required: ['metric', 'value', 'datasetHash'],
339
+ properties: {
340
+ metric: { type: 'string' },
341
+ value: { type: 'number' },
342
+ datasetHash: { type: 'string' },
343
+ evidenceRefs: evidenceRefsSchema,
344
+ },
345
+ },
346
+ integrity: {
347
+ type: 'object',
348
+ description: '**测量完整性必须由别处生产**(本能力不得自产自证)',
349
+ additionalProperties: false,
350
+ required: ['attempted', 'succeeded', 'failed', 'partial'],
351
+ properties: {
352
+ attempted: { type: 'number' },
353
+ succeeded: { type: 'number' },
354
+ failed: { type: 'number' },
355
+ partial: { type: 'boolean' },
356
+ producedBy: { type: 'string', description: '生产者标识;等于 effectJudgment 时判为自证(E_SELF_JUDGING)' },
357
+ evidenceRefs: evidenceRefsSchema,
358
+ },
359
+ },
360
+ direction: { type: 'string', enum: VOCAB.direction },
361
+ threshold: {
362
+ type: 'object',
363
+ additionalProperties: false,
364
+ required: ['minDelta'],
365
+ properties: { minDelta: { type: 'number', description: '**必填且无默认值**:达标所需的最小改进量' } },
366
+ },
367
+ noise: {
368
+ type: 'object',
369
+ additionalProperties: false,
370
+ required: ['se', 'z'],
371
+ properties: {
372
+ se: { type: 'number', description: '**必填且无默认值**:改进量的标准误(离散度)' },
373
+ z: { type: 'number', description: '**必填且无默认值**:显著性倍数(本库不硬编码 1.96)' },
374
+ },
375
+ },
376
+ },
377
+ },
378
+ output: {
379
+ type: 'object',
380
+ additionalProperties: false,
381
+ required: ['capability', 'version', 'verdict', 'refused', 'code', 'evidence', 'reasons', 'evidenceRefs'],
382
+ properties: {
383
+ capability: { type: 'string', const: 'effectJudgment' },
384
+ version: { type: 'string' },
385
+ verdict: { type: 'string', enum: VOCAB.verdict },
386
+ refused: { type: 'boolean' },
387
+ code: { type: ['string', 'null'] },
388
+ inconclusive: { type: 'boolean' },
389
+ evidence: {
390
+ type: 'object',
391
+ additionalProperties: false,
392
+ required: ['metric', 'direction', 'datasetHash', 'integrity', 'values'],
393
+ properties: {
394
+ metric: { type: ['string', 'null'] },
395
+ direction: { type: ['string', 'null'] },
396
+ datasetHash: { type: ['string', 'null'] },
397
+ integrity: { type: ['object', 'null'] },
398
+ values: {
399
+ type: 'object',
400
+ additionalProperties: false,
401
+ properties: {
402
+ before: { type: ['number', 'null'] },
403
+ after: { type: ['number', 'null'] },
404
+ improvement: { type: ['number', 'null'] },
405
+ minDelta: { type: ['number', 'null'] },
406
+ se: { type: ['number', 'null'] },
407
+ z: { type: ['number', 'null'] },
408
+ noiseBound: { type: ['number', 'null'] },
409
+ criterion: { type: ['string', 'null'] },
410
+ },
411
+ },
412
+ },
413
+ },
414
+ reasons: { type: 'array', items: { type: 'string' } },
415
+ evidenceRefs: evidenceRefsSchema,
416
+ },
417
+ },
418
+ },
419
+
420
+ riskAssessment: {
421
+ input: {
422
+ type: 'object',
423
+ additionalProperties: false,
424
+ required: ['direction'],
425
+ properties: {
426
+ direction: {
427
+ type: 'object',
428
+ additionalProperties: false,
429
+ required: ['id'],
430
+ properties: {
431
+ id: { type: 'string' },
432
+ kind: { type: 'string' },
433
+ targetMetric: { type: 'string' },
434
+ factors: {
435
+ type: 'array',
436
+ items: { type: 'string' },
437
+ description: '结构因子(已归类的风险事实)。**未声明任何因子 ⇒ 判为未识别 ⇒ 升到最高档**',
438
+ },
439
+ irreversible: { type: 'boolean' },
440
+ inPlace: { type: 'boolean', description: '就地变更(独立风险档)' },
441
+ declaredLevel: { type: 'string', enum: VOCAB.riskLevel, description: '调用方的自评档位;低于计算值 ⇒ 低估,高于 ⇒ 高估' },
442
+ evidenceRefs: evidenceRefsSchema,
443
+ },
444
+ },
445
+ state: { type: 'object', description: '系统状态(只读上下文;本能力不从中猜风险)' },
446
+ },
447
+ },
448
+ output: {
449
+ type: 'object',
450
+ additionalProperties: false,
451
+ required: ['capability', 'version', 'level', 'mitigations', 'requiresHumanConfirmation', 'reasons', 'factorLevels', 'calibration', 'refused', 'code', 'evidenceRefs'],
452
+ properties: {
453
+ capability: { type: 'string', const: 'riskAssessment' },
454
+ version: { type: 'string' },
455
+ level: { type: 'string', enum: VOCAB.riskLevel },
456
+ mitigations: { type: 'array', items: { type: 'string' } },
457
+ requiresHumanConfirmation: { type: 'boolean' },
458
+ reasons: { type: 'array', items: { type: 'string' } },
459
+ factorLevels: {
460
+ type: 'array',
461
+ items: {
462
+ type: 'object',
463
+ additionalProperties: false,
464
+ required: ['factor', 'level', 'known', 'injected'],
465
+ properties: {
466
+ factor: { type: 'string' },
467
+ level: { type: 'string', enum: VOCAB.riskLevel },
468
+ known: { type: 'boolean' },
469
+ injected: { type: 'boolean', description: '由结构布尔(irreversible/inPlace)自动注入,而非调用方列举' },
470
+ },
471
+ },
472
+ },
473
+ calibration: {
474
+ type: 'object',
475
+ additionalProperties: false,
476
+ required: ['declaredLevel', 'computedLevel', 'verdict'],
477
+ properties: {
478
+ declaredLevel: { type: ['string', 'null'] },
479
+ computedLevel: { type: 'string' },
480
+ verdict: { type: 'string', enum: ['consistent', 'underestimated', 'overestimated', 'undeclared'] },
481
+ },
482
+ },
483
+ refused: { type: 'boolean' },
484
+ code: { type: ['string', 'null'] },
485
+ evidenceRefs: evidenceRefsSchema,
486
+ },
487
+ },
488
+ },
489
+
490
+ rollbackDecision: {
491
+ input: {
492
+ type: 'object',
493
+ additionalProperties: false,
494
+ required: ['failure', 'history'],
495
+ properties: {
496
+ failure: {
497
+ type: 'object',
498
+ additionalProperties: false,
499
+ required: ['stateId', 'artifacts'],
500
+ properties: {
501
+ stateId: { type: 'string' },
502
+ artifacts: {
503
+ type: 'array',
504
+ items: {
505
+ type: 'object',
506
+ additionalProperties: false,
507
+ required: ['name', 'sha256'],
508
+ properties: { name: { type: 'string' }, sha256: { type: 'string' } },
509
+ },
510
+ },
511
+ evidenceRefs: evidenceRefsSchema,
512
+ },
513
+ },
514
+ history: {
515
+ type: 'array',
516
+ description: '已知的既往状态。**只有 verifiedGood 且有哈希背书的才算可信回退目标**',
517
+ items: {
518
+ type: 'object',
519
+ additionalProperties: false,
520
+ required: ['stateId', 'artifacts', 'verifiedGood'],
521
+ properties: {
522
+ stateId: { type: 'string' },
523
+ seq: { type: 'integer', description: '序号(唯一的排序依据;时间戳不参与决策)' },
524
+ artifacts: {
525
+ type: 'array',
526
+ items: {
527
+ type: 'object',
528
+ additionalProperties: false,
529
+ required: ['name', 'sha256'],
530
+ properties: { name: { type: 'string' }, sha256: { type: 'string' } },
531
+ },
532
+ },
533
+ verifiedGood: { type: 'boolean' },
534
+ verifiedBy: { type: 'string', description: '谁/什么证明了它 good;缺失 ⇒ 视为未验证' },
535
+ verifiedAt: { type: 'string', description: '仅记录,**不参与决策**' },
536
+ evidenceRefs: evidenceRefsSchema,
537
+ },
538
+ },
539
+ },
540
+ proposedVerification: {
541
+ type: 'object',
542
+ description: '调用方自带的核验方案;只断言过程的方案会被拒绝(E_PROCESS_ONLY_VERIFICATION)',
543
+ additionalProperties: true,
544
+ },
545
+ },
546
+ },
547
+ output: {
548
+ type: 'object',
549
+ additionalProperties: false,
550
+ required: ['capability', 'version', 'target', 'path', 'verification', 'refused', 'code', 'reasons', 'evidence', 'evidenceRefs'],
551
+ properties: {
552
+ capability: { type: 'string', const: 'rollbackDecision' },
553
+ version: { type: 'string' },
554
+ target: {
555
+ type: ['object', 'null'],
556
+ additionalProperties: false,
557
+ required: ['stateId', 'seq', 'artifacts'],
558
+ properties: {
559
+ stateId: { type: 'string' },
560
+ seq: { type: 'integer' },
561
+ artifacts: { type: 'array', items: { type: 'object' } },
562
+ },
563
+ },
564
+ path: {
565
+ type: ['object', 'null'],
566
+ additionalProperties: false,
567
+ required: ['kind', 'orderingBasis', 'steps'],
568
+ properties: {
569
+ kind: { type: 'string', const: 'restore-artifacts' },
570
+ orderingBasis: { type: 'string', enum: ['seq', 'array-index'] },
571
+ steps: {
572
+ type: 'array',
573
+ items: {
574
+ type: 'object',
575
+ additionalProperties: false,
576
+ required: ['op', 'artifact', 'from', 'expectSha256'],
577
+ properties: {
578
+ op: { type: 'string', const: 'restore' },
579
+ artifact: { type: 'string' },
580
+ from: { type: 'string' },
581
+ expectSha256: { type: 'string' },
582
+ },
583
+ },
584
+ },
585
+ },
586
+ },
587
+ verification: {
588
+ type: ['object', 'null'],
589
+ additionalProperties: false,
590
+ required: ['kind', 'comparator', 'checks', 'criterion', 'rejectsProcessOnly'],
591
+ properties: {
592
+ kind: { type: 'string', const: 'artifact-content-hash' },
593
+ comparator: { type: 'string', enum: VERIFICATION_COMPARATORS },
594
+ checks: {
595
+ type: 'array',
596
+ items: {
597
+ type: 'object',
598
+ additionalProperties: false,
599
+ required: ['artifact', 'expectSha256', 'observedFrom', 'notBeforeSha256'],
600
+ properties: {
601
+ artifact: { type: 'string' },
602
+ expectSha256: { type: 'string' },
603
+ observedFrom: { type: 'string' },
604
+ notBeforeSha256: { type: 'string', description: '回退前的哈希:与 expect 相同即空操作,必须判失败' },
605
+ },
606
+ },
607
+ },
608
+ criterion: { type: 'string' },
609
+ rejectsProcessOnly: { type: 'boolean' },
610
+ },
611
+ },
612
+ refused: { type: 'boolean' },
613
+ code: { type: ['string', 'null'] },
614
+ reasons: { type: 'array', items: { type: 'string' } },
615
+ evidence: {
616
+ type: 'object',
617
+ additionalProperties: false,
618
+ required: ['failureHashes', 'considered'],
619
+ properties: {
620
+ failureHashes: { type: 'object' },
621
+ considered: {
622
+ type: 'array',
623
+ items: {
624
+ type: 'object',
625
+ additionalProperties: false,
626
+ required: ['stateId', 'hashes', 'eligible', 'code', 'reason'],
627
+ properties: {
628
+ stateId: { type: 'string' },
629
+ hashes: { type: 'object', description: '该状态各产物的内容哈希(核验时用于把"未知版本"归属到具体状态)' },
630
+ eligible: { type: 'boolean' },
631
+ code: { type: ['string', 'null'] },
632
+ reason: { type: ['string', 'null'] },
633
+ },
634
+ },
635
+ },
636
+ },
637
+ },
638
+ evidenceRefs: evidenceRefsSchema,
639
+ },
640
+ },
641
+ },
642
+ }
643
+
644
+ /* ────────────────────────── 失败模式(声明 + 举证) ────────────────────────── */
645
+
646
+ const FAILURE_MODES = {
647
+ goalSetting: [
648
+ {
649
+ code: 'E_GOAL_UNREACHABLE',
650
+ name: '目标不可达',
651
+ detect: '期望改进量 ≤ 0 / 度量已顶到边界(atBound)/ 已声明手段菜单中无任何手段覆盖该度量 ⇒ 该候选 blocked',
652
+ falsifiedBy: 'unit/goal-setting.spec.mjs —— 「不可达必须 blocked,不得作为正常候选」',
653
+ },
654
+ {
655
+ code: 'E_GOAL_CONFLICT',
656
+ name: '目标冲突',
657
+ detect: '两候选的度量出现在**已声明**的 tradeoff / mutually-exclusive 关系中 ⇒ 冲突双方都 blocked 并互相点名(不静默替调用方选边)',
658
+ falsifiedBy: 'unit/goal-setting.spec.mjs —— 「已声明的冲突关系必须让双方 blocked 且 conflictsWith 非空」',
659
+ },
660
+ {
661
+ code: 'E_GOAL_MEANINGLESS',
662
+ name: '目标无意义',
663
+ detect: '缺可测目标(无 direction / 无期望增量来源)或无可解析的证据引用 ⇒ blocked',
664
+ falsifiedBy: 'unit/goal-setting.spec.mjs —— 「无证据引用 / 引用不可解析 ⇒ blocked」',
665
+ },
666
+ {
667
+ code: 'E_METRICS_ABSENT',
668
+ name: '度量缺失时不得发散自信目标',
669
+ detect: 'metrics 缺失或为空 ⇒ refused + goals: [](**绝不发明度量**)',
670
+ falsifiedBy: 'unit/goal-setting.spec.mjs —— 「缺度量 ⇒ 空目标 + refused」',
671
+ },
672
+ ],
673
+ directionSelection: [
674
+ {
675
+ code: 'E_DIRECTION_MISMATCH',
676
+ name: '方向错误',
677
+ detect: "所选手段的 targets 不含目标的 targetMetric ⇒ 该手段不可采纳;若被 preferredMeansId 强制指定则整体拒绝",
678
+ falsifiedBy: 'unit/direction-selection.spec.mjs —— 「手段不打目标度量 ⇒ 拒绝」',
679
+ },
680
+ {
681
+ code: 'E_MEANS_NOT_IN_MENU',
682
+ name: '不可行(不在声明的菜单内)',
683
+ detect: 'preferredMeansId 不在 means 菜单 ⇒ refused(fail-closed)',
684
+ falsifiedBy: 'unit/direction-selection.spec.mjs —— 「菜单外手段 ⇒ refused」',
685
+ },
686
+ {
687
+ code: 'E_CONSTRAINT_VIOLATION',
688
+ name: '与约束冲突',
689
+ detect: 'forbid-means / forbid-irreversible / require-target 三条约束被违反 ⇒ 该手段不可采纳',
690
+ falsifiedBy: 'unit/direction-selection.spec.mjs —— 「禁不可逆约束下不可逆手段不可采纳」',
691
+ },
692
+ {
693
+ code: 'E_RISK_MISSING',
694
+ name: '风险未透传',
695
+ detect: '手段缺 risk 字段 ⇒ 整体拒绝(选一个未评估风险的方向正是"风险未识别"的入口)',
696
+ falsifiedBy: 'unit/direction-selection.spec.mjs —— 「缺 risk ⇒ refused」',
697
+ },
698
+ {
699
+ code: 'E_MEANS_UNMEASURABLE',
700
+ name: '手段的期望增益不可测',
701
+ detect: 'expectedGain 不是有限数 ⇒ 该手段不可采纳(无收益数字的手段不得被选中)',
702
+ falsifiedBy: 'unit/direction-selection.spec.mjs —— 「expectedGain 缺失 ⇒ 不可采纳」',
703
+ },
704
+ {
705
+ code: 'E_NO_ADMISSIBLE_MEANS',
706
+ name: '菜单内无可采纳手段',
707
+ detect: '菜单非空但全部被拒 ⇒ refused(不是"随便挑一个")',
708
+ falsifiedBy: 'unit/direction-selection.spec.mjs —— 「全部被拒 ⇒ refused 且逐条给出理由」',
709
+ },
710
+ ],
711
+ effectJudgment: [
712
+ {
713
+ code: 'E_MEASUREMENT_INCOMPLETE',
714
+ name: '测量不成立被忽略',
715
+ detect: 'integrity 缺字段 / failed > 0 / partial === true / succeeded === 0 ⇒ verdict = measurement-invalid(**永不 success**)',
716
+ falsifiedBy: 'unit/effect-judgment.spec.mjs —— 「failed=40 的截断测量必须 measurement-invalid」',
717
+ },
718
+ {
719
+ code: 'E_DATASET_MISMATCH',
720
+ name: '误判成功(跨评估集比较)',
721
+ detect: '两侧 datasetHash 不同或缺哈希 ⇒ measurement-invalid(eval-contract §7:Δ 只在同一 datasetHash 内可比)',
722
+ falsifiedBy: 'unit/effect-judgment.spec.mjs —— 「换评估集必须拒绝判定」',
723
+ },
724
+ {
725
+ code: 'E_WITHIN_NOISE',
726
+ name: '误判成功(噪声内)',
727
+ detect: 'improvement ≤ z·SE ⇒ failure + inconclusive(**永不 success**)',
728
+ falsifiedBy: 'unit/effect-judgment.spec.mjs —— 「改进落在噪声内 ⇒ 不得 success」',
729
+ },
730
+ {
731
+ code: 'E_THRESHOLD_NOT_PROVIDED',
732
+ name: '阈值缺失时不得自行发明判据',
733
+ detect: 'minDelta / se / z 任一缺失 ⇒ refused + measurement-invalid(本库不硬编码 1.96 之类的默认值)',
734
+ falsifiedBy: 'unit/effect-judgment.spec.mjs —— 「缺 z 或 SE ⇒ refused」',
735
+ },
736
+ {
737
+ code: 'E_SELF_JUDGING',
738
+ name: '自证',
739
+ detect: 'integrity.producedBy === "effectJudgment" ⇒ measurement-invalid',
740
+ falsifiedBy: 'unit/effect-judgment.spec.mjs —— 「自产的完整性证据必须被拒」',
741
+ },
742
+ {
743
+ code: 'E_BELOW_THRESHOLD',
744
+ name: '误判成功(未达阈值)',
745
+ detect: '显著但 improvement < minDelta ⇒ failure',
746
+ falsifiedBy: 'unit/effect-judgment.spec.mjs —— 「显著但未达阈值 ⇒ failure」',
747
+ },
748
+ {
749
+ code: 'E_METRIC_MISMATCH',
750
+ name: '比错了对象',
751
+ detect: 'before.metric ≠ after.metric ⇒ measurement-invalid(不同度量之间没有 Δ)',
752
+ falsifiedBy: 'unit/effect-judgment.spec.mjs —— 「前后不是同一个度量 ⇒ 拒绝」',
753
+ },
754
+ {
755
+ code: 'E_METRIC_VALUE_MISSING',
756
+ name: '测量值缺失被当成 0',
757
+ detect: 'before.value / after.value 不是有限数 ⇒ measurement-invalid(缺值不得当 0)',
758
+ falsifiedBy: 'unit/effect-judgment.spec.mjs —— 「缺 value ⇒ 拒绝,不得当 0」',
759
+ },
760
+ {
761
+ code: 'E_DATASET_IDENTITY_MISSING',
762
+ name: '评估集身份缺失',
763
+ detect: 'datasetHash 缺失/为空 ⇒ measurement-invalid(eval-contract §7.3:未知 ≠ 已知完整)',
764
+ falsifiedBy: 'unit/effect-judgment.spec.mjs —— 「缺 datasetHash ⇒ 拒绝判定」',
765
+ },
766
+ ],
767
+ riskAssessment: [
768
+ {
769
+ code: 'E_RISK_UNCLASSIFIED',
770
+ name: '风险未识别',
771
+ detect: '未声明任何因子 / 出现词表外的因子 ⇒ 升到 critical 且 requiresHumanConfirmation = true(**never safe**)',
772
+ falsifiedBy: 'unit/risk-assessment.spec.mjs —— 「未归类因子 ⇒ critical + 需人工确认」',
773
+ },
774
+ {
775
+ code: 'E_RISK_UNDERESTIMATE',
776
+ name: '低估风险',
777
+ detect: 'declaredLevel 低于计算档位 ⇒ 采用计算档位并报 underestimated',
778
+ falsifiedBy: 'unit/risk-assessment.spec.mjs —— 「自评 low 但含就地变更 ⇒ 升到 high 并报低估」',
779
+ },
780
+ {
781
+ code: 'E_RISK_OVERESTIMATE',
782
+ name: '高估风险',
783
+ detect: '良性且已完全归类的方向必须落在 low 档(校准对照);实现若恒判 critical,该断言变红。declaredLevel 高于计算档位时保留调用方档位但报 overestimated',
784
+ falsifiedBy: 'unit/risk-assessment.spec.mjs —— 「只读/可逆本地 ⇒ 必须 low(不得高估)」',
785
+ },
786
+ ],
787
+ rollbackDecision: [
788
+ {
789
+ code: 'E_NO_ROLLBACK_TARGET',
790
+ name: '回退失败(无可信目标)',
791
+ detect: '没有任何"已验证良好 + 有哈希背书"的既往状态 ⇒ refused,不猜',
792
+ falsifiedBy: 'unit/rollback-decision.spec.mjs —— 「无 verifiedGood ⇒ refused」',
793
+ },
794
+ {
795
+ code: 'E_ROLLBACK_NOOP',
796
+ name: '回退失败(空操作)',
797
+ detect: '目标产物哈希与故障态相同 ⇒ 该候选不是回退目标;核验时 observed === notBefore ⇒ 判空操作',
798
+ falsifiedBy: 'unit/rollback-decision.spec.mjs —— 「只断言过程/哈希未变 ⇒ 判失败」',
799
+ },
800
+ {
801
+ code: 'E_ROLLBACK_WRONG_VERSION',
802
+ name: '回退到错误版本',
803
+ detect: 'observed 哈希等于**别的**历史状态 ⇒ verifyRollbackDecision 报错误版本',
804
+ falsifiedBy: 'unit/rollback-decision.spec.mjs —— 「回退到了另一个历史版本 ⇒ 指名报错」',
805
+ },
806
+ {
807
+ code: 'E_ROLLBACK_INCOMPLETE',
808
+ name: '回退不彻底',
809
+ detect: 'observed 缺某个必需产物的哈希 ⇒ 核验失败;候选若未覆盖故障态全部产物 ⇒ 决策期即排除',
810
+ falsifiedBy: 'unit/rollback-decision.spec.mjs —— 「只回退了一部分产物 ⇒ 判不彻底」',
811
+ },
812
+ {
813
+ code: 'E_PROCESS_ONLY_VERIFICATION',
814
+ name: '只断言过程(抓不到空操作)',
815
+ detect: 'proposedVerification 只含 process/exitCode 断言 ⇒ refused;observed 只有退出码无哈希 ⇒ 核验失败',
816
+ falsifiedBy: 'unit/rollback-decision.spec.mjs —— 「只断言过程的核验方案被拒」',
817
+ },
818
+ {
819
+ code: 'E_TARGET_NOT_VERIFIED_GOOD',
820
+ name: '把未验证状态当可信目标',
821
+ detect: 'verifiedGood !== true 或缺 verifiedBy ⇒ 不进入候选("谁证明了它 good"缺失即未知)',
822
+ falsifiedBy: 'unit/rollback-decision.spec.mjs —— 「verifiedGood 无背书 ⇒ 候选被排除」',
823
+ },
824
+ {
825
+ code: 'E_ARTIFACT_SET_MISMATCH',
826
+ name: '回退不彻底(决策期)',
827
+ detect: '候选未覆盖故障态全部产物 / 产物缺合法内容哈希 / 故障态本身无哈希 ⇒ 排除或拒绝',
828
+ falsifiedBy: 'unit/rollback-decision.spec.mjs —— 「只覆盖一部分产物 ⇒ 排除该候选」',
829
+ },
830
+ {
831
+ code: 'E_SELF_JUDGING',
832
+ name: '自证回退生效',
833
+ detect: 'verifyRollbackDecision 的 observed.producedBy === "rollbackDecision" ⇒ 核验失败',
834
+ falsifiedBy: 'unit/rollback-decision.spec.mjs —— 「用本能力的产物自证 ⇒ 拒绝」',
835
+ },
836
+ ],
837
+ }
838
+
839
+ /**
840
+ * 组装**机读能力清单**:id / version / input schema / output schema / 声明的失败模式。
841
+ * CI 只需 import 本对象即可枚举五个能力(不需要执行实现)。
842
+ */
843
+ export function buildManifest(capabilityVersion, implementations) {
844
+ return Object.freeze({
845
+ contractVersion: null, // 由 index.mjs 现场填入 CONTRACT_VERSION(避免循环 import)
846
+ capabilityVersion,
847
+ selfJudging: false,
848
+ capabilities: CAPABILITY_IDS.map((id) =>
849
+ Object.freeze({
850
+ id,
851
+ version: capabilityVersion,
852
+ implemented: typeof implementations?.[id] === 'function',
853
+ inputSchema: SCHEMAS[id].input,
854
+ outputSchema: SCHEMAS[id].output,
855
+ failureModes: FAILURE_MODES[id].map((m) => Object.freeze({ ...m })),
856
+ }),
857
+ ),
858
+ })
859
+ }
860
+
861
+ export function schemasFor(id) {
862
+ if (!CAPABILITY_IDS.includes(id)) throw new Error(`未知能力:${id}`)
863
+ return SCHEMAS[id]
864
+ }
865
+
866
+ export function failureModesFor(id) {
867
+ if (!CAPABILITY_IDS.includes(id)) throw new Error(`未知能力:${id}`)
868
+ return FAILURE_MODES[id]
869
+ }
870
+
871
+ /** 契约自检:每个能力都必须同时具备 id / 两个 schema / 实现 / 至少一条失败模式。 */
872
+ export function assertManifestComplete(manifest) {
873
+ const problems = []
874
+ if (manifest.selfJudging !== false) problems.push('selfJudging 必须为 false')
875
+ const seen = new Set()
876
+ for (const c of manifest.capabilities) {
877
+ if (seen.has(c.id)) problems.push(`能力 id 重复:${c.id}`)
878
+ seen.add(c.id)
879
+ if (!c.implemented) problems.push(`${c.id}: 缺实现`)
880
+ if (c.inputSchema?.type !== 'object') problems.push(`${c.id}: 缺 inputSchema`)
881
+ if (c.outputSchema?.type !== 'object') problems.push(`${c.id}: 缺 outputSchema`)
882
+ if (!Array.isArray(c.failureModes) || c.failureModes.length === 0) problems.push(`${c.id}: 未声明失败模式`)
883
+ for (const m of c.failureModes ?? []) {
884
+ if (!m.code || !m.name || !m.detect || !m.falsifiedBy) problems.push(`${c.id}: 失败模式 ${m.code} 声明不完整(须含 code/name/detect/falsifiedBy)`)
885
+ if (m.falsifiedBy && !m.falsifiedBy.includes('.spec')) problems.push(`${c.id}: 失败模式 ${m.code} 未指向会变红的测试`)
886
+ }
887
+ }
888
+ for (const id of CAPABILITY_IDS) if (!seen.has(id)) problems.push(`清单缺能力:${id}`)
889
+ return { ok: problems.length === 0, problems }
890
+ }