kld-sdd 2.6.12 → 2.6.13
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 +20 -0
- package/kld-sdd-guide.html +429 -159
- package/lib/init.js +40 -7
- package/package.json +5 -2
- package/skywalk-sdd/index.cjs +2668 -415
- package/skywalk-sdd/metrics-v3.cjs +1153 -0
- package/skywalk-sdd/ontology/archive-package.cjs +114 -3
- package/skywalk-sdd/ontology/resolve-spec-root.cjs +20 -80
- package/skywalk-sdd/ontology/working-artifacts.cjs +2 -1
- package/templates/hooks/claude/hooks/sdd-apply-test-gate.cjs +39 -9
- package/templates/hooks/claude/hooks/sdd-post-tool.cjs +87 -8
- package/templates/hooks/claude/hooks/sdd-prompt.cjs +3 -1
- package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +39 -9
- package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +87 -9
- package/templates/hooks/codebuddy/hooks/sdd-prompt.cjs +3 -1
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +14 -13
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +20 -20
- package/templates/skills/kld-sdd/opsx-apply/implementer-prompt.md +12 -12
- package/templates/skills/kld-sdd/opsx-apply/reference.md +32 -31
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +44 -6
- package/templates/skills/kld-sdd/opsx-archive/checklist.md +4 -1
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +19 -11
- package/templates/skills/kld-sdd/opsx-check/checklist.md +7 -6
- package/templates/skills/kld-sdd/opsx-design/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-design/checklist.md +1 -1
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +9 -1
- package/templates/skills/kld-sdd/opsx-propose/reference.md +29 -3
- package/templates/skills/kld-sdd/opsx-rules/reference.md +3 -1
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-spec/checklist.md +1 -1
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +10 -10
- package/templates/skills/kld-sdd/opsx-task/checklist.md +3 -3
- package/templates/skills/kld-sdd/opsx-task/reference.md +3 -3
- package/templates/skills/kld-sdd/opsx-test/SKILL.md +7 -5
- package/templates/skills/kld-sdd/{opsx-tdd-anti-patterns → tdd-anti-patterns}/SKILL.md +4 -4
- package/templates/skills/kld-sdd/{opsx-tdd-anti-patterns → tdd-anti-patterns}/reference.md +4 -4
- package/templates/skills/kld-sdd/{opsx-tdd-core → tdd-core}/SKILL.md +13 -13
- package/templates/skills/kld-sdd/{opsx-tdd-core → tdd-core}/checklist.md +5 -5
- package/templates/skills/kld-sdd/{opsx-tdd-core → tdd-core}/reference.md +12 -12
- package/templates/skills/kld-sdd/{opsx-tdd-metrics → tdd-metrics}/SKILL.md +3 -3
- package/templates/skills/kld-sdd/{opsx-tdd-metrics → tdd-metrics}/checklist.md +2 -2
- package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/SKILL.md +2 -2
- package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/parameterized-testing.md +1 -1
- package/templates/skills/kld-sdd/{opsx-tdd-review → tdd-review}/SKILL.md +5 -5
- package/templates/skills/kld-sdd/{opsx-tdd-review → tdd-review}/checklist.md +5 -5
- package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/SKILL.md +3 -3
- package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/non-tdd-modules.md +1 -1
- package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/refactor-checklist.md +1 -1
- package/templates/skills/kld-sdd/tdd-rules/rules/test-skeleton-telemetry.md +19 -0
- package/templates/skills/kld-sdd/opsx-tdd-rules/rules/test-skeleton-telemetry.md +0 -19
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/cause-effect-clarity.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/clean-test-data.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/existing-test-awareness.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/given-when-then.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/good-test-qualities.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/mock-boundary.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/naming-conventions.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/no-logic-in-tests.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/one-test-one-scenario.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/prefer-public-apis.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/test-behaviors-not-methods.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/argument-matching.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/controller-test-rules.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/domain-service-rules.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/java-test-template.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/json-serialization.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/logging-rules.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/post-generation/compilation-verification.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/post-generation/execution-verification.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/python/py-test-template.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/typescript/ts-test-template.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/controller-strategy.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/dag-generation-rules.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/des-step-annotation.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/exception-path-coverage.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/green-scope-declaration.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/green-yagni-fence.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/multi-validation-split.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/task-type-definitions.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/tdd-strategy-selection.md +0 -0
- /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/test-execution-gate.md +0 -0
|
@@ -0,0 +1,1153 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const REPORT_SCHEMA_VERSION = 'sdd-efficiency-report/v3';
|
|
4
|
+
const METRICS_CONTRACT_VERSION = '3.0';
|
|
5
|
+
const CHANGE_TYPES = ['config', 'document', 'report', 'composite', 'other'];
|
|
6
|
+
const MINIMUM_METRICS = ['e1', 'q3', 'p4', 'p_r', 'p_h'];
|
|
7
|
+
|
|
8
|
+
const ALERT_THRESHOLDS = {
|
|
9
|
+
q3: { red: 70, yellow: 85, direction: 'higher_better' },
|
|
10
|
+
p4: { red: 0.4, yellow: 0.6, direction: 'higher_better' },
|
|
11
|
+
p_h: { red: 60, yellow: 80, direction: 'higher_better' },
|
|
12
|
+
p_c: { red: 0.75, yellow: 0.85, direction: 'higher_better' },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const DEVELOPMENT_ACTIVITY_TYPES = new Set([
|
|
16
|
+
'task_update',
|
|
17
|
+
'build_result',
|
|
18
|
+
'test_result',
|
|
19
|
+
'ai_adoption_review',
|
|
20
|
+
'doc_update',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const DETERMINISTIC_REWORK_REASONS = new Set([
|
|
24
|
+
'incomplete',
|
|
25
|
+
'prev-failed',
|
|
26
|
+
'pre-archive-recheck',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const AGENT_OVERRIDE_REASONS = new Set([
|
|
30
|
+
'incomplete',
|
|
31
|
+
'prev-failed',
|
|
32
|
+
'pre-archive-recheck',
|
|
33
|
+
'code-changed-after-pass',
|
|
34
|
+
'recheck-no-change',
|
|
35
|
+
'unspecified',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const SCENARIO_HEADING_RE = /^#####\s+(场景|Scenario)\s*[::]/gm;
|
|
39
|
+
|
|
40
|
+
function createInvalidArgError(code, message) {
|
|
41
|
+
const err = new Error(message);
|
|
42
|
+
err.code = code;
|
|
43
|
+
return err;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function clamp(n, min, max) {
|
|
47
|
+
if (!Number.isFinite(n)) return n;
|
|
48
|
+
return Math.min(max, Math.max(min, n));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function round(n, digits = 4) {
|
|
52
|
+
if (!Number.isFinite(n)) return n;
|
|
53
|
+
const factor = 10 ** digits;
|
|
54
|
+
return Math.round(n * factor) / factor;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function percentile(sortedOrUnsortedValues, p) {
|
|
58
|
+
if (!Array.isArray(sortedOrUnsortedValues)) {
|
|
59
|
+
throw createInvalidArgError('invalid-arg', 'percentile expects an array');
|
|
60
|
+
}
|
|
61
|
+
const finite = sortedOrUnsortedValues.filter(v => Number.isFinite(v));
|
|
62
|
+
if (finite.length === 0) return null;
|
|
63
|
+
const sorted = [...finite].sort((a, b) => a - b);
|
|
64
|
+
if (p <= 0) return sorted[0];
|
|
65
|
+
if (p >= 100) return sorted[sorted.length - 1];
|
|
66
|
+
const rank = (p / 100) * (sorted.length - 1);
|
|
67
|
+
const lower = Math.floor(rank);
|
|
68
|
+
const upper = Math.ceil(rank);
|
|
69
|
+
if (lower === upper) return sorted[lower];
|
|
70
|
+
const weight = rank - lower;
|
|
71
|
+
return sorted[lower] + weight * (sorted[upper] - sorted[lower]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function mean(values) {
|
|
75
|
+
if (!values.length) return null;
|
|
76
|
+
return values.reduce((sum, v) => sum + v, 0) / values.length;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function detectIqrOutliers(values) {
|
|
80
|
+
const finite = (values || []).filter(v => Number.isFinite(v));
|
|
81
|
+
if (finite.length < 4) return [];
|
|
82
|
+
const q1 = percentile(finite, 25);
|
|
83
|
+
const q3 = percentile(finite, 75);
|
|
84
|
+
const iqr = q3 - q1;
|
|
85
|
+
const lower = q1 - 1.5 * iqr;
|
|
86
|
+
const upper = q3 + 1.5 * iqr;
|
|
87
|
+
return finite.filter(v => v < lower || v > upper);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function summarizeDistribution(values) {
|
|
91
|
+
const finite = (values || []).filter(v => Number.isFinite(v));
|
|
92
|
+
const count = finite.length;
|
|
93
|
+
const meanRaw = count > 0 ? round(mean(finite)) : null;
|
|
94
|
+
if (count < 4) {
|
|
95
|
+
return {
|
|
96
|
+
count,
|
|
97
|
+
p50: count > 0 ? round(percentile(finite, 50)) : null,
|
|
98
|
+
p75: count > 0 ? round(percentile(finite, 75)) : null,
|
|
99
|
+
p90: count > 0 ? round(percentile(finite, 90)) : null,
|
|
100
|
+
mean_raw: meanRaw,
|
|
101
|
+
mean_adjusted: meanRaw,
|
|
102
|
+
outliers: [],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const outliers = detectIqrOutliers(finite);
|
|
106
|
+
const outlierSet = new Set(outliers);
|
|
107
|
+
const adjusted = finite.filter(v => !outlierSet.has(v));
|
|
108
|
+
return {
|
|
109
|
+
count,
|
|
110
|
+
p50: round(percentile(finite, 50)),
|
|
111
|
+
p75: round(percentile(finite, 75)),
|
|
112
|
+
p90: round(percentile(finite, 90)),
|
|
113
|
+
mean_raw: meanRaw,
|
|
114
|
+
mean_adjusted: adjusted.length > 0 ? round(mean(adjusted)) : meanRaw,
|
|
115
|
+
outliers: outliers.map(v => round(v)),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function countScenariosInMarkdown(text) {
|
|
120
|
+
if (typeof text !== 'string' || !text) return 0;
|
|
121
|
+
const normalized = text.replace(/\r\n/g, '\n');
|
|
122
|
+
const matches = normalized.match(SCENARIO_HEADING_RE);
|
|
123
|
+
return matches ? matches.length : 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function validateChangeType(value) {
|
|
127
|
+
if (typeof value !== 'string') return 'unknown';
|
|
128
|
+
const normalized = value.trim().toLowerCase();
|
|
129
|
+
return CHANGE_TYPES.includes(normalized) ? normalized : 'unknown';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolveReviewerIndependence(meta = {}) {
|
|
133
|
+
if (meta.reviewer_independence === 'independent-review'
|
|
134
|
+
|| meta.reviewer_independence === 'self-review'
|
|
135
|
+
|| meta.reviewer_independence === 'unknown') {
|
|
136
|
+
return meta.reviewer_independence;
|
|
137
|
+
}
|
|
138
|
+
if (meta.reviewer_independence) return 'unknown';
|
|
139
|
+
if (meta.reviewer && meta.author && meta.reviewer !== meta.author) {
|
|
140
|
+
return 'independent-review';
|
|
141
|
+
}
|
|
142
|
+
if (meta.reviewer && meta.author && meta.reviewer === meta.author) {
|
|
143
|
+
return 'self-review';
|
|
144
|
+
}
|
|
145
|
+
return 'unknown';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function resolveScoreStatus(reviewerIndependence, meta = {}) {
|
|
149
|
+
if (reviewerIndependence !== 'independent-review') return 'provisional';
|
|
150
|
+
const reviewSession = meta.review_session_id || meta.reviewer_session_id || null;
|
|
151
|
+
const authorSession = meta.author_session_id || null;
|
|
152
|
+
if (reviewSession && authorSession && reviewSession !== authorSession) {
|
|
153
|
+
return 'verified';
|
|
154
|
+
}
|
|
155
|
+
return 'provisional';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function computeSpecQualityScore(checkResults, meta = {}) {
|
|
159
|
+
if (!checkResults || typeof checkResults !== 'object') {
|
|
160
|
+
return {
|
|
161
|
+
q3_spec_quality_score: null,
|
|
162
|
+
category_scores: {},
|
|
163
|
+
reviewer_independence: resolveReviewerIndependence(meta),
|
|
164
|
+
score_status: 'provisional',
|
|
165
|
+
availability: false,
|
|
166
|
+
availability_reason: 'invalid-total',
|
|
167
|
+
warnings: 0,
|
|
168
|
+
suggestions: 0,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const total = Number.isFinite(checkResults.total) ? checkResults.total : null;
|
|
173
|
+
const errors = Number.isFinite(checkResults.errors) ? checkResults.errors : 0;
|
|
174
|
+
const warnings = Number.isFinite(checkResults.warnings) ? checkResults.warnings : 0;
|
|
175
|
+
const suggestions = Number.isFinite(checkResults.suggestions) ? checkResults.suggestions : 0;
|
|
176
|
+
|
|
177
|
+
if (total === null || total <= 0) {
|
|
178
|
+
const reviewerIndependence = resolveReviewerIndependence(meta);
|
|
179
|
+
return {
|
|
180
|
+
q3_spec_quality_score: null,
|
|
181
|
+
category_scores: {},
|
|
182
|
+
reviewer_independence: reviewerIndependence,
|
|
183
|
+
score_status: resolveScoreStatus(reviewerIndependence, meta),
|
|
184
|
+
availability: false,
|
|
185
|
+
availability_reason: 'invalid-total',
|
|
186
|
+
warnings,
|
|
187
|
+
suggestions,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const categoryScores = {};
|
|
192
|
+
const categories = checkResults.categories && typeof checkResults.categories === 'object'
|
|
193
|
+
? checkResults.categories
|
|
194
|
+
: {};
|
|
195
|
+
for (const [name, entry] of Object.entries(categories)) {
|
|
196
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
197
|
+
const catTotal = Number.isFinite(entry.total) ? entry.total : null;
|
|
198
|
+
const passed = Number.isFinite(entry.passed) ? entry.passed : null;
|
|
199
|
+
if (catTotal === null || catTotal <= 0 || passed === null) continue;
|
|
200
|
+
categoryScores[name] = round(clamp((passed / catTotal) * 100, 0, 100));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const reviewerIndependence = resolveReviewerIndependence(meta);
|
|
204
|
+
const scoreStatus = resolveScoreStatus(reviewerIndependence, meta);
|
|
205
|
+
const q3 = round(clamp(((total - errors) / total) * 100, 0, 100));
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
q3_spec_quality_score: q3,
|
|
209
|
+
category_scores: categoryScores,
|
|
210
|
+
reviewer_independence: reviewerIndependence,
|
|
211
|
+
score_status: scoreStatus,
|
|
212
|
+
availability: true,
|
|
213
|
+
availability_reason: null,
|
|
214
|
+
warnings,
|
|
215
|
+
suggestions,
|
|
216
|
+
total,
|
|
217
|
+
errors,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function computeScenarioDeliveryEfficiency({ total_stage_duration_ms, scenario_count, scenario_sources }) {
|
|
222
|
+
const total = Number.isFinite(total_stage_duration_ms) ? total_stage_duration_ms : null;
|
|
223
|
+
const count = Number.isFinite(scenario_count) ? scenario_count : 0;
|
|
224
|
+
|
|
225
|
+
if (count <= 0) {
|
|
226
|
+
return {
|
|
227
|
+
e1_scenario_delivery_efficiency_ms: null,
|
|
228
|
+
scenario_count: count,
|
|
229
|
+
total_stage_duration_ms: total,
|
|
230
|
+
scenario_sources: scenario_sources || [],
|
|
231
|
+
availability: false,
|
|
232
|
+
availability_reason: 'no-scenarios',
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (total === null || total < 0) {
|
|
237
|
+
return {
|
|
238
|
+
e1_scenario_delivery_efficiency_ms: null,
|
|
239
|
+
scenario_count: count,
|
|
240
|
+
total_stage_duration_ms: total,
|
|
241
|
+
scenario_sources: scenario_sources || [],
|
|
242
|
+
availability: false,
|
|
243
|
+
availability_reason: 'invalid-duration',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
e1_scenario_delivery_efficiency_ms: round(total / count),
|
|
249
|
+
scenario_count: count,
|
|
250
|
+
total_stage_duration_ms: total,
|
|
251
|
+
scenario_sources: scenario_sources || [],
|
|
252
|
+
availability: true,
|
|
253
|
+
availability_reason: null,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function metricAvailable(value) {
|
|
258
|
+
return Number.isFinite(value);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function buildPendingUpstreamMetric(value, source) {
|
|
262
|
+
if (metricAvailable(value)) {
|
|
263
|
+
return { value: round(value), status: 'available', source: source || null };
|
|
264
|
+
}
|
|
265
|
+
return { value: null, status: 'pending-upstream', source: source || null };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function computeV3ChangeMetrics(context = {}) {
|
|
269
|
+
if (!context || typeof context !== 'object') {
|
|
270
|
+
throw createInvalidArgError('invalid-arg', 'computeV3ChangeMetrics expects a context object');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const changeType = validateChangeType(context.change_type);
|
|
274
|
+
const e1 = computeScenarioDeliveryEfficiency({
|
|
275
|
+
total_stage_duration_ms: context.total_stage_duration_ms,
|
|
276
|
+
scenario_count: context.scenario_count,
|
|
277
|
+
scenario_sources: context.scenario_sources,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const q3Result = context.q3 && typeof context.q3 === 'object' && 'q3_spec_quality_score' in context.q3
|
|
281
|
+
? context.q3
|
|
282
|
+
: computeSpecQualityScore(context.check_results, context.check_meta);
|
|
283
|
+
|
|
284
|
+
const totalStageDuration = Number.isFinite(context.total_stage_duration_ms)
|
|
285
|
+
? context.total_stage_duration_ms
|
|
286
|
+
: null;
|
|
287
|
+
const codingDuration = Number.isFinite(context.coding_duration_ms) ? context.coding_duration_ms : 0;
|
|
288
|
+
const specDuration = Number.isFinite(context.spec_duration_ms) ? context.spec_duration_ms : 0;
|
|
289
|
+
const reworkCodingDuration = Number.isFinite(context.rework_coding_duration_ms)
|
|
290
|
+
? context.rework_coding_duration_ms
|
|
291
|
+
: 0;
|
|
292
|
+
const reworkSpecDuration = Number.isFinite(context.rework_spec_duration_ms)
|
|
293
|
+
? context.rework_spec_duration_ms
|
|
294
|
+
: 0;
|
|
295
|
+
|
|
296
|
+
const ratio = (numerator) => (
|
|
297
|
+
totalStageDuration > 0 ? round(numerator / totalStageDuration) : null
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const p4 = Number.isFinite(context.p4_quality_gate_enforcement_rate)
|
|
301
|
+
? context.p4_quality_gate_enforcement_rate
|
|
302
|
+
: null;
|
|
303
|
+
const pC = Number.isFinite(context.p_c_stage_closure_rate)
|
|
304
|
+
? context.p_c_stage_closure_rate
|
|
305
|
+
: null;
|
|
306
|
+
const pH = Number.isFinite(context.p_h_telemetry_health_score)
|
|
307
|
+
? context.p_h_telemetry_health_score
|
|
308
|
+
: null;
|
|
309
|
+
|
|
310
|
+
const reworkSummary = context.rework_summary && typeof context.rework_summary === 'object'
|
|
311
|
+
? context.rework_summary
|
|
312
|
+
: {};
|
|
313
|
+
const pRAvg = Number.isFinite(reworkSummary.avg_rework_count)
|
|
314
|
+
? reworkSummary.avg_rework_count
|
|
315
|
+
: (Number.isFinite(reworkSummary.total_rework_attempts) ? reworkSummary.total_rework_attempts : null);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
change_name: context.change_name || null,
|
|
319
|
+
change_type: changeType,
|
|
320
|
+
efficiency: {
|
|
321
|
+
e1_scenario_delivery_efficiency_ms: e1.e1_scenario_delivery_efficiency_ms,
|
|
322
|
+
scenario_count: e1.scenario_count,
|
|
323
|
+
total_stage_duration_ms: e1.total_stage_duration_ms,
|
|
324
|
+
canonical_stage_duration_ms: context.canonical_stage_duration_ms ?? null,
|
|
325
|
+
rework_stage_duration_ms: context.rework_stage_duration_ms ?? null,
|
|
326
|
+
scenario_sources: e1.scenario_sources,
|
|
327
|
+
e1_availability: e1.availability,
|
|
328
|
+
e1_availability_reason: e1.availability_reason,
|
|
329
|
+
e2_coding_time_ratio: context.e2_coding_time_ratio ?? ratio(codingDuration),
|
|
330
|
+
e3_spec_time_ratio: context.e3_spec_time_ratio ?? ratio(specDuration),
|
|
331
|
+
e2_coding_time_ratio_including_rework: context.e2_coding_time_ratio_including_rework
|
|
332
|
+
?? ratio(codingDuration + reworkCodingDuration),
|
|
333
|
+
e3_spec_time_ratio_including_rework: context.e3_spec_time_ratio_including_rework
|
|
334
|
+
?? ratio(specDuration + reworkSpecDuration),
|
|
335
|
+
},
|
|
336
|
+
quality: {
|
|
337
|
+
q3_spec_quality_score: q3Result.q3_spec_quality_score,
|
|
338
|
+
q3_category_scores: q3Result.category_scores,
|
|
339
|
+
q3_reviewer_independence: q3Result.reviewer_independence,
|
|
340
|
+
q3_score_status: q3Result.score_status,
|
|
341
|
+
q3_availability: q3Result.availability,
|
|
342
|
+
q3_availability_reason: q3Result.availability_reason,
|
|
343
|
+
q1_spec_conformance: buildPendingUpstreamMetric(context.q1_spec_conformance, context.q1_source),
|
|
344
|
+
q4_tdd_coverage: buildPendingUpstreamMetric(context.q4_tdd_coverage, context.q4_source),
|
|
345
|
+
},
|
|
346
|
+
process: {
|
|
347
|
+
p4_quality_gate_enforcement_rate: p4,
|
|
348
|
+
p_c_stage_closure_rate: pC,
|
|
349
|
+
rework_summary: {
|
|
350
|
+
total_rework_attempts: reworkSummary.total_rework_attempts ?? null,
|
|
351
|
+
avg_rework_count: pRAvg,
|
|
352
|
+
reasons: reworkSummary.reasons || reworkSummary.by_category || {},
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
telemetry_health: {
|
|
356
|
+
p_h_telemetry_health_score: pH,
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function pickMetricValue(changeMetric, metricKey) {
|
|
362
|
+
if (!changeMetric || typeof changeMetric !== 'object') return null;
|
|
363
|
+
switch (metricKey) {
|
|
364
|
+
case 'e1':
|
|
365
|
+
return changeMetric.efficiency?.e1_scenario_delivery_efficiency_ms ?? null;
|
|
366
|
+
case 'q3':
|
|
367
|
+
return changeMetric.quality?.q3_spec_quality_score ?? null;
|
|
368
|
+
case 'p4':
|
|
369
|
+
return changeMetric.process?.p4_quality_gate_enforcement_rate ?? null;
|
|
370
|
+
case 'p_r':
|
|
371
|
+
return changeMetric.process?.rework_summary?.avg_rework_count
|
|
372
|
+
?? changeMetric.process?.rework_summary?.total_rework_attempts
|
|
373
|
+
?? null;
|
|
374
|
+
case 'p_h':
|
|
375
|
+
return changeMetric.telemetry_health?.p_h_telemetry_health_score ?? null;
|
|
376
|
+
case 'p_c':
|
|
377
|
+
return changeMetric.process?.p_c_stage_closure_rate ?? null;
|
|
378
|
+
default:
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function aggregateByChangeType(changeMetrics, metricSelector, options = {}) {
|
|
384
|
+
if (!Array.isArray(changeMetrics)) {
|
|
385
|
+
throw createInvalidArgError('invalid-arg', 'aggregateByChangeType expects changeMetrics array');
|
|
386
|
+
}
|
|
387
|
+
const selector = typeof metricSelector === 'function'
|
|
388
|
+
? metricSelector
|
|
389
|
+
: (metric) => pickMetricValue(metric, metricSelector);
|
|
390
|
+
|
|
391
|
+
const includeUnknown = options.includeUnknown !== false;
|
|
392
|
+
const grouped = new Map();
|
|
393
|
+
|
|
394
|
+
for (const metric of changeMetrics) {
|
|
395
|
+
const changeType = validateChangeType(metric.change_type);
|
|
396
|
+
if (!includeUnknown && changeType === 'unknown') continue;
|
|
397
|
+
if (!grouped.has(changeType)) grouped.set(changeType, []);
|
|
398
|
+
const value = selector(metric);
|
|
399
|
+
if (Number.isFinite(value)) grouped.get(changeType).push(value);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const result = {};
|
|
403
|
+
for (const [changeType, values] of grouped.entries()) {
|
|
404
|
+
result[changeType] = summarizeDistribution(values);
|
|
405
|
+
}
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function aggregateV3Metrics(changeMetrics, options = {}) {
|
|
410
|
+
if (!Array.isArray(changeMetrics)) {
|
|
411
|
+
throw createInvalidArgError('invalid-arg', 'aggregateV3Metrics expects changeMetrics array');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const metricKeys = options.metricKeys || ['e1', 'q3', 'p4', 'p_r', 'p_h', 'p_c'];
|
|
415
|
+
const byChangeType = {};
|
|
416
|
+
const distribution = {};
|
|
417
|
+
|
|
418
|
+
for (const key of metricKeys) {
|
|
419
|
+
byChangeType[key] = aggregateByChangeType(changeMetrics, key, options);
|
|
420
|
+
const allValues = changeMetrics
|
|
421
|
+
.map(m => pickMetricValue(m, key))
|
|
422
|
+
.filter(v => Number.isFinite(v));
|
|
423
|
+
distribution[key] = summarizeDistribution(allValues);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const unknownCount = changeMetrics.filter(m => validateChangeType(m.change_type) === 'unknown').length;
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
by_change: changeMetrics,
|
|
430
|
+
by_change_type: byChangeType,
|
|
431
|
+
distribution,
|
|
432
|
+
unknown_change_type_count: unknownCount,
|
|
433
|
+
change_count: changeMetrics.length,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function parseIsoDate(dateStr) {
|
|
438
|
+
if (typeof dateStr !== 'string') return null;
|
|
439
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateStr.trim());
|
|
440
|
+
if (!match) return null;
|
|
441
|
+
const year = Number(match[1]);
|
|
442
|
+
const month = Number(match[2]) - 1;
|
|
443
|
+
const day = Number(match[3]);
|
|
444
|
+
const date = new Date(Date.UTC(year, month, day));
|
|
445
|
+
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month || date.getUTCDate() !== day) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
return date;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function formatIsoDate(date) {
|
|
452
|
+
const y = date.getUTCFullYear();
|
|
453
|
+
const m = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
454
|
+
const d = String(date.getUTCDate()).padStart(2, '0');
|
|
455
|
+
return `${y}-${m}-${d}`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function addDaysUtc(date, days) {
|
|
459
|
+
const copy = new Date(date.getTime());
|
|
460
|
+
copy.setUTCDate(copy.getUTCDate() + days);
|
|
461
|
+
return copy;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function buildBiweeklyPeriods(dateFrom, dateTo) {
|
|
465
|
+
const start = parseIsoDate(dateFrom);
|
|
466
|
+
const end = parseIsoDate(dateTo);
|
|
467
|
+
if (!start || !end) {
|
|
468
|
+
throw createInvalidArgError('invalid-date', 'biweekly window requires valid dateFrom/dateTo');
|
|
469
|
+
}
|
|
470
|
+
const diffDays = Math.round((end.getTime() - start.getTime()) / 86400000) + 1;
|
|
471
|
+
if (diffDays !== 14) {
|
|
472
|
+
throw createInvalidArgError('invalid-window', 'biweekly window must span exactly 14 days');
|
|
473
|
+
}
|
|
474
|
+
if (end.getTime() < start.getTime()) {
|
|
475
|
+
throw createInvalidArgError('invalid-window', 'dateFrom must not be after dateTo');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const periods = [];
|
|
479
|
+
for (let i = 0; i < 3; i += 1) {
|
|
480
|
+
const periodStart = addDaysUtc(start, -14 * i);
|
|
481
|
+
const periodEnd = addDaysUtc(end, -14 * i);
|
|
482
|
+
periods.push({
|
|
483
|
+
period: 'biweekly',
|
|
484
|
+
index: i,
|
|
485
|
+
label: i === 0 ? 'current' : `prior-${i}`,
|
|
486
|
+
date_from: formatIsoDate(periodStart),
|
|
487
|
+
date_to: formatIsoDate(periodEnd),
|
|
488
|
+
length_days: 14,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
return periods;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function buildMonthlyPeriods(dateFrom, dateTo) {
|
|
495
|
+
const start = parseIsoDate(dateFrom);
|
|
496
|
+
const end = parseIsoDate(dateTo);
|
|
497
|
+
if (!start || !end) {
|
|
498
|
+
throw createInvalidArgError('invalid-date', 'monthly window requires valid dateFrom/dateTo');
|
|
499
|
+
}
|
|
500
|
+
if (end.getTime() < start.getTime()) {
|
|
501
|
+
throw createInvalidArgError('invalid-window', 'dateFrom must not be after dateTo');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const monthStart = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), 1));
|
|
505
|
+
const monthEnd = new Date(Date.UTC(end.getUTCFullYear(), end.getUTCMonth() + 1, 0));
|
|
506
|
+
const sameNaturalMonth = start.getUTCFullYear() === end.getUTCFullYear()
|
|
507
|
+
&& start.getUTCMonth() === end.getUTCMonth();
|
|
508
|
+
if (!sameNaturalMonth
|
|
509
|
+
|| formatIsoDate(monthStart) !== formatIsoDate(start)
|
|
510
|
+
|| formatIsoDate(monthEnd) !== formatIsoDate(end)) {
|
|
511
|
+
throw createInvalidArgError('invalid-window', 'monthly window must cover a full natural month');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const periods = [];
|
|
515
|
+
for (let i = 0; i < 3; i += 1) {
|
|
516
|
+
const year = start.getUTCFullYear();
|
|
517
|
+
const month = start.getUTCMonth() - i;
|
|
518
|
+
const periodStart = new Date(Date.UTC(year, month, 1));
|
|
519
|
+
const periodEnd = new Date(Date.UTC(year, month + 1, 0));
|
|
520
|
+
periods.push({
|
|
521
|
+
period: 'monthly',
|
|
522
|
+
index: i,
|
|
523
|
+
label: i === 0 ? 'current' : `prior-${i}`,
|
|
524
|
+
date_from: formatIsoDate(periodStart),
|
|
525
|
+
date_to: formatIsoDate(periodEnd),
|
|
526
|
+
length_days: periodEnd.getUTCDate(),
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
return periods;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function buildTrackingPeriods(window = {}) {
|
|
533
|
+
if (!window || typeof window !== 'object') {
|
|
534
|
+
throw createInvalidArgError('invalid-arg', 'buildTrackingPeriods expects a window object');
|
|
535
|
+
}
|
|
536
|
+
if (window.period === 'biweekly') {
|
|
537
|
+
return buildBiweeklyPeriods(window.dateFrom, window.dateTo);
|
|
538
|
+
}
|
|
539
|
+
if (window.period === 'monthly') {
|
|
540
|
+
return buildMonthlyPeriods(window.dateFrom, window.dateTo);
|
|
541
|
+
}
|
|
542
|
+
throw createInvalidArgError('invalid-period', `unsupported period: ${window.period}`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function readPeriodMetric(period, metricKey) {
|
|
546
|
+
if (!period || typeof period !== 'object') return null;
|
|
547
|
+
const metrics = period.metrics || period;
|
|
548
|
+
if (metrics[metricKey] !== undefined) {
|
|
549
|
+
const entry = metrics[metricKey];
|
|
550
|
+
if (entry && typeof entry === 'object' && 'value' in entry) {
|
|
551
|
+
return Number.isFinite(entry.value) ? entry.value : null;
|
|
552
|
+
}
|
|
553
|
+
return Number.isFinite(entry) ? entry : null;
|
|
554
|
+
}
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function metricHasData(period, metricKey) {
|
|
559
|
+
const value = readPeriodMetric(period, metricKey);
|
|
560
|
+
return Number.isFinite(value);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function isHigherBetter(metricKey, thresholds = ALERT_THRESHOLDS) {
|
|
564
|
+
return thresholds[metricKey]?.direction !== 'lower_better';
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function isDeterioration(prev, current, metricKey, thresholds = ALERT_THRESHOLDS) {
|
|
568
|
+
if (!Number.isFinite(prev) || !Number.isFinite(current)) return false;
|
|
569
|
+
return isHigherBetter(metricKey, thresholds) ? current < prev : current > prev;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function evaluateAlerts(periods, thresholds = ALERT_THRESHOLDS) {
|
|
573
|
+
if (!Array.isArray(periods)) {
|
|
574
|
+
throw createInvalidArgError('invalid-arg', 'evaluateAlerts expects periods array');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const alerts = [];
|
|
578
|
+
const alertKeys = ['q3', 'p4', 'p_h', 'p_c'];
|
|
579
|
+
|
|
580
|
+
for (const metricKey of alertKeys) {
|
|
581
|
+
const cfg = thresholds[metricKey];
|
|
582
|
+
if (!cfg) continue;
|
|
583
|
+
|
|
584
|
+
const values = periods.map(p => readPeriodMetric(p, metricKey));
|
|
585
|
+
const hasAny = values.some(v => Number.isFinite(v));
|
|
586
|
+
const hasMissing = periods.some(p => !metricHasData(p, metricKey));
|
|
587
|
+
|
|
588
|
+
if (!hasAny) {
|
|
589
|
+
alerts.push({
|
|
590
|
+
level: 'data-gap',
|
|
591
|
+
metric: metricKey,
|
|
592
|
+
reason: 'missing-data',
|
|
593
|
+
periods: periods.map(p => p.label || p.date_from),
|
|
594
|
+
});
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (hasMissing) {
|
|
599
|
+
alerts.push({
|
|
600
|
+
level: 'data-gap',
|
|
601
|
+
metric: metricKey,
|
|
602
|
+
reason: 'partial-data',
|
|
603
|
+
periods: periods.map(p => p.label || p.date_from),
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const current = values[0];
|
|
608
|
+
const prior = values[1];
|
|
609
|
+
const prior2 = values[2];
|
|
610
|
+
|
|
611
|
+
if (Number.isFinite(current)) {
|
|
612
|
+
if (current < cfg.red) {
|
|
613
|
+
alerts.push({
|
|
614
|
+
level: 'red',
|
|
615
|
+
metric: metricKey,
|
|
616
|
+
reason: 'below-red-threshold',
|
|
617
|
+
value: current,
|
|
618
|
+
threshold: cfg.red,
|
|
619
|
+
});
|
|
620
|
+
} else if (current < cfg.yellow) {
|
|
621
|
+
alerts.push({
|
|
622
|
+
level: 'yellow',
|
|
623
|
+
metric: metricKey,
|
|
624
|
+
reason: 'below-yellow-threshold',
|
|
625
|
+
value: current,
|
|
626
|
+
threshold: cfg.yellow,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (metricKey === 'q3' || metricKey === 'p4') {
|
|
632
|
+
if (Number.isFinite(current) && Number.isFinite(prior) && isDeterioration(prior, current, metricKey, thresholds)) {
|
|
633
|
+
alerts.push({
|
|
634
|
+
level: 'yellow',
|
|
635
|
+
metric: metricKey,
|
|
636
|
+
reason: 'single-period-deterioration',
|
|
637
|
+
current,
|
|
638
|
+
prior,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
if (Number.isFinite(current) && Number.isFinite(prior) && Number.isFinite(prior2)
|
|
642
|
+
&& isDeterioration(prior, current, metricKey, thresholds)
|
|
643
|
+
&& isDeterioration(prior2, prior, metricKey, thresholds)) {
|
|
644
|
+
alerts.push({
|
|
645
|
+
level: 'red',
|
|
646
|
+
metric: metricKey,
|
|
647
|
+
reason: 'consecutive-deterioration',
|
|
648
|
+
values: [current, prior, prior2],
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (metricKey === 'p_h' && Number.isFinite(current) && Number.isFinite(prior)) {
|
|
654
|
+
const drop = prior - current;
|
|
655
|
+
if (drop > 20) {
|
|
656
|
+
alerts.push({
|
|
657
|
+
level: 'yellow',
|
|
658
|
+
metric: metricKey,
|
|
659
|
+
reason: 'ph-drop-over-20-points',
|
|
660
|
+
current,
|
|
661
|
+
prior,
|
|
662
|
+
drop,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const q1Current = readPeriodMetric(periods[0], 'q1');
|
|
669
|
+
const q1PriorValues = periods.slice(1).map(p => readPeriodMetric(p, 'q1'));
|
|
670
|
+
const q1HadDataBefore = q1PriorValues.some(v => Number.isFinite(v));
|
|
671
|
+
if (!q1HadDataBefore && (q1Current === 0 || q1Current === 1 || q1Current === 100)) {
|
|
672
|
+
alerts.push({
|
|
673
|
+
level: 'blue',
|
|
674
|
+
metric: 'q1',
|
|
675
|
+
reason: 'first-boundary-value',
|
|
676
|
+
value: q1Current,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return alerts;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function evaluateMaturity(metrics = {}, evidence = {}) {
|
|
684
|
+
const hasSddFlow = Boolean(evidence.has_sdd_flow ?? metrics.has_sdd_flow);
|
|
685
|
+
const met = [];
|
|
686
|
+
const unmet = [];
|
|
687
|
+
const missingEvidence = [];
|
|
688
|
+
|
|
689
|
+
const p4 = metrics.p4_quality_gate_enforcement_rate ?? metrics.p4 ?? null;
|
|
690
|
+
const pC = metrics.p_c_stage_closure_rate ?? metrics.p_c ?? null;
|
|
691
|
+
const q1 = metrics.q1_spec_conformance ?? metrics.q1 ?? null;
|
|
692
|
+
const q3 = metrics.q3_spec_quality_score ?? metrics.q3 ?? null;
|
|
693
|
+
const pR = metrics.p_r_avg_rework_count ?? metrics.p_r ?? null;
|
|
694
|
+
const e1TargetMet = evidence.e1_type_target_met === true;
|
|
695
|
+
|
|
696
|
+
const criteria = [];
|
|
697
|
+
|
|
698
|
+
if (!hasSddFlow) {
|
|
699
|
+
return {
|
|
700
|
+
current_level: 'L0',
|
|
701
|
+
eligible_max_level: evidence.code_fully_spec_generated === true ? 'L4' : 'L3',
|
|
702
|
+
met_criteria: [],
|
|
703
|
+
unmet_criteria: ['has_sdd_flow'],
|
|
704
|
+
missing_evidence: ['has_sdd_flow'],
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
criteria.push({ id: 'l1_flow', level: 'L1', met: true });
|
|
709
|
+
met.push('l1_flow');
|
|
710
|
+
|
|
711
|
+
const l2Checks = [
|
|
712
|
+
{ id: 'l2_p4', met: Number.isFinite(p4) && p4 >= 0.8 },
|
|
713
|
+
{ id: 'l2_p_c', met: Number.isFinite(pC) && pC >= 0.95 },
|
|
714
|
+
{ id: 'l2_q1', met: Number.isFinite(q1) && q1 >= 0.8 },
|
|
715
|
+
];
|
|
716
|
+
const l2Met = l2Checks.every(c => c.met);
|
|
717
|
+
for (const check of l2Checks) {
|
|
718
|
+
(check.met ? met : unmet).push(check.id);
|
|
719
|
+
if (!check.met && !Number.isFinite(
|
|
720
|
+
check.id === 'l2_p4' ? p4 : check.id === 'l2_p_c' ? pC : q1,
|
|
721
|
+
)) {
|
|
722
|
+
missingEvidence.push(check.id);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (l2Met) criteria.push({ id: 'l2', level: 'L2', met: true });
|
|
726
|
+
|
|
727
|
+
const l3Checks = [
|
|
728
|
+
{ id: 'l3_q3', met: Number.isFinite(q3) && q3 >= 95 },
|
|
729
|
+
{ id: 'l3_e1', met: e1TargetMet },
|
|
730
|
+
{ id: 'l3_p4', met: Number.isFinite(p4) && p4 >= 0.95 },
|
|
731
|
+
{ id: 'l3_p_r', met: Number.isFinite(pR) && pR <= 1 },
|
|
732
|
+
{ id: 'l3_q1', met: Number.isFinite(q1) && q1 >= 0.9 },
|
|
733
|
+
];
|
|
734
|
+
const l3Met = l3Checks.every(c => c.met);
|
|
735
|
+
for (const check of l3Checks) {
|
|
736
|
+
(check.met ? met : unmet).push(check.id);
|
|
737
|
+
if (!check.met) {
|
|
738
|
+
if (check.id === 'l3_e1' && evidence.e1_type_target_met !== true) {
|
|
739
|
+
missingEvidence.push('e1_type_target');
|
|
740
|
+
} else if (!Number.isFinite(
|
|
741
|
+
check.id === 'l3_q3' ? q3
|
|
742
|
+
: check.id === 'l3_p4' ? p4
|
|
743
|
+
: check.id === 'l3_p_r' ? pR
|
|
744
|
+
: q1,
|
|
745
|
+
)) {
|
|
746
|
+
missingEvidence.push(check.id);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (l3Met) criteria.push({ id: 'l3', level: 'L3', met: true });
|
|
751
|
+
|
|
752
|
+
let currentLevel = 'L1';
|
|
753
|
+
if (l3Met) currentLevel = 'L3';
|
|
754
|
+
else if (l2Met) currentLevel = 'L2';
|
|
755
|
+
|
|
756
|
+
let eligibleMaxLevel = 'L3';
|
|
757
|
+
if (evidence.code_fully_spec_generated === true) {
|
|
758
|
+
eligibleMaxLevel = 'L4';
|
|
759
|
+
if (currentLevel === 'L3') currentLevel = 'L4';
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return {
|
|
763
|
+
current_level: currentLevel,
|
|
764
|
+
eligible_max_level: eligibleMaxLevel,
|
|
765
|
+
met_criteria: met,
|
|
766
|
+
unmet_criteria: unmet,
|
|
767
|
+
missing_evidence: [...new Set(missingEvidence)],
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function buildMinimumMetricStatus(metrics = {}) {
|
|
772
|
+
const efficiency = metrics.efficiency || metrics;
|
|
773
|
+
const quality = metrics.quality || metrics;
|
|
774
|
+
const processMetrics = metrics.process || metrics;
|
|
775
|
+
const telemetry = metrics.telemetry_health || metrics;
|
|
776
|
+
const metricEvidence = metrics.metric_evidence && typeof metrics.metric_evidence === 'object'
|
|
777
|
+
? metrics.metric_evidence
|
|
778
|
+
: {};
|
|
779
|
+
|
|
780
|
+
const entries = {
|
|
781
|
+
e1: efficiency.e1_scenario_delivery_efficiency_ms ?? efficiency.e1 ?? null,
|
|
782
|
+
q3: quality.q3_spec_quality_score ?? quality.q3 ?? null,
|
|
783
|
+
p4: processMetrics.p4_quality_gate_enforcement_rate ?? processMetrics.p4 ?? null,
|
|
784
|
+
p_r: processMetrics.rework_summary?.avg_rework_count
|
|
785
|
+
?? processMetrics.rework_summary?.total_rework_attempts
|
|
786
|
+
?? processMetrics.p_r
|
|
787
|
+
?? null,
|
|
788
|
+
p_h: telemetry.p_h_telemetry_health_score ?? telemetry.p_h ?? null,
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
const available = [];
|
|
792
|
+
const missing = [];
|
|
793
|
+
const evidenceStatus = {};
|
|
794
|
+
|
|
795
|
+
for (const key of MINIMUM_METRICS) {
|
|
796
|
+
if (!Number.isFinite(entries[key])) {
|
|
797
|
+
missing.push(key);
|
|
798
|
+
evidenceStatus[key] = 'missing';
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
available.push(key);
|
|
802
|
+
const declaredStatus = metricEvidence[key]?.status;
|
|
803
|
+
if (declaredStatus === 'trusted' || declaredStatus === 'provisional') {
|
|
804
|
+
evidenceStatus[key] = declaredStatus;
|
|
805
|
+
} else if (key === 'q3') {
|
|
806
|
+
evidenceStatus[key] = quality.q3_score_status === 'verified'
|
|
807
|
+
&& quality.q3_reviewer_independence === 'independent-review'
|
|
808
|
+
? 'trusted'
|
|
809
|
+
: 'provisional';
|
|
810
|
+
} else {
|
|
811
|
+
evidenceStatus[key] = 'provisional';
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const trustedMetrics = MINIMUM_METRICS.filter(key => evidenceStatus[key] === 'trusted');
|
|
816
|
+
const provisionalMetrics = MINIMUM_METRICS.filter(key => evidenceStatus[key] === 'provisional');
|
|
817
|
+
return {
|
|
818
|
+
minimum_metrics: MINIMUM_METRICS,
|
|
819
|
+
available,
|
|
820
|
+
missing,
|
|
821
|
+
coverage_rate: MINIMUM_METRICS.length > 0
|
|
822
|
+
? round(available.length / MINIMUM_METRICS.length)
|
|
823
|
+
: 0,
|
|
824
|
+
numeric_available: available.length,
|
|
825
|
+
numeric_total: MINIMUM_METRICS.length,
|
|
826
|
+
numeric_coverage_rate: MINIMUM_METRICS.length > 0
|
|
827
|
+
? round(available.length / MINIMUM_METRICS.length)
|
|
828
|
+
: 0,
|
|
829
|
+
trusted_available: trustedMetrics.length,
|
|
830
|
+
trusted_metrics: trustedMetrics,
|
|
831
|
+
provisional: provisionalMetrics.length,
|
|
832
|
+
provisional_metrics: provisionalMetrics,
|
|
833
|
+
missing_count: missing.length,
|
|
834
|
+
trusted_coverage_rate: MINIMUM_METRICS.length > 0
|
|
835
|
+
? round(trustedMetrics.length / MINIMUM_METRICS.length)
|
|
836
|
+
: 0,
|
|
837
|
+
evidence_status: evidenceStatus,
|
|
838
|
+
evidence: metricEvidence,
|
|
839
|
+
entries,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function buildSchemeCompliance(input = {}) {
|
|
844
|
+
const minimum = input.minimum_metric_set || {};
|
|
845
|
+
const reportMetrics = input.metrics || {};
|
|
846
|
+
const efficiency = reportMetrics.efficiency || {};
|
|
847
|
+
const quality = reportMetrics.quality || {};
|
|
848
|
+
const processMetrics = reportMetrics.process || {};
|
|
849
|
+
const formats = Array.isArray(input.formats) ? input.formats : [];
|
|
850
|
+
const minimumStatus = minimum.numeric_available === minimum.numeric_total
|
|
851
|
+
? (minimum.trusted_available === minimum.numeric_total ? 'implemented' : 'provisional')
|
|
852
|
+
: 'provisional';
|
|
853
|
+
const q1Status = quality.q1_status || quality.q1_spec_conformance?.status || 'pending-upstream';
|
|
854
|
+
const q4Status = quality.q4_status || quality.q4_tdd_coverage?.status || 'pending-upstream';
|
|
855
|
+
const q3Status = quality.q3_score_status === 'verified' ? 'implemented' : 'provisional';
|
|
856
|
+
const statusLabel = status => ({
|
|
857
|
+
implemented: '已实现',
|
|
858
|
+
provisional: '部分可用',
|
|
859
|
+
'pending-upstream': '待补数据',
|
|
860
|
+
'not-applicable': '本报告不适用',
|
|
861
|
+
verified: '已验证',
|
|
862
|
+
trusted: '可信',
|
|
863
|
+
missing: '缺失',
|
|
864
|
+
unknown: '未知',
|
|
865
|
+
'independent-review': '独立复核',
|
|
866
|
+
'self-review': '自评',
|
|
867
|
+
}[status] || status || '未知');
|
|
868
|
+
|
|
869
|
+
const items = [
|
|
870
|
+
{
|
|
871
|
+
requirement: 'minimum-verifiable-metrics',
|
|
872
|
+
label: '最小可验证指标(E1、Q3、P4、P-R、P-H)',
|
|
873
|
+
status: minimumStatus,
|
|
874
|
+
evidence: `数值覆盖 ${minimum.numeric_available ?? minimum.available?.length ?? 0}/${minimum.numeric_total ?? MINIMUM_METRICS.length};可信覆盖 ${minimum.trusted_available ?? 0}/${minimum.numeric_total ?? MINIMUM_METRICS.length}`,
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
requirement: 'e2-e3-assist',
|
|
878
|
+
label: '辅助效率指标(E2、E3)',
|
|
879
|
+
status: Number.isFinite(efficiency.e2_coding_time_ratio) && Number.isFinite(efficiency.e3_spec_time_ratio)
|
|
880
|
+
? 'implemented'
|
|
881
|
+
: 'provisional',
|
|
882
|
+
evidence: '报告模型中的效率数据',
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
requirement: 'q1-q4-conditional',
|
|
886
|
+
label: '条件指标(Q1、Q4)',
|
|
887
|
+
status: q1Status === 'pending-upstream' || q4Status === 'pending-upstream'
|
|
888
|
+
? 'pending-upstream'
|
|
889
|
+
: 'implemented',
|
|
890
|
+
evidence: `Q1:${statusLabel(q1Status)};Q4:${statusLabel(q4Status)}`,
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
requirement: 'stage-closure',
|
|
894
|
+
label: '步骤闭环率(P-C)',
|
|
895
|
+
status: Number.isFinite(processMetrics.p_c_stage_closure_rate) ? 'implemented' : 'provisional',
|
|
896
|
+
evidence: '步骤开始记录与结束记录配对',
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
requirement: 'independent-reviewer',
|
|
900
|
+
label: '独立复核',
|
|
901
|
+
status: q3Status,
|
|
902
|
+
evidence: `Q3:${statusLabel(quality.q3_score_status || 'provisional')};复核方式:${statusLabel(quality.q3_reviewer_independence || 'unknown')}`,
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
requirement: 'tracking-periods',
|
|
906
|
+
label: input.level === 'change' ? '本次变更独立统计' : '双周、月度追踪',
|
|
907
|
+
status: input.level === 'change'
|
|
908
|
+
? 'implemented'
|
|
909
|
+
: (['biweekly', 'monthly'].includes(input.period) ? 'implemented' : 'not-applicable'),
|
|
910
|
+
evidence: input.level === 'change'
|
|
911
|
+
? '只包含当前 change 的阶段、任务、测试、证据和产物;不混入跨变更趋势'
|
|
912
|
+
: (input.period
|
|
913
|
+
? `当前统计周期:${input.period === 'biweekly' ? '双周' : '月度'}`
|
|
914
|
+
: '当前报告未请求周期聚合'),
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
requirement: 'change-type-percentiles',
|
|
918
|
+
label: '按变更类型与 P50/P75/P90 聚合',
|
|
919
|
+
status: input.level === 'project' ? 'implemented' : 'not-applicable',
|
|
920
|
+
evidence: input.level === 'project' ? '项目级聚合模型' : '单变更报告不展示分位数',
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
requirement: 'alert-feedback-loop',
|
|
924
|
+
label: '指标与证据告警闭环',
|
|
925
|
+
status: input.evidence_alerts ? 'implemented' : 'provisional',
|
|
926
|
+
evidence: input.evidence_alerts ? '稳定问题编号、未解决/已解决状态和历史记录' : '当前模型未提供告警生命周期',
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
requirement: 'multi-format-same-model',
|
|
930
|
+
label: 'Markdown、HTML、JSON 同源',
|
|
931
|
+
status: ['md', 'html', 'json'].every(format => formats.includes(format)) ? 'implemented' : 'provisional',
|
|
932
|
+
evidence: formats.length > 0 ? formats.map(format => ({ md: 'Markdown', html: 'HTML', json: 'JSON' }[format] || format)).join(' / ') : '格式信息未提供',
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
requirement: 'reproducible-auditable',
|
|
936
|
+
label: '可复算、可审计',
|
|
937
|
+
status: input.event_count > 0 ? 'implemented' : 'provisional',
|
|
938
|
+
evidence: input.event_count > 0 ? `${input.event_count} 条 JSONL 事件` : '未提供事件数量',
|
|
939
|
+
},
|
|
940
|
+
];
|
|
941
|
+
const counts = {};
|
|
942
|
+
for (const item of items) counts[item.status] = (counts[item.status] || 0) + 1;
|
|
943
|
+
return {
|
|
944
|
+
scheme: '3.3.4.2_SDD赋能提效评估与追踪方案',
|
|
945
|
+
version: 'V3.0',
|
|
946
|
+
items,
|
|
947
|
+
counts,
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function evidenceAlertKey(event, warning, subject) {
|
|
952
|
+
return `${warning}:${subject || event.capability || event.command || event.change || 'change'}`;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function buildEvidenceAlerts(events = []) {
|
|
956
|
+
const byKey = new Map();
|
|
957
|
+
const ordered = events.slice().sort((a, b) => (
|
|
958
|
+
new Date(a.timestamp || 0).getTime() - new Date(b.timestamp || 0).getTime()
|
|
959
|
+
));
|
|
960
|
+
for (const event of ordered) {
|
|
961
|
+
if (event?.type !== 'telemetry_warning') continue;
|
|
962
|
+
const warning = event.details?.warning || event.warning;
|
|
963
|
+
if (!warning) continue;
|
|
964
|
+
const subjects = Array.isArray(event.details?.task_ids) && event.details.task_ids.length > 0
|
|
965
|
+
? event.details.task_ids
|
|
966
|
+
: [event.details?.task_id || event.task_id || event.capability || event.command || event.change];
|
|
967
|
+
for (const subject of subjects) {
|
|
968
|
+
const key = event.details?.key || evidenceAlertKey(event, warning, subject);
|
|
969
|
+
const existing = byKey.get(key);
|
|
970
|
+
if (existing) {
|
|
971
|
+
existing.occurrences += 1;
|
|
972
|
+
existing.last_seen_event_id = event.event_id || existing.last_seen_event_id;
|
|
973
|
+
existing.last_seen_at = event.timestamp || existing.last_seen_at;
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
const severity = warning === 'apply_test_missing' ? 'error' : 'warning';
|
|
977
|
+
byKey.set(key, {
|
|
978
|
+
key,
|
|
979
|
+
category: 'evidence',
|
|
980
|
+
warning,
|
|
981
|
+
subject: subject || null,
|
|
982
|
+
severity,
|
|
983
|
+
status: 'open',
|
|
984
|
+
occurrences: 1,
|
|
985
|
+
opened_by_event_id: event.event_id || null,
|
|
986
|
+
first_seen_at: event.timestamp || null,
|
|
987
|
+
last_seen_event_id: event.event_id || null,
|
|
988
|
+
last_seen_at: event.timestamp || null,
|
|
989
|
+
resolved_by_event_id: null,
|
|
990
|
+
resolved_at: null,
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
for (const event of ordered) {
|
|
996
|
+
const resolutionKeys = [
|
|
997
|
+
event?.details?.resolves_warning_key,
|
|
998
|
+
event?.details?.resolved_warning_key,
|
|
999
|
+
...(Array.isArray(event?.details?.resolves_warning_keys) ? event.details.resolves_warning_keys : []),
|
|
1000
|
+
].filter(Boolean);
|
|
1001
|
+
for (const key of resolutionKeys) {
|
|
1002
|
+
const alert = byKey.get(key);
|
|
1003
|
+
if (!alert || alert.status === 'resolved') continue;
|
|
1004
|
+
alert.status = 'resolved';
|
|
1005
|
+
alert.resolved_by_event_id = event.event_id || null;
|
|
1006
|
+
alert.resolved_at = event.timestamp || null;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const history = Array.from(byKey.values());
|
|
1011
|
+
const open = history.filter(alert => alert.status === 'open');
|
|
1012
|
+
const resolved = history.filter(alert => alert.status === 'resolved');
|
|
1013
|
+
return {
|
|
1014
|
+
open,
|
|
1015
|
+
resolved,
|
|
1016
|
+
history,
|
|
1017
|
+
counts: {
|
|
1018
|
+
open: open.length,
|
|
1019
|
+
resolved: resolved.length,
|
|
1020
|
+
total: history.length,
|
|
1021
|
+
},
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function isDevelopmentActivity(event) {
|
|
1026
|
+
if (!event || typeof event !== 'object') return false;
|
|
1027
|
+
const type = event.type || event.event_type || null;
|
|
1028
|
+
if (type === 'telemetry_warning') return false;
|
|
1029
|
+
if (DEVELOPMENT_ACTIVITY_TYPES.has(type)) return true;
|
|
1030
|
+
if (type === 'process_note') {
|
|
1031
|
+
const kind = event.details?.kind || event.kind || null;
|
|
1032
|
+
return kind === 'fix' || kind === 'manual-action';
|
|
1033
|
+
}
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function findDevelopmentActivities(events, startMs, endMs) {
|
|
1038
|
+
if (!Array.isArray(events)) return [];
|
|
1039
|
+
const start = Number.isFinite(startMs) ? startMs : null;
|
|
1040
|
+
const end = Number.isFinite(endMs) ? endMs : null;
|
|
1041
|
+
return events.filter((event) => {
|
|
1042
|
+
if (!isDevelopmentActivity(event)) return false;
|
|
1043
|
+
const ts = Date.parse(event.timestamp || event.ts || '');
|
|
1044
|
+
if (!Number.isFinite(ts)) return false;
|
|
1045
|
+
if (start !== null && ts < start) return false;
|
|
1046
|
+
if (end !== null && ts > end) return false;
|
|
1047
|
+
return true;
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function deriveSuccessfulReworkReason(attemptContext = {}) {
|
|
1052
|
+
const baseReason = attemptContext.base_reason || attemptContext.rework_reason || null;
|
|
1053
|
+
const agentOverride = attemptContext.agent_override_reason || null;
|
|
1054
|
+
|
|
1055
|
+
if (baseReason === 'incomplete') {
|
|
1056
|
+
return {
|
|
1057
|
+
reason: 'incomplete',
|
|
1058
|
+
reason_source: 'deterministic',
|
|
1059
|
+
evidence_event_ids: [],
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
if (baseReason === 'prev-failed') {
|
|
1063
|
+
return {
|
|
1064
|
+
reason: 'prev-failed',
|
|
1065
|
+
reason_source: 'deterministic',
|
|
1066
|
+
evidence_event_ids: [],
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
if (attemptContext.is_pre_archive_recheck === true || baseReason === 'pre-archive-recheck') {
|
|
1070
|
+
return {
|
|
1071
|
+
reason: 'pre-archive-recheck',
|
|
1072
|
+
reason_source: 'deterministic',
|
|
1073
|
+
evidence_event_ids: [],
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
if (agentOverride && AGENT_OVERRIDE_REASONS.has(agentOverride)) {
|
|
1077
|
+
return {
|
|
1078
|
+
reason: agentOverride,
|
|
1079
|
+
reason_source: 'agent-override',
|
|
1080
|
+
evidence_event_ids: attemptContext.evidence_event_ids || [],
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
if (attemptContext.git_has_commit_in_window === true) {
|
|
1085
|
+
return {
|
|
1086
|
+
reason: 'code-changed-after-pass',
|
|
1087
|
+
reason_source: 'git',
|
|
1088
|
+
evidence_event_ids: attemptContext.evidence_event_ids || [],
|
|
1089
|
+
window: attemptContext.window || null,
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const devActivities = attemptContext.dev_activities
|
|
1094
|
+
|| findDevelopmentActivities(
|
|
1095
|
+
attemptContext.events || [],
|
|
1096
|
+
attemptContext.window_start_ms,
|
|
1097
|
+
attemptContext.window_end_ms,
|
|
1098
|
+
);
|
|
1099
|
+
|
|
1100
|
+
if (devActivities.length > 0) {
|
|
1101
|
+
return {
|
|
1102
|
+
reason: 'code-changed-after-pass',
|
|
1103
|
+
reason_source: 'telemetry',
|
|
1104
|
+
evidence_event_ids: devActivities.map(e => e.event_id).filter(Boolean),
|
|
1105
|
+
window: attemptContext.window || null,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (baseReason === 'completed_rework_success' || baseReason === 'recheck-no-change' || baseReason === 'unspecified') {
|
|
1110
|
+
return {
|
|
1111
|
+
reason: 'recheck-no-change',
|
|
1112
|
+
reason_source: 'telemetry-no-activity',
|
|
1113
|
+
evidence_event_ids: [],
|
|
1114
|
+
window: attemptContext.window || null,
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
return {
|
|
1119
|
+
reason: 'unspecified',
|
|
1120
|
+
reason_source: 'unknown',
|
|
1121
|
+
evidence_event_ids: [],
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
module.exports = {
|
|
1126
|
+
REPORT_SCHEMA_VERSION,
|
|
1127
|
+
METRICS_CONTRACT_VERSION,
|
|
1128
|
+
CHANGE_TYPES,
|
|
1129
|
+
ALERT_THRESHOLDS,
|
|
1130
|
+
MINIMUM_METRICS,
|
|
1131
|
+
clamp,
|
|
1132
|
+
round,
|
|
1133
|
+
percentile,
|
|
1134
|
+
summarizeDistribution,
|
|
1135
|
+
detectIqrOutliers,
|
|
1136
|
+
computeSpecQualityScore,
|
|
1137
|
+
computeScenarioDeliveryEfficiency,
|
|
1138
|
+
computeV3ChangeMetrics,
|
|
1139
|
+
aggregateV3Metrics,
|
|
1140
|
+
aggregateByChangeType,
|
|
1141
|
+
buildTrackingPeriods,
|
|
1142
|
+
evaluateAlerts,
|
|
1143
|
+
evaluateMaturity,
|
|
1144
|
+
buildMinimumMetricStatus,
|
|
1145
|
+
buildSchemeCompliance,
|
|
1146
|
+
buildEvidenceAlerts,
|
|
1147
|
+
isDevelopmentActivity,
|
|
1148
|
+
findDevelopmentActivities,
|
|
1149
|
+
deriveSuccessfulReworkReason,
|
|
1150
|
+
validateChangeType,
|
|
1151
|
+
createInvalidArgError,
|
|
1152
|
+
countScenariosInMarkdown,
|
|
1153
|
+
};
|