iterate-plugin 2.12.2 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -12
- package/README.zh-CN.md +2 -1
- package/dist/approval-gate.js +16 -2
- package/dist/config-loader.js +5 -0
- package/dist/git-scope.js +61 -7
- package/dist/index.js +16 -6
- package/dist/session-hooks.js +36 -11
- package/dist/skill-prompt.js +3 -0
- package/dist/tools/decision-log.js +10 -1
- package/dist/tools/defense-events.js +260 -0
- package/dist/tools/defense-store.js +97 -0
- package/dist/tools/experience-bank.js +248 -0
- package/dist/tools/experience-store.js +132 -0
- package/dist/tools/quality-gate.js +180 -0
- package/dist/tools/quality-store.js +174 -0
- package/lib/client.js +662 -103
- package/lib/parse.js +93 -0
- package/package.json +7 -6
- package/src/approval-gate.ts +14 -2
- package/src/client/index.ts +542 -49
- package/src/config-loader.ts +5 -0
- package/src/git-scope.ts +48 -7
- package/src/index.ts +16 -6
- package/src/session-hooks.ts +33 -11
- package/src/skill-prompt.ts +3 -0
- package/src/tools/checkpoint.ts +1 -1
- package/src/tools/config.ts +1 -1
- package/src/tools/decision-log.ts +11 -2
- package/src/tools/defense-events.ts +295 -0
- package/src/tools/defense-store.ts +113 -0
- package/src/tools/experience-bank.ts +264 -0
- package/src/tools/experience-store.ts +160 -0
- package/src/tools/fix.ts +1 -1
- package/src/tools/history.ts +1 -1
- package/src/tools/prune.ts +1 -1
- package/src/tools/quality-gate.ts +193 -0
- package/src/tools/quality-store.ts +199 -0
- package/src/tools/review.ts +1 -1
- package/src/tools/transcript.ts +1 -1
- package/src/tools/triage.ts +1 -1
- package/src/types.ts +118 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/tools/experience-store.ts — experience bank storage layer.
|
|
3
|
+
*
|
|
4
|
+
* Provides read/write access to the experience bank stored in
|
|
5
|
+
* .iterate/experience.json. Experiences are accumulated across sessions.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
const EXPERIENCE_FILE = 'experience.json';
|
|
10
|
+
/** Default empty experience bank. */
|
|
11
|
+
function emptyBank() {
|
|
12
|
+
return {
|
|
13
|
+
entries: [],
|
|
14
|
+
lastUpdated: new Date().toISOString(),
|
|
15
|
+
totalHits: 0,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Read the experience bank from disk. Returns empty bank if not found. */
|
|
19
|
+
export function readExperienceBank(projectRoot) {
|
|
20
|
+
const filePath = path.join(projectRoot, '.iterate', EXPERIENCE_FILE);
|
|
21
|
+
try {
|
|
22
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
23
|
+
const parsed = JSON.parse(content);
|
|
24
|
+
if (parsed && Array.isArray(parsed.entries)) {
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// File not found or invalid JSON
|
|
30
|
+
}
|
|
31
|
+
return emptyBank();
|
|
32
|
+
}
|
|
33
|
+
/** Write the experience bank to disk. */
|
|
34
|
+
export function writeExperienceBank(projectRoot, bank) {
|
|
35
|
+
const dirPath = path.join(projectRoot, '.iterate');
|
|
36
|
+
const filePath = path.join(dirPath, EXPERIENCE_FILE);
|
|
37
|
+
try {
|
|
38
|
+
if (!fs.existsSync(dirPath)) {
|
|
39
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
fs.writeFileSync(filePath, JSON.stringify(bank, null, 2), 'utf-8');
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Silently fail - experience bank is not critical
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Search experience entries by query string. */
|
|
48
|
+
export function searchExperienceEntries(entries, query, opts = {}) {
|
|
49
|
+
const lowerQuery = query.toLowerCase();
|
|
50
|
+
return entries.filter((entry) => {
|
|
51
|
+
// Dimension filter
|
|
52
|
+
if (opts.dimension && entry.dimension !== opts.dimension) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
// Tags filter (AND logic)
|
|
56
|
+
if (opts.tags && opts.tags.length > 0) {
|
|
57
|
+
if (!opts.tags.every((t) => entry.tags.includes(t))) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Text search across multiple fields
|
|
62
|
+
if (query) {
|
|
63
|
+
const searchableText = [
|
|
64
|
+
entry.pattern,
|
|
65
|
+
entry.description,
|
|
66
|
+
entry.verifiedFix,
|
|
67
|
+
entry.findingSummary,
|
|
68
|
+
entry.dimension,
|
|
69
|
+
...entry.files,
|
|
70
|
+
...entry.tags,
|
|
71
|
+
].join(' ').toLowerCase();
|
|
72
|
+
if (!searchableText.includes(lowerQuery)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Add or update an experience entry.
|
|
81
|
+
*
|
|
82
|
+
* An entry with an `id` that already exists, OR a new entry whose
|
|
83
|
+
* `pattern`+`dimension` pair matches an existing entry, is treated as a HIT:
|
|
84
|
+
* the matching entry's hitCount is incremented (lastHitAt refreshed) so
|
|
85
|
+
* repeated encounters of the same pattern do not create duplicates. Otherwise
|
|
86
|
+
* a fresh entry is appended with hitCount 1. Never mutates the input bank.
|
|
87
|
+
*
|
|
88
|
+
* Returns the resulting bank plus whether a NEW entry was created and the id
|
|
89
|
+
* of the affected entry.
|
|
90
|
+
*/
|
|
91
|
+
export function upsertExperience(bank, entry) {
|
|
92
|
+
const lastUpdated = new Date().toISOString();
|
|
93
|
+
const existing = entry.id
|
|
94
|
+
? bank.entries.find((e) => e.id === entry.id)
|
|
95
|
+
: bank.entries.find((e) => e.pattern === entry.pattern && e.dimension === entry.dimension);
|
|
96
|
+
if (existing) {
|
|
97
|
+
const updated = {
|
|
98
|
+
...existing,
|
|
99
|
+
hitCount: (existing.hitCount ?? 0) + 1,
|
|
100
|
+
lastHitAt: lastUpdated,
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
bank: {
|
|
104
|
+
...bank,
|
|
105
|
+
entries: bank.entries.map((e) => (e.id === existing.id ? updated : e)),
|
|
106
|
+
lastUpdated,
|
|
107
|
+
totalHits: (bank.totalHits ?? 0) + 1,
|
|
108
|
+
},
|
|
109
|
+
added: false,
|
|
110
|
+
entryId: existing.id,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Add new entry
|
|
114
|
+
const id = entry.id || `exp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
115
|
+
const newEntry = {
|
|
116
|
+
id,
|
|
117
|
+
timestamp: lastUpdated,
|
|
118
|
+
hitCount: 1,
|
|
119
|
+
lastHitAt: lastUpdated,
|
|
120
|
+
...entry,
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
bank: {
|
|
124
|
+
...bank,
|
|
125
|
+
entries: [...bank.entries, newEntry],
|
|
126
|
+
lastUpdated,
|
|
127
|
+
totalHits: (bank.totalHits ?? 0) + 1,
|
|
128
|
+
},
|
|
129
|
+
added: true,
|
|
130
|
+
entryId: id,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/tools/quality-gate.ts — quality gate query & write tool.
|
|
3
|
+
*
|
|
4
|
+
* iterate_quality_gate — query the persisted quality certificate, or compute
|
|
5
|
+
* and persist a new one from review/validation data.
|
|
6
|
+
*
|
|
7
|
+
* Provides a machine-readable quality certificate for the current iteration.
|
|
8
|
+
*/
|
|
9
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
10
|
+
import { resolveProjectRootForExec } from "../config-loader.js";
|
|
11
|
+
import { readQualityGate, writeQualityGate, computeQualityGate } from "./quality-store.js";
|
|
12
|
+
/** Validate a single finding object; returns true when well-formed. */
|
|
13
|
+
function isValidFinding(raw) {
|
|
14
|
+
if (!raw || typeof raw !== 'object')
|
|
15
|
+
return false;
|
|
16
|
+
const f = raw;
|
|
17
|
+
return (typeof f.dimension === 'string' &&
|
|
18
|
+
typeof f.severity === 'string' &&
|
|
19
|
+
typeof f.file === 'string' &&
|
|
20
|
+
(f.line === undefined || typeof f.line === 'number'));
|
|
21
|
+
}
|
|
22
|
+
/** Validate a single validation result; returns true when well-formed. */
|
|
23
|
+
function isValidValidationResult(raw) {
|
|
24
|
+
if (!raw || typeof raw !== 'object')
|
|
25
|
+
return false;
|
|
26
|
+
const r = raw;
|
|
27
|
+
return typeof r.command === 'string' && typeof r.exitCode === 'number' && Number.isFinite(r.exitCode);
|
|
28
|
+
}
|
|
29
|
+
/** Sanitize a caller-supplied per-dimension number map. */
|
|
30
|
+
function sanitizeNumberMap(raw) {
|
|
31
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
32
|
+
return undefined;
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
35
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
|
|
36
|
+
out[key] = value;
|
|
37
|
+
}
|
|
38
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
39
|
+
}
|
|
40
|
+
/** Sanitize a caller-supplied per-dimension round series map. */
|
|
41
|
+
function sanitizeRoundSeries(raw) {
|
|
42
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
43
|
+
return undefined;
|
|
44
|
+
const out = {};
|
|
45
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
46
|
+
if (Array.isArray(value)) {
|
|
47
|
+
const series = value
|
|
48
|
+
.filter((n) => typeof n === 'number' && Number.isFinite(n) && n >= 0);
|
|
49
|
+
if (series.length > 0)
|
|
50
|
+
out[key] = series;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Register the `iterate_quality_gate` tool.
|
|
57
|
+
* Reads the persisted quality certificate, or computes + persists a new one.
|
|
58
|
+
*/
|
|
59
|
+
export function registerQualityGateTool(ctx) {
|
|
60
|
+
ctx.tools.register(defineTool({
|
|
61
|
+
name: 'iterate_quality_gate',
|
|
62
|
+
description: 'Query or write the quality gate status: dimension convergence rates, verification pass rates, ' +
|
|
63
|
+
'and overall PASS/FAIL status. ' +
|
|
64
|
+
'Operation "read" (default) returns the persisted machine-readable quality certificate. ' +
|
|
65
|
+
'Operation "compute" computes a fresh snapshot from this round\'s findings/validation results, ' +
|
|
66
|
+
'persists it to .iterate/quality-gate.json, and returns it.',
|
|
67
|
+
parameters: {
|
|
68
|
+
operation: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: 'Operation: read (load persisted certificate) or compute (recompute + persist). Default: read.',
|
|
71
|
+
enum: ['read', 'compute'],
|
|
72
|
+
},
|
|
73
|
+
dimensions: {
|
|
74
|
+
type: 'array',
|
|
75
|
+
items: { type: 'string' },
|
|
76
|
+
description: 'Dimensions to gate (required for compute).',
|
|
77
|
+
},
|
|
78
|
+
findings: {
|
|
79
|
+
type: 'json',
|
|
80
|
+
description: 'Findings array (required for compute). Each item: { dimension, severity (critical|high|medium|low), file, line? }.',
|
|
81
|
+
},
|
|
82
|
+
validationResults: {
|
|
83
|
+
type: 'json',
|
|
84
|
+
description: 'Validation results array (optional for compute). Each item: { command, exitCode }.',
|
|
85
|
+
},
|
|
86
|
+
findingsByRound: {
|
|
87
|
+
type: 'json',
|
|
88
|
+
description: 'Optional per-dimension NEW-finding counts across rounds (latest last) — used to compute real convergence rates. ' +
|
|
89
|
+
'Example: { "correctness": [5, 2, 0] }.',
|
|
90
|
+
},
|
|
91
|
+
fixedByDimension: {
|
|
92
|
+
type: 'json',
|
|
93
|
+
description: 'Optional per-dimension count of fixed findings, e.g. { "correctness": 3 }.',
|
|
94
|
+
},
|
|
95
|
+
path: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
description: 'Project root directory (default: current working directory).',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
output: {
|
|
101
|
+
schema: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
properties: {
|
|
105
|
+
ok: { type: 'boolean', required: true },
|
|
106
|
+
kind: { type: 'string' },
|
|
107
|
+
operation: { type: 'string' },
|
|
108
|
+
snapshot: { type: 'json' },
|
|
109
|
+
error: { type: 'string' },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
render: (_args, value) => {
|
|
113
|
+
if (!value.ok)
|
|
114
|
+
return [{ type: 'text', text: `quality gate query failed: ${value.error}` }];
|
|
115
|
+
const operation = typeof value.operation === 'string' ? value.operation : 'read';
|
|
116
|
+
const snapshot = value.snapshot;
|
|
117
|
+
if (!snapshot)
|
|
118
|
+
return [{ type: 'text', text: 'No quality gate data available.' }];
|
|
119
|
+
const statusEmoji = snapshot.overallStatus === 'pass' ? '✓' : snapshot.overallStatus === 'fail' ? '✗' : '○';
|
|
120
|
+
const lines = [
|
|
121
|
+
`${statusEmoji} Quality Gate: ${snapshot.overallStatus.toUpperCase()} (score: ${snapshot.overallScore})`,
|
|
122
|
+
`Verification: ${snapshot.passedChecks}/${snapshot.totalChecks} passed (${snapshot.verificationPassRate}%)`,
|
|
123
|
+
`Findings: ${snapshot.totalFindings} total (${snapshot.criticalCount} critical, ${snapshot.highCount} high, ${snapshot.mediumCount} medium, ${snapshot.lowCount} low)`,
|
|
124
|
+
'',
|
|
125
|
+
'Dimension Breakdown:',
|
|
126
|
+
...snapshot.dimensions.map((d) => {
|
|
127
|
+
const dimStatus = d.status === 'pass' ? '✓' : d.status === 'warn' ? '!' : '✗';
|
|
128
|
+
return ` ${dimStatus} ${d.dimension}: score=${d.score}, convergence=${d.convergenceRate}%, findings=${d.findingsCount}, fixed=${d.fixedCount}`;
|
|
129
|
+
}),
|
|
130
|
+
];
|
|
131
|
+
if (snapshot.failReason) {
|
|
132
|
+
lines.push('', `Fail Reason: ${snapshot.failReason}`);
|
|
133
|
+
}
|
|
134
|
+
if (operation === 'compute') {
|
|
135
|
+
lines.push('', 'Quality gate snapshot computed and persisted.');
|
|
136
|
+
}
|
|
137
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
async execute(args, exec) {
|
|
141
|
+
const resolved = resolveProjectRootForExec(exec, args.path);
|
|
142
|
+
if (!resolved.ok)
|
|
143
|
+
return { ok: false, kind: 'quality_gate', error: resolved.reason };
|
|
144
|
+
const projectRoot = resolved.root;
|
|
145
|
+
const operation = typeof args.operation === 'string' ? args.operation : 'read';
|
|
146
|
+
if (operation === 'compute') {
|
|
147
|
+
const dimensions = Array.isArray(args.dimensions)
|
|
148
|
+
? args.dimensions.filter((d) => typeof d === 'string' && d.length > 0)
|
|
149
|
+
: [];
|
|
150
|
+
const findings = Array.isArray(args.findings) ? args.findings.filter(isValidFinding) : [];
|
|
151
|
+
const validationResults = Array.isArray(args.validationResults)
|
|
152
|
+
? args.validationResults.filter(isValidValidationResult)
|
|
153
|
+
: undefined;
|
|
154
|
+
const findingsByRound = sanitizeRoundSeries(args.findingsByRound);
|
|
155
|
+
const fixedByDimension = sanitizeNumberMap(args.fixedByDimension);
|
|
156
|
+
const snapshot = computeQualityGate({
|
|
157
|
+
dimensions,
|
|
158
|
+
findings,
|
|
159
|
+
validationResults,
|
|
160
|
+
findingsByRound,
|
|
161
|
+
fixedByDimension,
|
|
162
|
+
});
|
|
163
|
+
writeQualityGate(projectRoot, snapshot);
|
|
164
|
+
return {
|
|
165
|
+
ok: true,
|
|
166
|
+
kind: 'quality_gate',
|
|
167
|
+
operation: 'compute',
|
|
168
|
+
snapshot: snapshot,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const snapshot = readQualityGate(projectRoot);
|
|
172
|
+
return {
|
|
173
|
+
ok: true,
|
|
174
|
+
kind: 'quality_gate',
|
|
175
|
+
operation: 'read',
|
|
176
|
+
snapshot: snapshot,
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/tools/quality-store.ts — quality gate storage layer.
|
|
3
|
+
*
|
|
4
|
+
* Provides read/write access to quality gate data stored in
|
|
5
|
+
* .iterate/quality-gate.json. Quality gate snapshots are generated
|
|
6
|
+
* from review results and validation outcomes.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
const QUALITY_GATE_FILE = 'quality-gate.json';
|
|
11
|
+
/** Default empty quality gate snapshot. */
|
|
12
|
+
function emptySnapshot() {
|
|
13
|
+
return {
|
|
14
|
+
timestamp: new Date().toISOString(),
|
|
15
|
+
overallStatus: 'pending',
|
|
16
|
+
overallScore: 0,
|
|
17
|
+
dimensions: [],
|
|
18
|
+
verificationPassRate: 0,
|
|
19
|
+
totalChecks: 0,
|
|
20
|
+
passedChecks: 0,
|
|
21
|
+
failedChecks: 0,
|
|
22
|
+
totalFindings: 0,
|
|
23
|
+
criticalCount: 0,
|
|
24
|
+
highCount: 0,
|
|
25
|
+
mediumCount: 0,
|
|
26
|
+
lowCount: 0,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Read the quality gate snapshot from disk. */
|
|
30
|
+
export function readQualityGate(projectRoot) {
|
|
31
|
+
const filePath = path.join(projectRoot, '.iterate', QUALITY_GATE_FILE);
|
|
32
|
+
try {
|
|
33
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
34
|
+
const parsed = JSON.parse(content);
|
|
35
|
+
if (parsed && typeof parsed === 'object') {
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// File not found or invalid JSON
|
|
41
|
+
}
|
|
42
|
+
return emptySnapshot();
|
|
43
|
+
}
|
|
44
|
+
/** Write the quality gate snapshot to disk. */
|
|
45
|
+
export function writeQualityGate(projectRoot, snapshot) {
|
|
46
|
+
const dirPath = path.join(projectRoot, '.iterate');
|
|
47
|
+
const filePath = path.join(dirPath, QUALITY_GATE_FILE);
|
|
48
|
+
try {
|
|
49
|
+
if (!fs.existsSync(dirPath)) {
|
|
50
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
fs.writeFileSync(filePath, JSON.stringify(snapshot, null, 2), 'utf-8');
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Silently fail - quality gate is not critical
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Compute the convergence rate for a dimension.
|
|
60
|
+
*
|
|
61
|
+
* Convergence measures how much NEW-finding volume shrank across rounds:
|
|
62
|
+
* `(first - last) / first` from the dimension's per-round findings series,
|
|
63
|
+
* expressed as a 0-100 percentage, clamped. A series with no fresh findings
|
|
64
|
+
* (or a dimension never reporting a first-round reading) counts as fully
|
|
65
|
+
* converged (100). Returns 0 — no measurable improvement — when a reading
|
|
66
|
+
* exists but the series is empty or malformed.
|
|
67
|
+
*/
|
|
68
|
+
export function convergenceRateFor(series, currentCount) {
|
|
69
|
+
if (Array.isArray(series) && series.length > 0) {
|
|
70
|
+
const first = series.find((n) => typeof n === 'number' && Number.isFinite(n));
|
|
71
|
+
const last = [...series].reverse().find((n) => typeof n === 'number' && Number.isFinite(n));
|
|
72
|
+
if (first === undefined || last === undefined)
|
|
73
|
+
return currentCount === 0 ? 100 : 0;
|
|
74
|
+
if (first <= 0)
|
|
75
|
+
return currentCount === 0 ? 100 : 0;
|
|
76
|
+
const raw = ((first - Math.max(0, last)) / first) * 100;
|
|
77
|
+
return Math.max(0, Math.min(100, Math.round(raw)));
|
|
78
|
+
}
|
|
79
|
+
return currentCount === 0 ? 100 : 0;
|
|
80
|
+
}
|
|
81
|
+
/** Compute a quality gate snapshot from review data. */
|
|
82
|
+
export function computeQualityGate(opts) {
|
|
83
|
+
const { dimensions, findings, validationResults, findingsByRound, fixedByDimension } = opts;
|
|
84
|
+
// Count findings by severity
|
|
85
|
+
const criticalCount = findings.filter((f) => f.severity === 'critical').length;
|
|
86
|
+
const highCount = findings.filter((f) => f.severity === 'high').length;
|
|
87
|
+
const mediumCount = findings.filter((f) => f.severity === 'medium').length;
|
|
88
|
+
const lowCount = findings.filter((f) => f.severity === 'low').length;
|
|
89
|
+
const totalFindings = findings.length;
|
|
90
|
+
// Compute dimension scores
|
|
91
|
+
const dimensionStats = {};
|
|
92
|
+
for (const dim of dimensions) {
|
|
93
|
+
dimensionStats[dim] = { count: 0, critical: 0, high: 0, medium: 0, low: 0 };
|
|
94
|
+
}
|
|
95
|
+
for (const finding of findings) {
|
|
96
|
+
const stats = dimensionStats[finding.dimension];
|
|
97
|
+
if (stats) {
|
|
98
|
+
stats.count++;
|
|
99
|
+
if (finding.severity === 'critical')
|
|
100
|
+
stats.critical++;
|
|
101
|
+
else if (finding.severity === 'high')
|
|
102
|
+
stats.high++;
|
|
103
|
+
else if (finding.severity === 'medium')
|
|
104
|
+
stats.medium++;
|
|
105
|
+
else
|
|
106
|
+
stats.low++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Compute dimension-level quality gates
|
|
110
|
+
const dimensionGates = dimensions.map((dim) => {
|
|
111
|
+
const stats = dimensionStats[dim] || { count: 0, critical: 0, high: 0, medium: 0, low: 0 };
|
|
112
|
+
// Score: 100 - (critical*30 + high*15 + medium*5 + low*1), capped at 0
|
|
113
|
+
const penalty = stats.critical * 30 + stats.high * 15 + stats.medium * 5 + stats.low * 1;
|
|
114
|
+
const score = Math.max(0, 100 - penalty);
|
|
115
|
+
const status = score >= 80 ? 'pass' : score >= 50 ? 'warn' : 'fail';
|
|
116
|
+
const series = findingsByRound?.[dim];
|
|
117
|
+
const convergenceRate = convergenceRateFor(Array.isArray(series) ? series : undefined, stats.count);
|
|
118
|
+
return {
|
|
119
|
+
dimension: dim,
|
|
120
|
+
convergenceRate,
|
|
121
|
+
findingsCount: stats.count,
|
|
122
|
+
fixedCount: fixedByDimension?.[dim] ?? 0,
|
|
123
|
+
score,
|
|
124
|
+
status,
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
// Compute verification pass rate
|
|
128
|
+
const totalChecks = validationResults?.length ?? 0;
|
|
129
|
+
const passedChecks = validationResults?.filter((r) => r.exitCode === 0).length ?? 0;
|
|
130
|
+
const failedChecks = totalChecks - passedChecks;
|
|
131
|
+
const verificationPassRate = totalChecks > 0 ? Math.round((passedChecks / totalChecks) * 100) : 0;
|
|
132
|
+
// Compute overall score (weighted average of dimension scores)
|
|
133
|
+
const overallScore = dimensionGates.length > 0
|
|
134
|
+
? Math.round(dimensionGates.reduce((sum, d) => sum + d.score, 0) / dimensionGates.length)
|
|
135
|
+
: 0;
|
|
136
|
+
// Determine overall status
|
|
137
|
+
const hasCritical = criticalCount > 0;
|
|
138
|
+
const hasHighFail = dimensionGates.some((d) => d.status === 'fail');
|
|
139
|
+
const verificationFails = totalChecks > 0 && failedChecks > 0;
|
|
140
|
+
let overallStatus = 'pass';
|
|
141
|
+
let failReason;
|
|
142
|
+
if (hasCritical) {
|
|
143
|
+
overallStatus = 'fail';
|
|
144
|
+
failReason = `${criticalCount} critical findings present`;
|
|
145
|
+
}
|
|
146
|
+
else if (hasHighFail) {
|
|
147
|
+
overallStatus = 'fail';
|
|
148
|
+
failReason = 'One or more dimensions failed quality gate';
|
|
149
|
+
}
|
|
150
|
+
else if (verificationFails) {
|
|
151
|
+
overallStatus = 'fail';
|
|
152
|
+
failReason = `${failedChecks} validation checks failed`;
|
|
153
|
+
}
|
|
154
|
+
else if (overallScore < 70) {
|
|
155
|
+
overallStatus = 'fail';
|
|
156
|
+
failReason = `Overall score ${overallScore} below threshold (70)`;
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
timestamp: new Date().toISOString(),
|
|
160
|
+
overallStatus,
|
|
161
|
+
overallScore,
|
|
162
|
+
dimensions: dimensionGates,
|
|
163
|
+
verificationPassRate,
|
|
164
|
+
totalChecks,
|
|
165
|
+
passedChecks,
|
|
166
|
+
failedChecks,
|
|
167
|
+
failReason,
|
|
168
|
+
totalFindings,
|
|
169
|
+
criticalCount,
|
|
170
|
+
highCount,
|
|
171
|
+
mediumCount,
|
|
172
|
+
lowCount,
|
|
173
|
+
};
|
|
174
|
+
}
|