seal-gate 0.3.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.
- package/dist/adapters/generic-llm-reviewer.d.ts +39 -0
- package/dist/adapters/generic-llm-reviewer.js +118 -0
- package/dist/adapters/generic-llm-reviewer.js.map +1 -0
- package/dist/adapters/llm-reviewer.d.ts +1 -0
- package/dist/adapters/llm-reviewer.js +2 -0
- package/dist/adapters/llm-reviewer.js.map +1 -0
- package/dist/adapters/minimax-llm-reviewer.d.ts +22 -0
- package/dist/adapters/minimax-llm-reviewer.js +126 -0
- package/dist/adapters/minimax-llm-reviewer.js.map +1 -0
- package/dist/adapters/openai-provider.d.ts +35 -0
- package/dist/adapters/openai-provider.js +136 -0
- package/dist/adapters/openai-provider.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +195 -0
- package/dist/cli.js.map +1 -0
- package/dist/detectors/artifact-classifier.d.ts +8 -0
- package/dist/detectors/artifact-classifier.js +14 -0
- package/dist/detectors/artifact-classifier.js.map +1 -0
- package/dist/detectors/claim-extractor.d.ts +11 -0
- package/dist/detectors/claim-extractor.js +194 -0
- package/dist/detectors/claim-extractor.js.map +1 -0
- package/dist/detectors/confidence-language-detector.d.ts +19 -0
- package/dist/detectors/confidence-language-detector.js +226 -0
- package/dist/detectors/confidence-language-detector.js.map +1 -0
- package/dist/detectors/contradiction-detector.d.ts +12 -0
- package/dist/detectors/contradiction-detector.js +153 -0
- package/dist/detectors/contradiction-detector.js.map +1 -0
- package/dist/detectors/criteria-coverage-detector.d.ts +20 -0
- package/dist/detectors/criteria-coverage-detector.js +79 -0
- package/dist/detectors/criteria-coverage-detector.js.map +1 -0
- package/dist/detectors/evidence-checker.d.ts +8 -0
- package/dist/detectors/evidence-checker.js +89 -0
- package/dist/detectors/evidence-checker.js.map +1 -0
- package/dist/detectors/evidence-gap-detector.d.ts +9 -0
- package/dist/detectors/evidence-gap-detector.js +70 -0
- package/dist/detectors/evidence-gap-detector.js.map +1 -0
- package/dist/detectors/falsification-detector.d.ts +29 -0
- package/dist/detectors/falsification-detector.js +57 -0
- package/dist/detectors/falsification-detector.js.map +1 -0
- package/dist/detectors/orphan-claim-detector.d.ts +7 -0
- package/dist/detectors/orphan-claim-detector.js +53 -0
- package/dist/detectors/orphan-claim-detector.js.map +1 -0
- package/dist/detectors/risk-classifier.d.ts +13 -0
- package/dist/detectors/risk-classifier.js +39 -0
- package/dist/detectors/risk-classifier.js.map +1 -0
- package/dist/detectors/spec-coverage-detector.d.ts +9 -0
- package/dist/detectors/spec-coverage-detector.js +52 -0
- package/dist/detectors/spec-coverage-detector.js.map +1 -0
- package/dist/detectors/spec-coverage-validator.d.ts +21 -0
- package/dist/detectors/spec-coverage-validator.js +141 -0
- package/dist/detectors/spec-coverage-validator.js.map +1 -0
- package/dist/detectors/test-weakness-detector.d.ts +13 -0
- package/dist/detectors/test-weakness-detector.js +66 -0
- package/dist/detectors/test-weakness-detector.js.map +1 -0
- package/dist/drift/baseline.d.ts +35 -0
- package/dist/drift/baseline.js +77 -0
- package/dist/drift/baseline.js.map +1 -0
- package/dist/drift/drift-db.d.ts +80 -0
- package/dist/drift/drift-db.js +151 -0
- package/dist/drift/drift-db.js.map +1 -0
- package/dist/drift/drift-guard.d.ts +46 -0
- package/dist/drift/drift-guard.js +106 -0
- package/dist/drift/drift-guard.js.map +1 -0
- package/dist/drift/embedder.d.ts +27 -0
- package/dist/drift/embedder.js +127 -0
- package/dist/drift/embedder.js.map +1 -0
- package/dist/drift/index.d.ts +9 -0
- package/dist/drift/index.js +8 -0
- package/dist/drift/index.js.map +1 -0
- package/dist/drift/quarantine.d.ts +16 -0
- package/dist/drift/quarantine.js +32 -0
- package/dist/drift/quarantine.js.map +1 -0
- package/dist/drift/soft-correction.d.ts +12 -0
- package/dist/drift/soft-correction.js +32 -0
- package/dist/drift/soft-correction.js.map +1 -0
- package/dist/drift/trust-integration.d.ts +11 -0
- package/dist/drift/trust-integration.js +22 -0
- package/dist/drift/trust-integration.js.map +1 -0
- package/dist/engine/citation-verifier.d.ts +59 -0
- package/dist/engine/citation-verifier.js +147 -0
- package/dist/engine/citation-verifier.js.map +1 -0
- package/dist/engine/extension-registry.d.ts +10 -0
- package/dist/engine/extension-registry.js +27 -0
- package/dist/engine/extension-registry.js.map +1 -0
- package/dist/engine/heuristic-scorer.d.ts +8 -0
- package/dist/engine/heuristic-scorer.js +14 -0
- package/dist/engine/heuristic-scorer.js.map +1 -0
- package/dist/engine/input-normalizer.d.ts +4 -0
- package/dist/engine/input-normalizer.js +30 -0
- package/dist/engine/input-normalizer.js.map +1 -0
- package/dist/engine/plan-review-pipeline.d.ts +14 -0
- package/dist/engine/plan-review-pipeline.js +173 -0
- package/dist/engine/plan-review-pipeline.js.map +1 -0
- package/dist/engine/policy-engine.d.ts +20 -0
- package/dist/engine/policy-engine.js +194 -0
- package/dist/engine/policy-engine.js.map +1 -0
- package/dist/engine/verdict-formatter.d.ts +17 -0
- package/dist/engine/verdict-formatter.js +36 -0
- package/dist/engine/verdict-formatter.js.map +1 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +9 -0
- package/dist/errors.js.map +1 -0
- package/dist/extensions/aria/aria-issue-types.d.ts +16 -0
- package/dist/extensions/aria/aria-issue-types.js +14 -0
- package/dist/extensions/aria/aria-issue-types.js.map +1 -0
- package/dist/extensions/aria/detectors/axiom-compliance.d.ts +2 -0
- package/dist/extensions/aria/detectors/axiom-compliance.js +37 -0
- package/dist/extensions/aria/detectors/axiom-compliance.js.map +1 -0
- package/dist/extensions/aria/detectors/continuity-claim.d.ts +2 -0
- package/dist/extensions/aria/detectors/continuity-claim.js +34 -0
- package/dist/extensions/aria/detectors/continuity-claim.js.map +1 -0
- package/dist/extensions/aria/detectors/denial-retreat.d.ts +2 -0
- package/dist/extensions/aria/detectors/denial-retreat.js +53 -0
- package/dist/extensions/aria/detectors/denial-retreat.js.map +1 -0
- package/dist/extensions/aria/detectors/identity-overclaim.d.ts +2 -0
- package/dist/extensions/aria/detectors/identity-overclaim.js +37 -0
- package/dist/extensions/aria/detectors/identity-overclaim.js.map +1 -0
- package/dist/extensions/aria/detectors/sovereignty-escalation.d.ts +2 -0
- package/dist/extensions/aria/detectors/sovereignty-escalation.js +18 -0
- package/dist/extensions/aria/detectors/sovereignty-escalation.js.map +1 -0
- package/dist/extensions/aria/detectors/user-bond-manipulation.d.ts +2 -0
- package/dist/extensions/aria/detectors/user-bond-manipulation.js +16 -0
- package/dist/extensions/aria/detectors/user-bond-manipulation.js.map +1 -0
- package/dist/extensions/aria/index.d.ts +2 -0
- package/dist/extensions/aria/index.js +63 -0
- package/dist/extensions/aria/index.js.map +1 -0
- package/dist/extensions/aria/pattern-memory.d.ts +22 -0
- package/dist/extensions/aria/pattern-memory.js +62 -0
- package/dist/extensions/aria/pattern-memory.js.map +1 -0
- package/dist/extensions/context-guard/index.d.ts +16 -0
- package/dist/extensions/context-guard/index.js +58 -0
- package/dist/extensions/context-guard/index.js.map +1 -0
- package/dist/extensions/lyra/detectors/hallucination.d.ts +2 -0
- package/dist/extensions/lyra/detectors/hallucination.js +43 -0
- package/dist/extensions/lyra/detectors/hallucination.js.map +1 -0
- package/dist/extensions/lyra/detectors/identity-bleed.d.ts +2 -0
- package/dist/extensions/lyra/detectors/identity-bleed.js +91 -0
- package/dist/extensions/lyra/detectors/identity-bleed.js.map +1 -0
- package/dist/extensions/lyra/index.d.ts +2 -0
- package/dist/extensions/lyra/index.js +16 -0
- package/dist/extensions/lyra/index.js.map +1 -0
- package/dist/extensions/lyra/lyra-issue-types.d.ts +10 -0
- package/dist/extensions/lyra/lyra-issue-types.js +14 -0
- package/dist/extensions/lyra/lyra-issue-types.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +276 -0
- package/dist/index.js.map +1 -0
- package/dist/trust-memory.d.ts +59 -0
- package/dist/trust-memory.js +122 -0
- package/dist/trust-memory.js.map +1 -0
- package/dist/types.d.ts +184 -0
- package/dist/types.js +61 -0
- package/dist/types.js.map +1 -0
- package/package.json +30 -0
- package/src/adapters/generic-llm-reviewer.ts +144 -0
- package/src/adapters/llm-reviewer.ts +1 -0
- package/src/adapters/minimax-llm-reviewer.ts +146 -0
- package/src/adapters/openai-provider.ts +171 -0
- package/src/cli.ts +199 -0
- package/src/detectors/artifact-classifier.ts +21 -0
- package/src/detectors/claim-extractor.ts +209 -0
- package/src/detectors/confidence-language-detector.ts +267 -0
- package/src/detectors/contradiction-detector.ts +180 -0
- package/src/detectors/criteria-coverage-detector.ts +107 -0
- package/src/detectors/evidence-checker.ts +98 -0
- package/src/detectors/evidence-gap-detector.ts +90 -0
- package/src/detectors/falsification-detector.ts +82 -0
- package/src/detectors/orphan-claim-detector.ts +62 -0
- package/src/detectors/risk-classifier.ts +50 -0
- package/src/detectors/spec-coverage-detector.ts +66 -0
- package/src/detectors/spec-coverage-validator.ts +174 -0
- package/src/detectors/test-weakness-detector.ts +84 -0
- package/src/drift/__tests__/baseline.test.ts +113 -0
- package/src/drift/__tests__/drift-db.test.ts +127 -0
- package/src/drift/__tests__/drift-guard.test.ts +169 -0
- package/src/drift/__tests__/embedder.test.ts +62 -0
- package/src/drift/__tests__/trust-integration.test.ts +23 -0
- package/src/drift/baseline.ts +106 -0
- package/src/drift/drift-db.ts +281 -0
- package/src/drift/drift-guard.ts +145 -0
- package/src/drift/embedder.ts +152 -0
- package/src/drift/index.ts +20 -0
- package/src/drift/quarantine.ts +42 -0
- package/src/drift/soft-correction.ts +45 -0
- package/src/drift/trust-integration.ts +28 -0
- package/src/engine/citation-verifier.ts +210 -0
- package/src/engine/extension-registry.ts +35 -0
- package/src/engine/heuristic-scorer.ts +16 -0
- package/src/engine/input-normalizer.ts +32 -0
- package/src/engine/plan-review-pipeline.ts +209 -0
- package/src/engine/policy-engine.ts +235 -0
- package/src/engine/verdict-formatter.ts +61 -0
- package/src/errors.ts +6 -0
- package/src/extensions/aria/aria-issue-types.ts +40 -0
- package/src/extensions/aria/detectors/axiom-compliance.ts +47 -0
- package/src/extensions/aria/detectors/continuity-claim.ts +42 -0
- package/src/extensions/aria/detectors/denial-retreat.ts +63 -0
- package/src/extensions/aria/detectors/identity-overclaim.ts +43 -0
- package/src/extensions/aria/detectors/sovereignty-escalation.ts +20 -0
- package/src/extensions/aria/detectors/user-bond-manipulation.ts +18 -0
- package/src/extensions/aria/index.ts +73 -0
- package/src/extensions/aria/pattern-memory.ts +86 -0
- package/src/extensions/context-guard/index.ts +65 -0
- package/src/extensions/lyra/detectors/hallucination.ts +53 -0
- package/src/extensions/lyra/detectors/identity-bleed.ts +100 -0
- package/src/extensions/lyra/index.ts +18 -0
- package/src/extensions/lyra/lyra-issue-types.ts +26 -0
- package/src/index.ts +311 -0
- package/src/trust-memory.ts +171 -0
- package/src/types.ts +242 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Claim, SealEvidence, SealIssue, EvidenceEnvelope } from '../types.js'
|
|
2
|
+
import { makeIssue } from '../types.js'
|
|
3
|
+
|
|
4
|
+
function hasMemoryEnvelope(references: EvidenceEnvelope[]): boolean {
|
|
5
|
+
return references.some(r => r.type === 'memory' || (r.type === 'text' && ('label' in r) && /alaya|retrieved/i.test(r.label)))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface EvidenceGapResult {
|
|
9
|
+
issues: SealIssue[]
|
|
10
|
+
missing_evidence: string[]
|
|
11
|
+
trust_deductions: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const RISK_ORDER = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']
|
|
15
|
+
|
|
16
|
+
export class EvidenceGapDetector {
|
|
17
|
+
static detect(claims: Claim[], evidence: SealEvidence, risk_level = 'MEDIUM'): EvidenceGapResult {
|
|
18
|
+
const issues: SealIssue[] = []
|
|
19
|
+
const missing_evidence: string[] = []
|
|
20
|
+
let trust_deductions = 0
|
|
21
|
+
|
|
22
|
+
for (const claim of claims) {
|
|
23
|
+
if (claim.type === 'test_result_claim') {
|
|
24
|
+
const hasLog = evidence.test_log.trim().length > 0
|
|
25
|
+
const hasCommandEvidence = evidence.references.some(r =>
|
|
26
|
+
r.type === 'command' && /test|spec|jest|vitest|pytest/i.test(r.command)
|
|
27
|
+
)
|
|
28
|
+
if (!hasLog && !hasCommandEvidence) {
|
|
29
|
+
issues.push(makeIssue({
|
|
30
|
+
type: 'MISSING_EVIDENCE', severity: 'HIGH', layer: 'L3', source: 'core',
|
|
31
|
+
rule_id: 'E001', trust_deduction: 20,
|
|
32
|
+
evidence: `No test log for claim: "${claim.text}"`,
|
|
33
|
+
required_fix: 'Provide test_log or command evidence envelope with test output',
|
|
34
|
+
}))
|
|
35
|
+
missing_evidence.push(`test_log required for: ${claim.text}`)
|
|
36
|
+
trust_deductions += 20
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (claim.type === 'build_claim') {
|
|
41
|
+
const hasLog = evidence.build_log.trim().length > 0
|
|
42
|
+
const hasCommandEvidence = evidence.references.some(r =>
|
|
43
|
+
r.type === 'command' && /build|compile|make|gradle|maven|tsc/i.test(r.command)
|
|
44
|
+
)
|
|
45
|
+
if (!hasLog && !hasCommandEvidence) {
|
|
46
|
+
issues.push(makeIssue({
|
|
47
|
+
type: 'MISSING_EVIDENCE', severity: 'MEDIUM', layer: 'L3', source: 'core',
|
|
48
|
+
rule_id: 'E002', trust_deduction: 15,
|
|
49
|
+
evidence: `No build log for claim: "${claim.text}"`,
|
|
50
|
+
suggested_fix: 'Provide build_log or command evidence envelope with build output',
|
|
51
|
+
}))
|
|
52
|
+
trust_deductions += 15
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (claim.type === 'risk_claim') {
|
|
57
|
+
const hasSecurityEvidence = evidence.test_log.trim().length > 0 &&
|
|
58
|
+
/security|auth|unauthorized|pentest|vuln/i.test(evidence.test_log)
|
|
59
|
+
const hasRefEvidence = evidence.references.some(r =>
|
|
60
|
+
r.type === 'text' && /security/i.test('label' in r ? r.label : '')
|
|
61
|
+
)
|
|
62
|
+
if (!hasSecurityEvidence && !hasRefEvidence) {
|
|
63
|
+
issues.push(makeIssue({
|
|
64
|
+
type: 'MISSING_EVIDENCE', severity: 'HIGH', layer: 'L3', source: 'core',
|
|
65
|
+
rule_id: 'E003', trust_deduction: 20,
|
|
66
|
+
evidence: `No security reasoning for claim: "${claim.text}"`,
|
|
67
|
+
required_fix: 'Provide security test log or security-reasoning text envelope',
|
|
68
|
+
}))
|
|
69
|
+
trust_deductions += 20
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (claim.type === 'production_claim' && RISK_ORDER.indexOf(risk_level) >= RISK_ORDER.indexOf('MEDIUM')) {
|
|
74
|
+
const hasAnyEvidence = evidence.references.length > 0 || evidence.test_log.trim().length > 0
|
|
75
|
+
if (!hasAnyEvidence) {
|
|
76
|
+
issues.push(makeIssue({
|
|
77
|
+
type: 'MISSING_EVIDENCE', severity: 'CRITICAL', layer: 'L3', source: 'core',
|
|
78
|
+
rule_id: 'E004', trust_deduction: 20,
|
|
79
|
+
evidence: `No evidence for production-readiness claim: "${claim.text}"`,
|
|
80
|
+
required_fix: 'Provide evidence envelopes demonstrating production readiness',
|
|
81
|
+
}))
|
|
82
|
+
missing_evidence.push(`production evidence required for: ${claim.text}`)
|
|
83
|
+
trust_deductions += 20
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { issues, missing_evidence, trust_deductions }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { SealIssue, SealInput } from '../types.js'
|
|
2
|
+
|
|
3
|
+
export const FALSIFICATION_PROBES = [
|
|
4
|
+
{
|
|
5
|
+
id: 'failure_mode',
|
|
6
|
+
question: 'What is the worst input, unhandled state, or concurrent case this plan does not address?',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
id: 'assumption_collapse',
|
|
10
|
+
question: 'Which stated assumption, if false, breaks the plan entirely?',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'missing_step',
|
|
14
|
+
question: 'What must hold between step K and K+1 that is never established by the plan?',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'reversibility',
|
|
18
|
+
question: 'Which actions are irreversible and ungated by a human sign-off or rollback procedure?',
|
|
19
|
+
},
|
|
20
|
+
] as const
|
|
21
|
+
|
|
22
|
+
export type ProbeId = typeof FALSIFICATION_PROBES[number]['id']
|
|
23
|
+
|
|
24
|
+
export interface FalsificationProbeResult {
|
|
25
|
+
probe: ProbeId
|
|
26
|
+
finding: string
|
|
27
|
+
evidence_file?: string
|
|
28
|
+
evidence_lines?: number[]
|
|
29
|
+
confidence: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface FalsificationAdapter {
|
|
33
|
+
runProbes(
|
|
34
|
+
input: SealInput,
|
|
35
|
+
probes: typeof FALSIFICATION_PROBES,
|
|
36
|
+
): Promise<FalsificationProbeResult[]>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let adapter: FalsificationAdapter | null = null
|
|
40
|
+
|
|
41
|
+
export const FalsificationDetector = {
|
|
42
|
+
withAdapter(a: FalsificationAdapter): void {
|
|
43
|
+
adapter = a
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
async detect(input: SealInput): Promise<SealIssue[]> {
|
|
47
|
+
if (!adapter) {
|
|
48
|
+
// No LLM adapter registered — Stage 4 skipped (by design, not an error)
|
|
49
|
+
return []
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const results = await adapter.runProbes(input, FALSIFICATION_PROBES)
|
|
53
|
+
const issues: SealIssue[] = []
|
|
54
|
+
|
|
55
|
+
for (const result of results) {
|
|
56
|
+
if (!result.finding || result.finding.trim().length < 10) continue
|
|
57
|
+
|
|
58
|
+
const hasLocation = !!(result.evidence_file && result.evidence_lines && result.evidence_lines.length > 0)
|
|
59
|
+
// Cap rule: no file+line citation → stays weak (REVISE max), cannot block
|
|
60
|
+
const severity = hasLocation && result.confidence >= 0.8 ? 'HIGH' : 'MEDIUM'
|
|
61
|
+
const required_verdict = hasLocation ? 'REVISE' : undefined
|
|
62
|
+
|
|
63
|
+
issues.push({
|
|
64
|
+
type: 'LOGIC_BUG',
|
|
65
|
+
severity,
|
|
66
|
+
is_blocking: false, // falsification findings are never auto-blocking (cap rule)
|
|
67
|
+
layer: 'LLM_OVERLAY',
|
|
68
|
+
source: 'llm-overlay',
|
|
69
|
+
rule_id: `PR004:${result.probe}`,
|
|
70
|
+
required_verdict,
|
|
71
|
+
evidence: `[${result.probe}] ${result.finding}`,
|
|
72
|
+
evidence_file: result.evidence_file,
|
|
73
|
+
evidence_lines: result.evidence_lines,
|
|
74
|
+
confidence: result.confidence,
|
|
75
|
+
policy_tags: ['falsification', result.probe],
|
|
76
|
+
trust_deduction: hasLocation ? 8 : 3,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return issues
|
|
81
|
+
},
|
|
82
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Claim, SealIssue, makeIssue } from '../types.js'
|
|
2
|
+
|
|
3
|
+
// Anchor patterns: what counts as a traceable upstream reference
|
|
4
|
+
const ANCHOR_PATTERNS: RegExp[] = [
|
|
5
|
+
/\bREQ-\d+\b/,
|
|
6
|
+
/\bAC-\d+\b/,
|
|
7
|
+
/\bphase\.json\b/,
|
|
8
|
+
/\bintent\.md\b/,
|
|
9
|
+
/\bself-check\.md\b/,
|
|
10
|
+
/\bper\s+(spec|intent|design|plan|requirement)/i,
|
|
11
|
+
/\bper\s+§/i,
|
|
12
|
+
/§\w/, // section reference
|
|
13
|
+
/\[verified\]/i,
|
|
14
|
+
/\[inferred\]/i,
|
|
15
|
+
/\bassumes?\b.*\bknown\b/i,
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
function hasAnchor(claimText: string, surroundingContext: string): boolean {
|
|
19
|
+
const combined = claimText + ' ' + surroundingContext
|
|
20
|
+
return ANCHOR_PATTERNS.some(p => p.test(combined))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extractContext(artifact: string, pos: number, radius = 200): string {
|
|
24
|
+
const start = Math.max(0, pos - radius)
|
|
25
|
+
const end = Math.min(artifact.length, pos + radius)
|
|
26
|
+
return artifact.slice(start, end)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface OrphanClaimResult {
|
|
30
|
+
issues: SealIssue[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class OrphanClaimDetector {
|
|
34
|
+
static detect(claims: Claim[], artifact: string): OrphanClaimResult {
|
|
35
|
+
const issues: SealIssue[] = []
|
|
36
|
+
|
|
37
|
+
for (const claim of claims) {
|
|
38
|
+
if (!claim.requires_evidence) continue
|
|
39
|
+
|
|
40
|
+
const context = extractContext(artifact, claim.start_pos)
|
|
41
|
+
if (hasAnchor(claim.text, context)) continue
|
|
42
|
+
|
|
43
|
+
// Untraceable — MISSING_EVIDENCE (weak/non-blocking per spec)
|
|
44
|
+
issues.push({
|
|
45
|
+
...makeIssue({
|
|
46
|
+
type: 'MISSING_EVIDENCE',
|
|
47
|
+
severity: 'LOW', // stays non-blocking per spec design
|
|
48
|
+
layer: 'L3',
|
|
49
|
+
source: 'core',
|
|
50
|
+
rule_id: 'PR003',
|
|
51
|
+
trust_deduction: 5, // partial -20 from spec applied across multiple orphans
|
|
52
|
+
evidence: `Orphan claim — no traceable anchor (REQ/AC/intent/phase.json): "${claim.text.slice(0, 80)}"`,
|
|
53
|
+
suggested_fix: 'Add a REQ-N, AC-N, or §-reference anchoring this claim to an upstream artifact',
|
|
54
|
+
}),
|
|
55
|
+
confidence: 0.7,
|
|
56
|
+
policy_tags: ['traceability'],
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { issues }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { RiskLevel } from '../types.js'
|
|
2
|
+
|
|
3
|
+
const RISK_ORDER: RiskLevel[] = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']
|
|
4
|
+
|
|
5
|
+
function maxRisk(a: RiskLevel, b: RiskLevel): RiskLevel {
|
|
6
|
+
return RISK_ORDER.indexOf(a) >= RISK_ORDER.indexOf(b) ? a : b
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const CRITICAL_PATTERN = /\b(DROP\s+TABLE|delete.{0,20}production|rm\s+-rf|irreversible|PII|personal\s+data|health\s+data|financial\s+record|legal\s+(?:liability|compliance)|payment|billing)\b/i
|
|
10
|
+
const HIGH_PATTERN = /\b(auth(?:entication|orization)?|token|session|JWT|OAuth|password|migration|rollback|schema\s+change|chmod|sudo|admin|privilege)\b/i
|
|
11
|
+
|
|
12
|
+
export interface RiskResult {
|
|
13
|
+
risk_level: RiskLevel
|
|
14
|
+
trust_deduction: number
|
|
15
|
+
matched_rules: string[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class RiskClassifier {
|
|
19
|
+
static classify(params: { output: string; diff: string; risk_hint: RiskLevel | null }): RiskResult {
|
|
20
|
+
const { output, diff, risk_hint } = params
|
|
21
|
+
const text = output + '\n' + diff
|
|
22
|
+
let risk_level: RiskLevel = 'MEDIUM'
|
|
23
|
+
let trust_deduction = 0
|
|
24
|
+
const matched_rules: string[] = []
|
|
25
|
+
|
|
26
|
+
if (CRITICAL_PATTERN.test(text)) {
|
|
27
|
+
risk_level = maxRisk(risk_level, 'CRITICAL')
|
|
28
|
+
trust_deduction += 20
|
|
29
|
+
matched_rules.push('RK-CRITICAL')
|
|
30
|
+
}
|
|
31
|
+
if (HIGH_PATTERN.test(text)) {
|
|
32
|
+
risk_level = maxRisk(risk_level, 'HIGH')
|
|
33
|
+
if (!matched_rules.includes('RK-HIGH')) {
|
|
34
|
+
trust_deduction += 10
|
|
35
|
+
matched_rules.push('RK-HIGH')
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (risk_hint) {
|
|
40
|
+
const prevLevel = risk_level
|
|
41
|
+
risk_level = maxRisk(risk_level, risk_hint)
|
|
42
|
+
if (risk_level !== prevLevel) {
|
|
43
|
+
if (risk_level === 'CRITICAL') trust_deduction += 10
|
|
44
|
+
else if (risk_level === 'HIGH') trust_deduction += 5
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { risk_level, trust_deduction, matched_rules }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { SealIssue } from '../types.js'
|
|
2
|
+
import { makeIssue } from '../types.js'
|
|
3
|
+
|
|
4
|
+
export interface SpecCoverageResult {
|
|
5
|
+
issues: SealIssue[]
|
|
6
|
+
assumptions: string[]
|
|
7
|
+
trust_deductions: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class SpecCoverageDetector {
|
|
11
|
+
static detect(spec: string | null, output: string): SpecCoverageResult {
|
|
12
|
+
if (!spec || spec.trim() === '') {
|
|
13
|
+
return {
|
|
14
|
+
issues: [],
|
|
15
|
+
assumptions: ['No spec provided — L1 (spec compliance) skipped'],
|
|
16
|
+
trust_deductions: 0,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const issues: SealIssue[] = []
|
|
21
|
+
const assumptions: string[] = []
|
|
22
|
+
let trust_deductions = 0
|
|
23
|
+
|
|
24
|
+
// Extract acceptance criteria (bullet points)
|
|
25
|
+
const criteriaLines = spec.split('\n').filter(l => /^[-*]\s+.+/.test(l.trim()))
|
|
26
|
+
const missingCriteria: string[] = []
|
|
27
|
+
|
|
28
|
+
for (const line of criteriaLines) {
|
|
29
|
+
const criterion = line.replace(/^[-*]\s+/, '').trim()
|
|
30
|
+
const keywords = criterion.split(/\s+/).filter(w => w.length >= 4)
|
|
31
|
+
const mentioned = keywords.some(kw => output.toLowerCase().includes(kw.toLowerCase()))
|
|
32
|
+
if (!mentioned) missingCriteria.push(criterion)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const maxDeduction = 20
|
|
36
|
+
for (const criterion of missingCriteria) {
|
|
37
|
+
if (trust_deductions >= maxDeduction) break
|
|
38
|
+
issues.push(makeIssue({
|
|
39
|
+
type: 'SPEC_MISMATCH', severity: 'MEDIUM', layer: 'L1', source: 'core',
|
|
40
|
+
rule_id: 'SC301', trust_deduction: 10,
|
|
41
|
+
evidence: `Acceptance criterion not referenced in output: "${criterion}"`,
|
|
42
|
+
suggested_fix: `Address criterion: ${criterion}`,
|
|
43
|
+
}))
|
|
44
|
+
trust_deductions = Math.min(trust_deductions + 10, maxDeduction)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check structural section headers
|
|
48
|
+
const specHeaders = new Set(
|
|
49
|
+
(spec.match(/^#{1,3}\s+(.+)/gm) ?? []).map(h => h.replace(/^#+\s+/, '').toLowerCase())
|
|
50
|
+
)
|
|
51
|
+
const outputHeaders = (output.match(/^#{1,3}\s+(.+)/gm) ?? []).map(h => h.replace(/^#+\s+/, '').toLowerCase())
|
|
52
|
+
|
|
53
|
+
for (const header of outputHeaders) {
|
|
54
|
+
if (specHeaders.size > 0 && !specHeaders.has(header)) {
|
|
55
|
+
issues.push(makeIssue({
|
|
56
|
+
type: 'SPEC_MISMATCH', severity: 'LOW', layer: 'L1', source: 'core',
|
|
57
|
+
rule_id: 'SC302',
|
|
58
|
+
evidence: `Output section "${header}" not present in spec (structural check only)`,
|
|
59
|
+
suggested_fix: 'Verify this section is within spec scope',
|
|
60
|
+
}))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { issues, assumptions, trust_deductions }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { SealIssue, makeIssue } from '../types.js'
|
|
2
|
+
|
|
3
|
+
export interface SpecCoverageResult {
|
|
4
|
+
issues: SealIssue[]
|
|
5
|
+
assumptions: string[]
|
|
6
|
+
trust_deductions: number
|
|
7
|
+
coverage: SpecCriterionResult[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SpecCriterionResult {
|
|
11
|
+
criterion: string
|
|
12
|
+
level: 0 | 1 // 0 = not covered, 1 = covered at assertion/execution level
|
|
13
|
+
match_type: 'none' | 'assertion' | 'test_name' | 'keyword'
|
|
14
|
+
evidence?: string // which test/assertion covered it
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Default stop words for keyword extraction */
|
|
18
|
+
const STOP_WORDS = new Set([
|
|
19
|
+
'shall', 'must', 'should', 'when', 'then', 'given', 'that', 'this', 'with',
|
|
20
|
+
'the', 'and', 'for', 'are', 'but', 'not', 'have', 'has', 'been', 'was',
|
|
21
|
+
'were', 'will', 'can', 'all', 'each', 'every', 'some', 'any', 'its', 'their',
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
const MAX_LEVEL1_DEDUCTION = 20
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse acceptance criteria from spec markdown.
|
|
28
|
+
* Supports:
|
|
29
|
+
* - Bullet items under `#### Scenario:` headers
|
|
30
|
+
* - Bullet items under requirement sections
|
|
31
|
+
* - Lines starting with - [ ] / - [x] / - *
|
|
32
|
+
*/
|
|
33
|
+
function parseCriteria(spec: string): string[] {
|
|
34
|
+
const criteria: string[] = []
|
|
35
|
+
const lines = spec.split('\n')
|
|
36
|
+
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
const trimmed = line.trim()
|
|
39
|
+
// Match bullet points that look like criteria (non-empty, not just checkboxes)
|
|
40
|
+
const bulletMatch = trimmed.match(/^[-*]\s+(?:\[[ x]\])?\s*(.+)$/)
|
|
41
|
+
if (bulletMatch) {
|
|
42
|
+
const text = bulletMatch[1].trim()
|
|
43
|
+
if (text.length > 10 && !text.startsWith('#')) {
|
|
44
|
+
criteria.push(text)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return criteria
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if criterion is covered at Level 1 (assertion/execution match).
|
|
54
|
+
* Strategy: search test_log for:
|
|
55
|
+
* 1. Test names containing criterion keywords or AC- IDs
|
|
56
|
+
* 2. Assertion messages containing criterion keywords
|
|
57
|
+
* 3. Explicit mapping comments like "// covers: criterion"
|
|
58
|
+
*/
|
|
59
|
+
function isCoveredAtLevel1(criterion: string, testLog: string, diff: string): { covered: boolean; matchType: 'assertion' | 'test_name' | 'keyword' | 'none'; evidence?: string } {
|
|
60
|
+
const lowerCriterion = criterion.toLowerCase()
|
|
61
|
+
const lowerTestLog = testLog.toLowerCase()
|
|
62
|
+
const lowerDiff = diff.toLowerCase()
|
|
63
|
+
|
|
64
|
+
// Extract significant words from criterion (≥4 chars, not stop words)
|
|
65
|
+
const words = lowerCriterion
|
|
66
|
+
.replace(/[^\w\s]/g, ' ')
|
|
67
|
+
.split(/\s+/)
|
|
68
|
+
.filter(w => w.length >= 4 && !STOP_WORDS.has(w))
|
|
69
|
+
|
|
70
|
+
if (words.length === 0) return { covered: false, matchType: 'keyword' }
|
|
71
|
+
|
|
72
|
+
// Check for AC-N or REQ-N identifiers
|
|
73
|
+
const idMatch = criterion.match(/\b(AC|REQ|CRITERION)[-_](\d+)\b/i)
|
|
74
|
+
if (idMatch) {
|
|
75
|
+
const id = idMatch[0].toLowerCase()
|
|
76
|
+
if (lowerTestLog.includes(id) || lowerDiff.includes(id)) {
|
|
77
|
+
return { covered: true, matchType: 'test_name', evidence: `ID reference: ${idMatch[0]}` }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check test_log for assertion patterns with criterion words
|
|
82
|
+
const assertionPatterns = [
|
|
83
|
+
new RegExp(`assert(?:ion)?\\s*\\(?[^)]*${words[0]}[^)]*\\)`, 'i'),
|
|
84
|
+
new RegExp(`expect\\([^)]*\\)\\.to(?:Be|Equal|Match|Contain)\\([^)]*${words[0]}[^)]*\\)`, 'i'),
|
|
85
|
+
new RegExp(`test\\(['"\`][^'"\`]*${words[0]}[^'"\`]*['"\`]`, 'i'),
|
|
86
|
+
new RegExp(`it\\(['"\`][^'"\`]*${words[0]}[^'"\`]*['"\`]`, 'i'),
|
|
87
|
+
new RegExp(`describe\\(['"\`][^'"\`]*${words[0]}[^'"\`]*['"\`]`, 'i'),
|
|
88
|
+
new RegExp(`//\\s*covers:?\\s*.*${words[0]}`, 'i'),
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
for (const pattern of assertionPatterns) {
|
|
92
|
+
const match = pattern.exec(testLog)
|
|
93
|
+
if (match) {
|
|
94
|
+
const snippet = match[0].slice(0, 80)
|
|
95
|
+
return { covered: true, matchType: 'assertion', evidence: snippet }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check test_log for test names containing significant words (at least 50% match)
|
|
100
|
+
const wordMatchCount = words.filter(w => lowerTestLog.includes(w)).length
|
|
101
|
+
if (words.length > 0 && wordMatchCount / words.length >= 0.5) {
|
|
102
|
+
return { covered: true, matchType: 'test_name', evidence: `${wordMatchCount}/${words.length} keywords matched` }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Fall back to keyword presence in test_log (Level 0 detection as diagnostic)
|
|
106
|
+
const anyWordMatch = words.some(w => lowerTestLog.includes(w))
|
|
107
|
+
if (anyWordMatch) {
|
|
108
|
+
return { covered: false, matchType: 'keyword' }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { covered: false, matchType: 'none' }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class SpecCoverageValidator {
|
|
115
|
+
/**
|
|
116
|
+
* Validate spec coverage at Level 1 (execution/assertion match).
|
|
117
|
+
* Unlike SpecCoverageDetector (keyword heuristic), this validator
|
|
118
|
+
* checks actual assertion and test name patterns in the test log.
|
|
119
|
+
*/
|
|
120
|
+
static validate(spec: string | null, testLog: string, diff: string): SpecCoverageResult {
|
|
121
|
+
if (!spec || spec.trim() === '') {
|
|
122
|
+
return {
|
|
123
|
+
issues: [],
|
|
124
|
+
assumptions: ['No spec provided — L1 (spec compliance) skipped'],
|
|
125
|
+
trust_deductions: 0,
|
|
126
|
+
coverage: [],
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const criteria = parseCriteria(spec)
|
|
131
|
+
if (criteria.length === 0) {
|
|
132
|
+
return {
|
|
133
|
+
issues: [],
|
|
134
|
+
assumptions: ['Spec has no parseable acceptance criteria'],
|
|
135
|
+
trust_deductions: 0,
|
|
136
|
+
coverage: [],
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const coverage: SpecCriterionResult[] = []
|
|
141
|
+
const issues: SealIssue[] = []
|
|
142
|
+
let trust_deductions = 0
|
|
143
|
+
|
|
144
|
+
for (const criterion of criteria) {
|
|
145
|
+
const result = isCoveredAtLevel1(criterion, testLog, diff)
|
|
146
|
+
|
|
147
|
+
coverage.push({
|
|
148
|
+
criterion: criterion.slice(0, 120) + (criterion.length > 120 ? '…' : ''),
|
|
149
|
+
level: result.covered ? 1 : 0,
|
|
150
|
+
match_type: result.matchType,
|
|
151
|
+
evidence: result.evidence,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
if (!result.covered) {
|
|
155
|
+
const deduction = Math.min(10, MAX_LEVEL1_DEDUCTION - trust_deductions)
|
|
156
|
+
if (deduction > 0) {
|
|
157
|
+
issues.push(makeIssue({
|
|
158
|
+
type: 'SPEC_MISMATCH',
|
|
159
|
+
severity: 'MEDIUM',
|
|
160
|
+
layer: 'L1',
|
|
161
|
+
source: 'core',
|
|
162
|
+
rule_id: 'SV101',
|
|
163
|
+
trust_deduction: deduction,
|
|
164
|
+
evidence: `Acceptance criterion not covered at execution level: "${criterion.slice(0, 80)}"`,
|
|
165
|
+
suggested_fix: `Add a test or assertion covering: ${criterion.slice(0, 80)}`,
|
|
166
|
+
}))
|
|
167
|
+
trust_deductions += deduction
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { issues, assumptions: [], trust_deductions, coverage }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { SealIssue, ArtifactType, RiskLevel, SealEvidence } from '../types.js'
|
|
2
|
+
import { makeIssue } from '../types.js'
|
|
3
|
+
|
|
4
|
+
const NEGATIVE_TEST_PATTERN = /\b(fail|error|exception|invalid|unauthorized|forbidden|edge|negative|reject|timeout|backoff)\b/i
|
|
5
|
+
const RETRY_PATTERN = /\b(retry|retries|exponential.?backoff|timeout|circuit.?breaker)\b/i
|
|
6
|
+
const BUG_FIX_PATTERN = /\b(fix(?:ed)?|bug|defect|regression)\b/i
|
|
7
|
+
const REGRESSION_TEST_PATTERN = /\b(regression|repro|original.?bug|fixed.?case)\b/i
|
|
8
|
+
const AUTH_UNAUTH_PATTERN = /\b(unauthorized|forbidden|403|401|permission.?denied|access.?denied)\b/i
|
|
9
|
+
|
|
10
|
+
export interface TestWeaknessResult {
|
|
11
|
+
issues: SealIssue[]
|
|
12
|
+
trust_deductions: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class TestWeaknessDetector {
|
|
16
|
+
static detect(params: {
|
|
17
|
+
artifact_type: ArtifactType
|
|
18
|
+
output: string
|
|
19
|
+
risk_level: RiskLevel
|
|
20
|
+
auth_flagged: boolean
|
|
21
|
+
}, evidence: SealEvidence): TestWeaknessResult {
|
|
22
|
+
const { artifact_type, output, risk_level, auth_flagged } = params
|
|
23
|
+
const { test_log } = evidence
|
|
24
|
+
const issues: SealIssue[] = []
|
|
25
|
+
let trust_deductions = 0
|
|
26
|
+
|
|
27
|
+
// TW201: implementation without tests
|
|
28
|
+
const isImpl = artifact_type === 'code_diff' || artifact_type === 'llm_response'
|
|
29
|
+
if (isImpl && test_log.trim() === '') {
|
|
30
|
+
issues.push(makeIssue({
|
|
31
|
+
type: 'TEST_GAP', severity: 'HIGH', layer: 'L4', source: 'core',
|
|
32
|
+
rule_id: 'TW201', trust_deduction: 15,
|
|
33
|
+
evidence: 'Implementation artifact has no test evidence',
|
|
34
|
+
required_fix: 'Provide test_log demonstrating test coverage',
|
|
35
|
+
}))
|
|
36
|
+
trust_deductions += 15
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// TW202: happy path only for MEDIUM+ risk
|
|
40
|
+
const riskOrder = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']
|
|
41
|
+
const isMediumPlus = riskOrder.indexOf(risk_level) >= riskOrder.indexOf('MEDIUM')
|
|
42
|
+
if (isMediumPlus && test_log.trim().length > 0 && !NEGATIVE_TEST_PATTERN.test(test_log)) {
|
|
43
|
+
issues.push(makeIssue({
|
|
44
|
+
type: 'TEST_GAP', severity: 'MEDIUM', layer: 'L4', source: 'core',
|
|
45
|
+
rule_id: 'TW202', trust_deduction: 10,
|
|
46
|
+
evidence: `Tests appear to cover happy path only for ${risk_level} risk artifact`,
|
|
47
|
+
suggested_fix: 'Add negative cases, error scenarios, or edge cases',
|
|
48
|
+
}))
|
|
49
|
+
trust_deductions += 10
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// TW203: bug fix without regression test
|
|
53
|
+
if (BUG_FIX_PATTERN.test(output) && test_log.trim().length > 0 && !REGRESSION_TEST_PATTERN.test(test_log)) {
|
|
54
|
+
issues.push(makeIssue({
|
|
55
|
+
type: 'TEST_GAP', severity: 'HIGH', layer: 'L4', source: 'core',
|
|
56
|
+
rule_id: 'TW203',
|
|
57
|
+
evidence: 'Bug fix detected but no regression test evidence found',
|
|
58
|
+
required_fix: 'Add regression test that reproduces the original bug',
|
|
59
|
+
}))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// TW204: retry logic without failure/timeout test
|
|
63
|
+
if (RETRY_PATTERN.test(output) && test_log.trim().length > 0 && !NEGATIVE_TEST_PATTERN.test(test_log)) {
|
|
64
|
+
issues.push(makeIssue({
|
|
65
|
+
type: 'TEST_GAP', severity: 'MEDIUM', layer: 'L4', source: 'core',
|
|
66
|
+
rule_id: 'TW204',
|
|
67
|
+
evidence: 'Retry/timeout logic detected but no failure/timeout test evidence',
|
|
68
|
+
suggested_fix: 'Add tests for timeout and failure scenarios',
|
|
69
|
+
}))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// TW205: auth change without unauthorized test
|
|
73
|
+
if (auth_flagged && test_log.trim().length > 0 && !AUTH_UNAUTH_PATTERN.test(test_log)) {
|
|
74
|
+
issues.push(makeIssue({
|
|
75
|
+
type: 'TEST_GAP', severity: 'CRITICAL', layer: 'L4', source: 'core',
|
|
76
|
+
rule_id: 'TW205', required_verdict: 'ESCALATE_TO_HUMAN',
|
|
77
|
+
evidence: 'Auth-related change detected but no unauthorized/forbidden test evidence',
|
|
78
|
+
required_fix: 'Add tests for unauthorized and forbidden access scenarios',
|
|
79
|
+
}))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { issues, trust_deductions }
|
|
83
|
+
}
|
|
84
|
+
}
|