progmune-runtime 3.7.56 → 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.
- package/CHANGELOG.md +37 -0
- package/dist/frameworks/nestjs-detector.js +428 -161
- package/dist/frameworks/nestjs-detector.test.js +220 -0
- package/dist/protocol-detector.js +11 -11
- package/dist/sdk.js +1 -1
- package/dist/trust/alert-ranker.js +3 -1
- package/dist/trust/confidence-calculator.js +3 -1
- package/dist/trust/engine.js +20 -2
- package/dist/trust/score-calculator.js +88 -8
- package/dist/trust/score-calculator.test.js +149 -0
- package/dist/trust/types.js +17 -1
- package/package.json +1 -1
|
@@ -418,3 +418,223 @@ export class NotesController {
|
|
|
418
418
|
(0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST notes/create")).toBe(true);
|
|
419
419
|
});
|
|
420
420
|
});
|
|
421
|
+
// ═══════════════════════════════════════════════════════════
|
|
422
|
+
// §52(2026-10-03):app-level provider 注册事件
|
|
423
|
+
//
|
|
424
|
+
// immich held-out 实测倒在了第三种注册途径上:
|
|
425
|
+
// const commonMiddleware = [{ provide: APP_PIPE, useClass: ZodValidationPipe }, ...];
|
|
426
|
+
// const apiMiddleware = [FileUploadInterceptor, ...commonMiddleware, {...}];
|
|
427
|
+
// @Module({ providers: [...common, ...apiMiddleware, ...] })
|
|
428
|
+
// `APP_PIPE` 与 `@Module` 之间隔了**两跳数组 spread**,只看装饰器那一层永远
|
|
429
|
+
// 看不到 ⇒ 153 条 NO_VALIDATION 误报。修复原则是「注册事件」而非「写法」:
|
|
430
|
+
// 哪里有注册意图,哪里就是证据,不再要求它必须直连某个 @Module。
|
|
431
|
+
//
|
|
432
|
+
// 同组反向测试是硬要求(R75):压制放宽后必须证明「该报的还在报」。
|
|
433
|
+
// ═══════════════════════════════════════════════════════════
|
|
434
|
+
(0, vitest_1.describe)("nestjs-detector app-level provider 注册事件(§52)", () => {
|
|
435
|
+
const ZOD_STUB = `
|
|
436
|
+
export function createZodDto(schema: any): any { return class {} as any; }
|
|
437
|
+
export const CreateAlbumSchema = { albumName: "string" };
|
|
438
|
+
export class CreateAlbumDto extends createZodDto(CreateAlbumSchema) {}
|
|
439
|
+
export class PlainAlbumDto { albumName: string = ""; }
|
|
440
|
+
`;
|
|
441
|
+
const ALBUM_CONTROLLER = `
|
|
442
|
+
import { Body, Controller, Post } from "@nestjs/common";
|
|
443
|
+
import { CreateAlbumDto } from "./album.dto";
|
|
444
|
+
@Controller("albums")
|
|
445
|
+
export class AlbumController {
|
|
446
|
+
@Post()
|
|
447
|
+
create(@Body() dto: CreateAlbumDto) { return {}; }
|
|
448
|
+
}
|
|
449
|
+
`;
|
|
450
|
+
const APP_MODULE_SPREAD = `
|
|
451
|
+
import { Module } from "@nestjs/common";
|
|
452
|
+
import { APP_PIPE } from "@nestjs/core";
|
|
453
|
+
import { ZodValidationPipe } from "nestjs-zod";
|
|
454
|
+
import { FileUploadInterceptor } from "./middleware";
|
|
455
|
+
|
|
456
|
+
const commonMiddleware = [
|
|
457
|
+
{ provide: APP_PIPE, useClass: ZodValidationPipe },
|
|
458
|
+
];
|
|
459
|
+
const apiMiddleware = [FileUploadInterceptor, ...commonMiddleware];
|
|
460
|
+
|
|
461
|
+
@Module({ providers: [...apiMiddleware] })
|
|
462
|
+
export class AppModule {}
|
|
463
|
+
`;
|
|
464
|
+
function writeImmishApp() {
|
|
465
|
+
write("tsconfig.json", TSCONFIG);
|
|
466
|
+
write("src/album.dto.ts", ZOD_STUB);
|
|
467
|
+
write("src/album.controller.ts", ALBUM_CONTROLLER);
|
|
468
|
+
write("src/middleware.ts", `
|
|
469
|
+
export class FileUploadInterceptor {}
|
|
470
|
+
`);
|
|
471
|
+
}
|
|
472
|
+
(0, vitest_1.it)("APP_PIPE 经两跳 spread 进 @Module ⇒ 已校验 DTO 的 mutation 不报 NO_VALIDATION(immich 形态)", () => {
|
|
473
|
+
writeImmishApp();
|
|
474
|
+
write("src/app.module.ts", APP_MODULE_SPREAD);
|
|
475
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
476
|
+
(0, vitest_1.expect)(a.globalValidationPipes).toContain("ZodValidationPipe");
|
|
477
|
+
const nv = a.issues.filter((i) => i.type === "NESTJS_NO_VALIDATION");
|
|
478
|
+
(0, vitest_1.expect)(nv.map((i) => i.route)).not.toContain("POST albums");
|
|
479
|
+
});
|
|
480
|
+
(0, vitest_1.it)("反向:同一形态但摘掉 APP_PIPE 注册 ⇒ 精确转红(不是无差别压制)", () => {
|
|
481
|
+
writeImmishApp();
|
|
482
|
+
write("src/app.module.ts", `
|
|
483
|
+
import { Module } from "@nestjs/common";
|
|
484
|
+
import { FileUploadInterceptor } from "./middleware";
|
|
485
|
+
const apiMiddleware = [FileUploadInterceptor];
|
|
486
|
+
@Module({ providers: [...apiMiddleware] })
|
|
487
|
+
export class AppModule {}
|
|
488
|
+
`);
|
|
489
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
490
|
+
(0, vitest_1.expect)(a.globalValidationPipes).toEqual([]);
|
|
491
|
+
(0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST albums")).toBe(true);
|
|
492
|
+
});
|
|
493
|
+
(0, vitest_1.it)("反向:有全局管道但入参不是已校验 DTO ⇒ 仍报(保守豁免不扩散到裸类型)", () => {
|
|
494
|
+
writeImmishApp();
|
|
495
|
+
write("src/app.module.ts", APP_MODULE_SPREAD);
|
|
496
|
+
write("src/album.controller.ts", `
|
|
497
|
+
import { Body, Controller, Post } from "@nestjs/common";
|
|
498
|
+
import { PlainAlbumDto } from "./album.dto";
|
|
499
|
+
@Controller("albums")
|
|
500
|
+
export class AlbumController {
|
|
501
|
+
@Post()
|
|
502
|
+
create(@Body() dto: PlainAlbumDto) { return {}; }
|
|
503
|
+
}
|
|
504
|
+
`);
|
|
505
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
506
|
+
(0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST albums")).toBe(true);
|
|
507
|
+
});
|
|
508
|
+
(0, vitest_1.it)("反向:注释里的 APP_PIPE 不算注册事件 ⇒ 仍报(text-fallback 抗噪)", () => {
|
|
509
|
+
writeImmishApp();
|
|
510
|
+
write("src/app.module.ts", `
|
|
511
|
+
import { Module } from "@nestjs/common";
|
|
512
|
+
// 计划中:providers: [{ provide: APP_PIPE, useClass: ZodValidationPipe }]
|
|
513
|
+
// * 文档注释形态同样不应被当作注册证据
|
|
514
|
+
@Module({ providers: [] })
|
|
515
|
+
export class AppModule {}
|
|
516
|
+
`);
|
|
517
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
518
|
+
(0, vitest_1.expect)(a.globalValidationPipes).toEqual([]);
|
|
519
|
+
(0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST albums")).toBe(true);
|
|
520
|
+
});
|
|
521
|
+
(0, vitest_1.it)("@UsePipes 自定义管道名(不带 Pipe 后缀)⇒ 识别为已注册管道", () => {
|
|
522
|
+
writeImmishApp();
|
|
523
|
+
write("src/album.controller.ts", `
|
|
524
|
+
import { Body, Controller, Post, UsePipes } from "@nestjs/common";
|
|
525
|
+
import { CreateAlbumDto } from "./album.dto";
|
|
526
|
+
@Controller("albums")
|
|
527
|
+
export class AlbumController {
|
|
528
|
+
@Post()
|
|
529
|
+
@UsePipes(bodyTrimSanitizer)
|
|
530
|
+
create(@Body() dto: CreateAlbumDto) { return {}; }
|
|
531
|
+
}
|
|
532
|
+
`);
|
|
533
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
534
|
+
(0, vitest_1.expect)(a.issues.filter((i) => i.type === "NESTJS_NO_VALIDATION" && i.route === "POST albums")).toEqual([]);
|
|
535
|
+
});
|
|
536
|
+
(0, vitest_1.it)("@UseGuards(AuthGuard('JWT')) 的参数字符串不产生假认证证据(反向 Sophie)", () => {
|
|
537
|
+
write("tsconfig.json", TSCONFIG);
|
|
538
|
+
write("src/auth.guard.ts", GUARD);
|
|
539
|
+
write("src/api.controller.ts", `
|
|
540
|
+
import { Controller, Post, UseGuards } from "@nestjs/common";
|
|
541
|
+
import { JWTStrategy } from "./jwt";
|
|
542
|
+
@Controller("api")
|
|
543
|
+
export class ApiController {
|
|
544
|
+
@Post("transfer")
|
|
545
|
+
@UseGuards(JWTStrategy)
|
|
546
|
+
transfer() { return {}; }
|
|
547
|
+
}
|
|
548
|
+
`);
|
|
549
|
+
write("src/jwt.ts", `
|
|
550
|
+
export const JWT = "Bearer";
|
|
551
|
+
export class JWTStrategy { canActivate(): boolean { return false; } }
|
|
552
|
+
`);
|
|
553
|
+
write("src/app.module.ts", MODULE_EMPTY);
|
|
554
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
555
|
+
// JWTStrategy 名字里带 jwt ⇒ 认证守卫(EXPECT 有保护)
|
|
556
|
+
(0, vitest_1.expect)(a.issues.filter((i) => i.type === "NESTJS_NO_AUTH")).toEqual([]);
|
|
557
|
+
// 但若写成字符串参数,不得凭 'JWT' 这个 token 冒充守卫名
|
|
558
|
+
write("src/api.controller.ts", `
|
|
559
|
+
import { Controller, Post, UseGuards } from "@nestjs/common";
|
|
560
|
+
import { AnonymousGate } from "./gate";
|
|
561
|
+
@Controller("api")
|
|
562
|
+
export class ApiController {
|
|
563
|
+
@Post("transfer")
|
|
564
|
+
@UseGuards(AnonymousGate("JWT"))
|
|
565
|
+
transfer() { return {}; }
|
|
566
|
+
}
|
|
567
|
+
`);
|
|
568
|
+
write("src/gate.ts", `
|
|
569
|
+
export function AnonymousGate(scheme: string): any { return class { canActivate() { return true; } }; }
|
|
570
|
+
`);
|
|
571
|
+
const b = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
572
|
+
(0, vitest_1.expect)(b.issues.some((i) => i.type === "NESTJS_NO_AUTH" && i.route === "POST api/transfer")).toBe(true);
|
|
573
|
+
});
|
|
574
|
+
(0, vitest_1.it)("registrations 带可复核的 (file, line) 与发现途径", () => {
|
|
575
|
+
writeImmishApp();
|
|
576
|
+
write("src/app.module.ts", APP_MODULE_SPREAD);
|
|
577
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
578
|
+
const pipeReg = a.registrations.filter((r) => r.token === "APP_PIPE");
|
|
579
|
+
(0, vitest_1.expect)(pipeReg.length).toBeGreaterThan(0);
|
|
580
|
+
for (const r of pipeReg) {
|
|
581
|
+
// 可复核:每条注册都必须能指回某个文件的某一行
|
|
582
|
+
(0, vitest_1.expect)(r.file).toMatch(/app\.module\.ts$/);
|
|
583
|
+
(0, vitest_1.expect)(r.line).toBeGreaterThan(0);
|
|
584
|
+
(0, vitest_1.expect)(r.impl).toBe("ZodValidationPipe");
|
|
585
|
+
(0, vitest_1.expect)(["module-decorator", "module-property", "array-literal", "text-fallback"]).toContain(r.source);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
(0, vitest_1.it)("数组路径别名:展开成多条 route,公开豁免重新生效(nocodb 形态,普查 72%)", () => {
|
|
589
|
+
write("tsconfig.json", TSCONFIG);
|
|
590
|
+
write("src/app.module.ts", MODULE_EMPTY);
|
|
591
|
+
write("src/token.controller.ts", `
|
|
592
|
+
import { Controller, Post } from "@nestjs/common";
|
|
593
|
+
@Controller(["meta/bases/:id", "api/v1/db/meta/projects/:id"])
|
|
594
|
+
export class TokenController {
|
|
595
|
+
@Post(["login", "signin"])
|
|
596
|
+
login() { return {}; }
|
|
597
|
+
@Post(["api-tokens"])
|
|
598
|
+
createToken() { return {}; }
|
|
599
|
+
}
|
|
600
|
+
`);
|
|
601
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
602
|
+
const paths = a.routes.map((r) => `${r.method} ${r.path}`).sort();
|
|
603
|
+
// 2 个 controller 别名 × 3 个方法别名 = 6 条
|
|
604
|
+
(0, vitest_1.expect)(paths).toEqual([
|
|
605
|
+
"POST api/v1/db/meta/projects/:id/api-tokens",
|
|
606
|
+
"POST api/v1/db/meta/projects/:id/login",
|
|
607
|
+
"POST api/v1/db/meta/projects/:id/signin",
|
|
608
|
+
"POST meta/bases/:id/api-tokens",
|
|
609
|
+
"POST meta/bases/:id/login",
|
|
610
|
+
"POST meta/bases/:id/signin",
|
|
611
|
+
]);
|
|
612
|
+
// 旧实现下 path 是整段数组文本 ⇒ /login 豁免匹配不到 ⇒ 4 条 login/signin 全被误报
|
|
613
|
+
const noAuth = a.issues.filter((i) => i.type === "NESTJS_NO_AUTH").map((i) => i.route);
|
|
614
|
+
(0, vitest_1.expect)(noAuth.sort()).toEqual([
|
|
615
|
+
"POST api/v1/db/meta/projects/:id/api-tokens",
|
|
616
|
+
"POST meta/bases/:id/api-tokens",
|
|
617
|
+
]);
|
|
618
|
+
});
|
|
619
|
+
(0, vitest_1.it)("@Module(configVar):装饰器参数是变量时仍能读到 providers(nocodb 形态,普查 71%)", () => {
|
|
620
|
+
writeImmishApp();
|
|
621
|
+
write("src/app.module.ts", `
|
|
622
|
+
import { Module } from "@nestjs/common";
|
|
623
|
+
import { APP_PIPE } from "@nestjs/core";
|
|
624
|
+
import { ZodValidationPipe } from "nestjs-zod";
|
|
625
|
+
|
|
626
|
+
const moduleMetadata = {
|
|
627
|
+
imports: [],
|
|
628
|
+
controllers: [],
|
|
629
|
+
providers: [{ provide: APP_PIPE, useClass: ZodValidationPipe }],
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
@Module(moduleMetadata)
|
|
633
|
+
export class AppModule {}
|
|
634
|
+
`);
|
|
635
|
+
const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
|
|
636
|
+
(0, vitest_1.expect)(a.globalValidationPipes).toContain("ZodValidationPipe");
|
|
637
|
+
const nv = a.issues.filter((i) => i.type === "NESTJS_NO_VALIDATION");
|
|
638
|
+
(0, vitest_1.expect)(nv.map((i) => i.route)).not.toContain("POST albums");
|
|
639
|
+
});
|
|
640
|
+
});
|
|
@@ -1121,17 +1121,17 @@ const SAFEGUARD_RULES = [
|
|
|
1121
1121
|
conceptExpected: ["checkExpiry", "autoRenew", "certificate watch"],
|
|
1122
1122
|
},
|
|
1123
1123
|
// ── Data Integrity: Mutation without audit ──
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1124
|
+
// 2026-10-04(修冤枉第一刀):移除 "Data Mutation Without Audit Trail"。
|
|
1125
|
+
// 数据依据:fp-gold 131/131 全 FP(docmost/hedgedoc/hoppscotch/verdaccio/
|
|
1126
|
+
// 3×realworld 七仓九片);TS 盲测 gold 795 条 0 条此族;PyGoat 232 条里
|
|
1127
|
+
// 此族 8 条全 unlabeled 且样本全 FP 形态;84 条 GHSA 无一条审计轨迹类。
|
|
1128
|
+
// 根因是概念性失明(非词表问题):真实架构里审计在框架/中间件层
|
|
1129
|
+
// (ORM 拦截器/数据库触发器/日志系统),函数名里永远不会出现 audit
|
|
1130
|
+
// 字样 ⇒ 判据「mutation 函数名附近有审计词汇」在真实代码形态上永远
|
|
1131
|
+
// 不可能满足。与授权族保留的理由对照(R92):授权族在真实世界有 TP
|
|
1132
|
+
// (fr 语料 8/9 位置命中是授权类),此族在所有口径 TP=0。
|
|
1133
|
+
// 若未来有函数级审计判据(如 decorator/@Audited 标记通道),按
|
|
1134
|
+
// 「先语料后规则」重新提案。
|
|
1135
1135
|
// ═══════════════════════════════════════════════════════════════
|
|
1136
1136
|
// Governance meta-rule: Framework Version Convention Check
|
|
1137
1137
|
// Lesson from proxy.ts incident (2026-08-03)
|
package/dist/sdk.js
CHANGED
|
@@ -20,7 +20,7 @@ const risk_model_1 = require("./risk-model");
|
|
|
20
20
|
const protocol_knowledge_1 = require("./protocol-knowledge");
|
|
21
21
|
const evidence_repository_1 = require("./evidence-repository");
|
|
22
22
|
/** Runtime version — stable public identifier. Internal layers evolve underneath. */
|
|
23
|
-
exports.RUNTIME_VERSION = "3.7.
|
|
23
|
+
exports.RUNTIME_VERSION = "3.7.57";
|
|
24
24
|
function verify(filePath) {
|
|
25
25
|
const cert = (0, certify_1.certify)(filePath);
|
|
26
26
|
const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
|
|
@@ -79,7 +79,9 @@ exports.DEFAULT_RULE_PRIOR = {
|
|
|
79
79
|
"Password Hashing": 0.0525,
|
|
80
80
|
"Password Hashing (Weak)": 0.0525,
|
|
81
81
|
"Registration Without Email Verification": 0.0525,
|
|
82
|
-
|
|
82
|
+
// 2026-10-04:Data Mutation Without Audit Trail 规则已移除(fp-gold
|
|
83
|
+
// 131/131 全 FP、全口径 TP=0,概念性失明——见 protocol-detector.ts 注记),
|
|
84
|
+
// 先验条目随之删除。
|
|
83
85
|
};
|
|
84
86
|
/** 未知族的兜底先验 = 全局基线 */
|
|
85
87
|
exports.DEFAULT_BASE = 0.0984;
|
|
@@ -207,6 +207,8 @@ function computeCoverageConfidence(projectPath) {
|
|
|
207
207
|
// 1. Load protocol transition space
|
|
208
208
|
const space = loadTransitionSpace(projectPath);
|
|
209
209
|
const namespaces = [...space.perNamespace.keys()];
|
|
210
|
+
// §53:协议定义不存在 ⇒ 本指标不可测(0 分是「没得测」,不是「测得低」)
|
|
211
|
+
const applicable = fs.existsSync(path.join(projectPath, "protocols.json"));
|
|
210
212
|
// 2. Load trajectory coverage
|
|
211
213
|
const coverage = loadTrajectoryCoverage(projectPath);
|
|
212
214
|
// 3. Compute per-namespace confidence
|
|
@@ -250,7 +252,7 @@ function computeCoverageConfidence(projectPath) {
|
|
|
250
252
|
const summary = `Weighted coverage: ${score}% ±${margin}%. ` +
|
|
251
253
|
`${saturated} namespaces saturated, ${partial} partial, ${noVocab} no vocabulary. ` +
|
|
252
254
|
(noVocab > 0 ? `Top gap: add trajectories for ${noVocab} uncovered namespaces.` : "");
|
|
253
|
-
return { score, margin, level, breakdown, summary };
|
|
255
|
+
return { score, margin, level, breakdown, summary, applicable };
|
|
254
256
|
}
|
|
255
257
|
/**
|
|
256
258
|
* Clear internal caches (for testing or when corpus is updated).
|
package/dist/trust/engine.js
CHANGED
|
@@ -178,16 +178,30 @@ async function evaluateTrust(ctx) {
|
|
|
178
178
|
const effectiveScore = policyResult.hasCritical
|
|
179
179
|
? Math.min(overallScore, types_1.DECISION_THRESHOLDS.criticalLock)
|
|
180
180
|
: overallScore;
|
|
181
|
-
const decision = (0, score_calculator_1.determineDecision)(effectiveScore, policyResult.hasCritical, explainResult.status);
|
|
182
181
|
// ── Phase 1: Coverage-based confidence (replaces qualitative labels) ──
|
|
183
182
|
const coverageConfidence = (0, confidence_calculator_1.computeCoverageConfidence)(ctx.projectPath);
|
|
183
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
184
|
+
// §53 / R97:决策门禁
|
|
185
|
+
//
|
|
186
|
+
// 实测触发场景(immich / nocodb):authentication 子协议 0 分(21 / 586 条违规),
|
|
187
|
+
// coverageConfidence 0% LOW,mappingCoverage 4%~8% LOW —— 三项独立的
|
|
188
|
+
// 「其实没看到什么」的指标全部触底,聚合结果却仍是 83 / APPROVED / HIGH。
|
|
189
|
+
//
|
|
190
|
+
// 门禁只做**封顶**(不许说通过),不做扣分:没有观察就没有扣分依据。
|
|
191
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
192
|
+
const decisionGates = (0, score_calculator_1.evaluateDecisionGates)({
|
|
193
|
+
protocolSafety: protocolResult,
|
|
194
|
+
coverageLevel: coverageConfidence.level,
|
|
195
|
+
coverageApplicable: coverageConfidence.applicable,
|
|
196
|
+
});
|
|
197
|
+
const decision = (0, score_calculator_1.determineDecision)(effectiveScore, policyResult.hasCritical, explainResult.status, decisionGates);
|
|
184
198
|
const dimConfidences = [
|
|
185
199
|
policyResult.hasCritical ? "LOW" : coverageResult.confidence,
|
|
186
200
|
protocolResult.confidence,
|
|
187
201
|
coverageResult.confidence,
|
|
188
202
|
governanceResult.confidence,
|
|
189
203
|
];
|
|
190
|
-
const confidence = (0, score_calculator_1.determineConfidence)(dimConfidences, explainResult.status);
|
|
204
|
+
const confidence = (0, score_calculator_1.determineConfidence)(dimConfidences, explainResult.status, decisionGates.observationIncomplete);
|
|
191
205
|
// ═══════════════════════════════════════
|
|
192
206
|
// PHASE 5: ASSEMBLE
|
|
193
207
|
// ═══════════════════════════════════════
|
|
@@ -227,17 +241,21 @@ async function evaluateTrust(ctx) {
|
|
|
227
241
|
score: effectiveScore,
|
|
228
242
|
decision,
|
|
229
243
|
confidence,
|
|
244
|
+
decisionGates,
|
|
230
245
|
coverageConfidence: extractionWarning
|
|
231
246
|
? {
|
|
232
247
|
score: 0,
|
|
233
248
|
margin: 25,
|
|
234
249
|
level: "LOW",
|
|
250
|
+
// §53:IR 提取失败同样是「没得测」,不是「测得低」
|
|
251
|
+
applicable: false,
|
|
235
252
|
summary: `${extractionWarning}——本次扫描是废票,不得据此得出「干净」结论`,
|
|
236
253
|
}
|
|
237
254
|
: {
|
|
238
255
|
score: coverageConfidence.score,
|
|
239
256
|
margin: coverageConfidence.margin,
|
|
240
257
|
level: coverageConfidence.level,
|
|
258
|
+
applicable: coverageConfidence.applicable,
|
|
241
259
|
summary: coverageConfidence.summary,
|
|
242
260
|
},
|
|
243
261
|
extractionWarning,
|
|
@@ -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
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
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
|
-
?
|
|
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
|
+
});
|