progmune-runtime 3.7.55 → 3.7.57

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.
@@ -138,6 +138,29 @@ function formatTrustTerminal(decision) {
138
138
  lines.push(`${BOX_V}${" ".repeat(w - 2)}${BOX_V}`);
139
139
  }
140
140
  }
141
+ // Ranked Safeguard Alerts — 证据流(2026-10-02,§49.15 方案 b):
142
+ // 排序后的 safeguard 告警,不进判定(decision/score 不受影响)。
143
+ // 展示分组形态的头部:组间按族先验、组内按语义分;每组最多 3 条。
144
+ const sa = overall.safeguardAlerts;
145
+ if (sa && sa.total > 0) {
146
+ lines.push(`${BOX_ML}${BOX_H.repeat(w - 2)}${BOX_MR}`);
147
+ lines.push(`${BOX_V} ${BOLD}Ranked Safeguard Alerts${RESET} (evidence stream — does not affect the decision)${BOX_V}`.slice(0, w + 1));
148
+ lines.push(`${BOX_V}${" ".repeat(w - 2)}${BOX_V}`);
149
+ lines.push(`${BOX_V} ${DIM}${sa.total} alert(s), ${sa.groups.length} rule group(s). Groups ordered by rule prior; within each group by semantic signals.${RESET}`.slice(0, w + 1));
150
+ lines.push(`${BOX_V}${" ".repeat(w - 2)}${BOX_V}`);
151
+ for (const g of sa.groups.slice(0, 8)) {
152
+ lines.push(`${BOX_V} ${CYAN}◆ ${g.rule}${RESET} ${DIM}(prior ${(g.prior * 100).toFixed(1)}%, ${g.count} alerts)${RESET}`.slice(0, w + 1));
153
+ for (const r of g.alerts.slice(0, 3)) {
154
+ const a = r.alert;
155
+ const fn = `${a.function || "?"} @ ${a.file || "?"}`;
156
+ lines.push(`${BOX_V} ${DIM}${fn.slice(0, w - 8)}${RESET}`);
157
+ if (r.reasons.length > 0) {
158
+ lines.push(`${BOX_V} ${DIM}↳ ${r.reasons.join(" · ").slice(0, w - 9)}${RESET}`);
159
+ }
160
+ }
161
+ lines.push(`${BOX_V}${" ".repeat(w - 2)}${BOX_V}`);
162
+ }
163
+ }
141
164
  // Audit Trail
142
165
  lines.push(`${BOX_ML}${BOX_H.repeat(w - 2)}${BOX_MR}`);
143
166
  lines.push(`${BOX_V} ${BOLD}Audit Trail${RESET}${" ".repeat(w - 16)}${BOX_V}`);
@@ -14,6 +14,7 @@ exports.scoreProtocolSafety = scoreProtocolSafety;
14
14
  exports.scoreVerificationCoverage = scoreVerificationCoverage;
15
15
  exports.scoreGovernanceIntegrity = scoreGovernanceIntegrity;
16
16
  exports.calculateOverallScore = calculateOverallScore;
17
+ exports.evaluateDecisionGates = evaluateDecisionGates;
17
18
  exports.determineDecision = determineDecision;
18
19
  exports.determineConfidence = determineConfidence;
19
20
  exports.countViolationsBySeverity = countViolationsBySeverity;
@@ -80,13 +81,14 @@ function scoreProtocolSafety(violations, protocolWeights) {
80
81
  totalWeight += detail.weight;
81
82
  }
82
83
  const score = totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 100;
83
- // Determine confidence for this dimension
84
- // A protocol is considered "checked" if it was evaluated (score < 100 found issues,
85
- // or score = 100 with no violations means we checked and it passed clean)
86
- const protocolsChecked = Object.values(details).filter((d) => d.score <= 100 // all protocols are always evaluated
87
- ).length;
84
+ // ── 观察度(§53 / R97):0 条违规的协议不是「查过且干净」,而是「没看到东西」 ──
85
+ // 此前这里写的是 `d.score <= 100`(字面恒真)⇒ 5/5 ⇒ 永远 HIGH。
86
+ // 后果:authentication 0 分、其余 4 个协议零观察,整维度仍报 confidence HIGH,
87
+ // 进而让 overall confidence = HIGH。改成按**真的产出过观察**的协议计数。
88
+ const observedProtocols = protocolNames.filter((n) => (byProtocol[n] || []).length > 0);
89
+ const blindProtocols = protocolNames.filter((n) => (byProtocol[n] || []).length === 0);
88
90
  const confidenceRatio = protocolNames.length > 0
89
- ? Math.min(1, protocolsChecked / protocolNames.length)
91
+ ? observedProtocols.length / protocolNames.length
90
92
  : 1;
91
93
  const confidence = mapDimensionConfidence(confidenceRatio);
92
94
  // Collect only protocol-related violations for evidence
@@ -97,6 +99,8 @@ function scoreProtocolSafety(violations, protocolWeights) {
97
99
  confidence,
98
100
  details,
99
101
  violations: protocolViolations,
102
+ observedProtocols,
103
+ blindProtocols,
100
104
  };
101
105
  }
102
106
  /**
@@ -191,6 +195,56 @@ function calculateOverallScore(dimensions) {
191
195
  // ═══════════════════════════════════════════════
192
196
  // 6. Decision Mapping
193
197
  // ═══════════════════════════════════════════════
198
+ /**
199
+ * 计算决策封顶门禁(§53 / R97)。纯函数,只依赖已算好的维度结果。
200
+ *
201
+ * 只回答两个问题:
202
+ * ① 「有没有一条**我们确实看过**的安全维度塌了」——有 ⇒ 不许说通过。
203
+ * ② 「覆盖率测出来是不是真的很低」——**仅在可测时**才问;不可测 ⇒ 不适用(R98)。
204
+ *
205
+ * ⚠ 两者都不**扣分**:没有观察就没有扣分依据(扣分也是臆造)。
206
+ * 它们只作用于 decision 上限与 confidence,这正是「ABSENCE ≠ evidence of
207
+ * absence」在聚合层应有的落地形态。
208
+ *
209
+ * ⚠ 语义映射命中率(mappingCoverage)**故意不作为门禁**:实测 9 个真实 TS 项目
210
+ * 的取值范围是 1%~21%(中位 ~9%),没有任何一个够得到 30%。拿它当门槛就等于
211
+ * 宣布「TS 项目永不放行」——那是常数不是判据,而且阈值会变成从数据里挑出来的
212
+ * 数字(R86)。它只出现在输出里供人看,不参与决策。
213
+ */
214
+ function evaluateDecisionGates(input) {
215
+ const reasons = [];
216
+ // ① 安全维度下限 —— 只对「有观察」的协议生效(0 条违规的盲区不参与,避免臆造)
217
+ const breachedProtocols = types_1.SECURITY_PROTOCOLS
218
+ .filter((p) => input.protocolSafety.observedProtocols.includes(p))
219
+ .map((p) => ({
220
+ protocol: p,
221
+ score: input.protocolSafety.details[p]?.score ?? 100,
222
+ }))
223
+ .filter((d) => d.score < types_1.SECURITY_PROTOCOL_FLOOR);
224
+ for (const b of breachedProtocols) {
225
+ reasons.push(`安全维度 ${b.protocol} 得分 ${b.score} < ${types_1.SECURITY_PROTOCOL_FLOOR}(已观察,非盲区)——不得输出 APPROVED`);
226
+ }
227
+ // ② 覆盖率门槛 —— 只在**可测**时生效
228
+ let observationIncomplete = false;
229
+ if (input.coverageLevel === "LOW") {
230
+ if (input.coverageApplicable) {
231
+ observationIncomplete = true;
232
+ reasons.push("覆盖率置信度 LOW(已测得)——本次扫描不足以支撑「通过」结论");
233
+ }
234
+ else {
235
+ // 不适用:没有 protocols.json ⇒ 0% 是「没得测」,不是「测得低」。
236
+ // 若在此处当成 LOW 处理,门禁会对所有未接入协议定义的项目 100% 触发
237
+ // ⇒ 连「0 违规、auth=100」的项目也被封顶(实测 6/6)。这是常数不是判据。
238
+ reasons.push("覆盖率不可测(项目无 protocols.json)——按「不适用」处理,不作门禁(R98)");
239
+ }
240
+ }
241
+ return {
242
+ securityFloorBreach: breachedProtocols.length > 0,
243
+ breachedProtocols,
244
+ observationIncomplete,
245
+ reasons,
246
+ };
247
+ }
194
248
  /**
195
249
  * Maps score + gates → APPROVED / NEEDS_REVIEW / BLOCKED.
196
250
  *
@@ -199,9 +253,14 @@ function calculateOverallScore(dimensions) {
199
253
  * - Score < 60 → BLOCKED
200
254
  * - 60 ≤ Score < 80 → NEEDS_REVIEW
201
255
  * - Score ≥ 80 → APPROVED
256
+ * - 安全维度下限被击穿 ⇒ 上限 NEEDS_REVIEW(§53/R97)
257
+ * - 观察度不足 ⇒ 上限 NEEDS_REVIEW(§53/R97)
202
258
  * - Explainability UNCERTAIN → degrade one level
259
+ *
260
+ * ⚠ 封顶(cap)不是降级(degrade):封顶只阻止「说通过」,不会把 BLOCKED 抬上来,
261
+ * 也不会把 NEEDS_REVIEW 打成 BLOCKED —— 避免为了修「虚高」而过冲成「虚低」。
203
262
  */
204
- function determineDecision(overallScore, hasCriticalViolation, explainabilityStatus) {
263
+ function determineDecision(overallScore, hasCriticalViolation, explainabilityStatus, gates) {
205
264
  // Hard gate: critical = BLOCKED regardless of score
206
265
  if (hasCriticalViolation) {
207
266
  return "BLOCKED";
@@ -217,6 +276,15 @@ function determineDecision(overallScore, hasCriticalViolation, explainabilitySta
217
276
  else {
218
277
  decision = "BLOCKED";
219
278
  }
279
+ // ── §53 / R97:封顶 ──
280
+ // 背景:immich(auth 0 分 / coverage 0% / mapping 8%)与 nocodb
281
+ // (auth 0 分 / 586 条 / mapping 4%)都拿到了 83 / APPROVED / HIGH。
282
+ // 「分数够高」不等于「可以说通过」——前提是我们确实看过、且看过的维度没塌。
283
+ if (gates &&
284
+ (gates.securityFloorBreach || gates.observationIncomplete) &&
285
+ decision === "APPROVED") {
286
+ decision = "NEEDS_REVIEW";
287
+ }
220
288
  // Explainability degrade: drop one level
221
289
  if (explainabilityStatus === "UNCERTAIN") {
222
290
  if (decision === "APPROVED")
@@ -239,7 +307,16 @@ function determineDecision(overallScore, hasCriticalViolation, explainabilitySta
239
307
  * - 2+ dimensions MEDIUM → overall MEDIUM
240
308
  * - All HIGH → overall HIGH
241
309
  */
242
- function determineConfidence(dimConfidences, explainabilityStatus) {
310
+ function determineConfidence(dimConfidences, explainabilityStatus,
311
+ /**
312
+ * §53 / R97:观察度不足时,置信度**封顶**为 MEDIUM。
313
+ *
314
+ * 背景:immich 的 coverageConfidence 是 0% ±25% LOW,而 overall confidence
315
+ * 仍是 HIGH —— 四个维度的 confidence 里没有一项接了覆盖率这条线。
316
+ * 代码里甚至已经写了「本次扫描是废票,不得据此得出『干净』结论」,
317
+ * 但没有任何一行消费它。**声明与实现分离**是这里真正的缺陷。
318
+ */
319
+ observationIncomplete) {
243
320
  // Explainability gate overrides everything
244
321
  if (explainabilityStatus === "UNCERTAIN") {
245
322
  return "UNCERTAIN";
@@ -252,6 +329,9 @@ function determineConfidence(dimConfidences, explainabilityStatus) {
252
329
  return "MEDIUM";
253
330
  if (mediumCount === 1)
254
331
  return "MEDIUM"; // Even one medium drags confidence
332
+ // 观察度不足 ⇒ 即便各维度都报 HIGH,也不得给出 HIGH
333
+ if (observationIncomplete)
334
+ return "MEDIUM";
255
335
  return "HIGH";
256
336
  }
257
337
  // ═══════════════════════════════════════════════
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ /**
3
+ * §53 / R97:决策门禁单测
4
+ *
5
+ * 背景(immich + nocodb 实测):authentication 子协议 0 分(21 / 586 条违规)、
6
+ * coverageConfidence 0% LOW、mappingCoverage 4%~8% LOW —— 三项独立的
7
+ * 「其实没看到什么」的指标全部触底,聚合结果却仍是 83 / APPROVED / HIGH。
8
+ *
9
+ * 本文件的重点不是「门禁会不会触发」,而是**它会不会过冲**:
10
+ * - 只封顶(不许说通过),不降级(不许把 BLOCKED 抬上来、也不许把 NEEDS_REVIEW 打成 BLOCKED)
11
+ * - 盲区(0 条违规)不得被当成「查过且干净」加分,也不得被当成「有问题」扣分
12
+ * - 没有观察 ⇒ 影响的是 confidence 与 decision 上限,不是分数
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const vitest_1 = require("vitest");
16
+ const score_calculator_1 = require("./score-calculator");
17
+ function v(ruleId, severity = "medium") {
18
+ return {
19
+ rule_id: ruleId,
20
+ severity,
21
+ file: "src/a.ts",
22
+ function: "handler",
23
+ message: "test",
24
+ evidence: "test",
25
+ why: "test",
26
+ fix: "test",
27
+ policy_ref: "protocol-safety.specific",
28
+ };
29
+ }
30
+ /** 造一个「只有 authentication 有观察」的维度(= immich 的真实形态) */
31
+ function authOnlyProtocolSafety(violationCount) {
32
+ return (0, score_calculator_1.scoreProtocolSafety)(Array.from({ length: violationCount }, () => v("AUTH_MISSING")));
33
+ }
34
+ (0, vitest_1.describe)("§53 decision gates — 观察度(protocolSafety.confidence)", () => {
35
+ (0, vitest_1.it)("0 条违规的协议是 blind,不是 checked-clean(此前恒为 HIGH)", () => {
36
+ const ps = authOnlyProtocolSafety(21);
37
+ (0, vitest_1.expect)(ps.observedProtocols).toEqual(["authentication"]);
38
+ (0, vitest_1.expect)(ps.blindProtocols.sort()).toEqual([
39
+ "authorization",
40
+ "data_integrity",
41
+ "ledger",
42
+ "payment",
43
+ ]);
44
+ // 5 个协议只有 1 个有观察 ⇒ 0.2 ⇒ LOW
45
+ (0, vitest_1.expect)(ps.confidence).toBe("LOW");
46
+ });
47
+ (0, vitest_1.it)("全盲(0 违规)时分数仍为 100,但 confidence 塌到 LOW —— 不加分也不扣分", () => {
48
+ const ps = (0, score_calculator_1.scoreProtocolSafety)([]);
49
+ (0, vitest_1.expect)(ps.score).toBe(100);
50
+ (0, vitest_1.expect)(ps.observedProtocols).toEqual([]);
51
+ (0, vitest_1.expect)(ps.blindProtocols.length).toBe(5);
52
+ (0, vitest_1.expect)(ps.confidence).toBe("LOW");
53
+ });
54
+ });
55
+ (0, vitest_1.describe)("§53 decision gates — 安全维度下限", () => {
56
+ (0, vitest_1.it)("已观察的安全协议塌方 ⇒ 封顶 NEEDS_REVIEW(且分数不动)", () => {
57
+ const ps = authOnlyProtocolSafety(21);
58
+ (0, vitest_1.expect)(ps.details.authentication.score).toBe(0);
59
+ const gates = (0, score_calculator_1.evaluateDecisionGates)({ protocolSafety: ps });
60
+ (0, vitest_1.expect)(gates.securityFloorBreach).toBe(true);
61
+ (0, vitest_1.expect)(gates.breachedProtocols).toEqual([{ protocol: "authentication", score: 0 }]);
62
+ // 83 分照样 APPROVED —— 门禁把它封住
63
+ (0, vitest_1.expect)((0, score_calculator_1.determineDecision)(83, false, "EXPLAINABLE", gates)).toBe("NEEDS_REVIEW");
64
+ });
65
+ (0, vitest_1.it)("反向:盲区不得触发下限(没有观察就没有扣分依据)", () => {
66
+ const ps = (0, score_calculator_1.scoreProtocolSafety)([]);
67
+ const gates = (0, score_calculator_1.evaluateDecisionGates)({ protocolSafety: ps });
68
+ // 全盲:分数都是 100,本来也不会 breach;关键是 breachedProtocols 必须为空
69
+ (0, vitest_1.expect)(gates.securityFloorBreach).toBe(false);
70
+ (0, vitest_1.expect)(gates.breachedProtocols).toEqual([]);
71
+ });
72
+ (0, vitest_1.it)("反向:封顶不是降级 —— 低分 BLOCKED 不会被抬成 NEEDS_REVIEW", () => {
73
+ const ps = authOnlyProtocolSafety(21);
74
+ const gates = (0, score_calculator_1.evaluateDecisionGates)({ protocolSafety: ps });
75
+ (0, vitest_1.expect)((0, score_calculator_1.determineDecision)(30, false, "EXPLAINABLE", gates)).toBe("BLOCKED");
76
+ (0, vitest_1.expect)((0, score_calculator_1.determineDecision)(65, false, "EXPLAINABLE", gates)).toBe("NEEDS_REVIEW");
77
+ });
78
+ });
79
+ (0, vitest_1.describe)("§53 decision gates — 观察度门槛", () => {
80
+ (0, vitest_1.it)("coverage LOW **且可测** ⇒ observationIncomplete ⇒ decision 封顶、confidence 封顶 MEDIUM", () => {
81
+ const ps = authOnlyProtocolSafety(1);
82
+ const gates = (0, score_calculator_1.evaluateDecisionGates)({
83
+ protocolSafety: ps,
84
+ coverageLevel: "LOW",
85
+ coverageApplicable: true,
86
+ });
87
+ (0, vitest_1.expect)(gates.observationIncomplete).toBe(true);
88
+ (0, vitest_1.expect)((0, score_calculator_1.determineDecision)(95, false, "EXPLAINABLE", gates)).toBe("NEEDS_REVIEW");
89
+ (0, vitest_1.expect)((0, score_calculator_1.determineConfidence)(["HIGH", "HIGH", "HIGH", "HIGH"], "EXPLAINABLE", true)).toBe("MEDIUM");
90
+ });
91
+ (0, vitest_1.it)("★ 反向(R98):coverage LOW 但**不可测** ⇒ 不封顶 —— 没得测 ≠ 测得低", () => {
92
+ // 实测:9 个真实 TS 项目全部没有 protocols.json ⇒ coverage 恒 0% LOW。
93
+ // 若不区分「不适用」与「真的低」,门禁 100% 触发,连「0 违规、auth=100」
94
+ // 的项目也被封顶(实测 6/6 全变 NEEDS_REVIEW)——那是常数不是判据。
95
+ const ps = (0, score_calculator_1.scoreProtocolSafety)([v("AUTH_ONE")]);
96
+ const gates = (0, score_calculator_1.evaluateDecisionGates)({
97
+ protocolSafety: ps,
98
+ coverageLevel: "LOW",
99
+ coverageApplicable: false,
100
+ });
101
+ (0, vitest_1.expect)(gates.observationIncomplete).toBe(false);
102
+ (0, vitest_1.expect)(gates.reasons.some((r) => r.includes("不适用"))).toBe(true);
103
+ (0, vitest_1.expect)((0, score_calculator_1.determineDecision)(95, false, "EXPLAINABLE", gates)).toBe("APPROVED");
104
+ });
105
+ (0, vitest_1.it)("★ 反向:0 违规且 auth=100 的干净项目不得被封顶(过冲防线)", () => {
106
+ // express-realworld / koa-realworld / netflx-web 的真实形态:
107
+ // 0 条违规、authentication=100、coverage 不可测 ⇒ 必须仍是 APPROVED。
108
+ const ps = (0, score_calculator_1.scoreProtocolSafety)([]);
109
+ const gates = (0, score_calculator_1.evaluateDecisionGates)({
110
+ protocolSafety: ps,
111
+ coverageLevel: "LOW",
112
+ coverageApplicable: false,
113
+ });
114
+ (0, vitest_1.expect)(gates.securityFloorBreach).toBe(false);
115
+ (0, vitest_1.expect)(gates.observationIncomplete).toBe(false);
116
+ (0, vitest_1.expect)((0, score_calculator_1.determineDecision)(90, false, "EXPLAINABLE", gates)).toBe("APPROVED");
117
+ });
118
+ (0, vitest_1.it)("反向:观察度充足且无塌方 ⇒ 不封顶(不许把门禁做成一律 NEEDS_REVIEW)", () => {
119
+ const ps = (0, score_calculator_1.scoreProtocolSafety)([v("AUTH_OK_LOW")]);
120
+ // 造一个 authentication 高分、且 5 个协议都有观察的形态
121
+ const allObserved = {
122
+ ...ps,
123
+ observedProtocols: ["authentication", "authorization", "payment", "data_integrity", "ledger"],
124
+ blindProtocols: [],
125
+ confidence: "HIGH",
126
+ };
127
+ const gates = (0, score_calculator_1.evaluateDecisionGates)({
128
+ protocolSafety: allObserved,
129
+ coverageLevel: "HIGH",
130
+ coverageApplicable: true,
131
+ });
132
+ (0, vitest_1.expect)(gates.securityFloorBreach).toBe(false);
133
+ (0, vitest_1.expect)(gates.observationIncomplete).toBe(false);
134
+ (0, vitest_1.expect)(gates.reasons).toEqual([]);
135
+ (0, vitest_1.expect)((0, score_calculator_1.determineDecision)(95, false, "EXPLAINABLE", gates)).toBe("APPROVED");
136
+ });
137
+ });
138
+ (0, vitest_1.describe)("§53 decision gates — 硬门优先级", () => {
139
+ (0, vitest_1.it)("critical 仍然压过一切(门禁不得削弱 BLOCKED)", () => {
140
+ const ps = authOnlyProtocolSafety(21);
141
+ const gates = (0, score_calculator_1.evaluateDecisionGates)({ protocolSafety: ps });
142
+ (0, vitest_1.expect)((0, score_calculator_1.determineDecision)(95, true, "EXPLAINABLE", gates)).toBe("BLOCKED");
143
+ });
144
+ (0, vitest_1.it)("门禁为 undefined 时行为与改动前完全一致(向后兼容)", () => {
145
+ (0, vitest_1.expect)((0, score_calculator_1.determineDecision)(95, false, "EXPLAINABLE")).toBe("APPROVED");
146
+ (0, vitest_1.expect)((0, score_calculator_1.determineDecision)(70, false, "EXPLAINABLE")).toBe("NEEDS_REVIEW");
147
+ (0, vitest_1.expect)((0, score_calculator_1.determineDecision)(30, false, "EXPLAINABLE")).toBe("BLOCKED");
148
+ });
149
+ });
@@ -8,7 +8,7 @@
8
8
  * these types during evaluation.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.DECISION_THRESHOLDS = exports.DEFAULT_GOVERNANCE_DEDUCTIONS = exports.DEFAULT_COVERAGE_MAX_SCORES = exports.DEFAULT_PROTOCOL_WEIGHTS = exports.DEFAULT_SEVERITY_DEDUCTIONS = exports.DEFAULT_DIMENSION_WEIGHTS = exports.TRUST_VIOLATION_REQUIRED_FIELDS = void 0;
11
+ exports.DECISION_THRESHOLDS = exports.DEFAULT_GOVERNANCE_DEDUCTIONS = exports.DEFAULT_COVERAGE_MAX_SCORES = exports.DEFAULT_PROTOCOL_WEIGHTS = exports.DEFAULT_SEVERITY_DEDUCTIONS = exports.DEFAULT_DIMENSION_WEIGHTS = exports.OBSERVATION_RATE_FLOOR = exports.SECURITY_PROTOCOL_FLOOR = exports.SECURITY_PROTOCOLS = exports.TRUST_VIOLATION_REQUIRED_FIELDS = void 0;
12
12
  /** The 7 required fields for explainability completeness */
13
13
  exports.TRUST_VIOLATION_REQUIRED_FIELDS = [
14
14
  "severity",
@@ -20,6 +20,22 @@ exports.TRUST_VIOLATION_REQUIRED_FIELDS = [
20
20
  "fix",
21
21
  "policy_ref",
22
22
  ];
23
+ // ── Decision Gates(§53 / R97) ──
24
+ //
25
+ // 背景(immich + nocodb 实测):authentication 子协议 0 分(21 条 / 586 条违规)、
26
+ // coverageConfidence 0% LOW、mappingCoverage 4%~8% LOW —— 三项独立的
27
+ // 「我们其实没看到什么」指标全部触底,输出却仍是 83 / APPROVED / HIGH。
28
+ // 根因是聚合层只有「分数 → 档位」一条路径,没有任何**下限**与**观察度门槛**。
29
+ /** 安全相关协议:这些维度出现塌方时,不得给出「通过」结论 */
30
+ exports.SECURITY_PROTOCOLS = [
31
+ "authentication",
32
+ "authorization",
33
+ "data_integrity",
34
+ ];
35
+ /** 安全协议分数下限:任一条已观察的安全协议低于此分 ⇒ decision 不得为 APPROVED */
36
+ exports.SECURITY_PROTOCOL_FLOOR = 50;
37
+ /** 观察度门槛:coverage / mapping 命中率低于此值 ⇒ 视为「没看清楚」 */
38
+ exports.OBSERVATION_RATE_FLOOR = 0.3;
23
39
  // ── Default Dimension Weights (from design doc) ──
24
40
  exports.DEFAULT_DIMENSION_WEIGHTS = {
25
41
  policyCompliance: 0.35,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.55",
3
+ "version": "3.7.57",
4
4
  "description": "Progmune — AI Trust Decision Engine. Verify AI-generated code before it reaches production. Outputs APPROVED / NEEDS_REVIEW / BLOCKED with evidence.",
5
5
  "files": [
6
6
  "dist/",