scene-capability-engine 3.6.45 → 3.6.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/docs/releases/README.md +1 -0
- package/docs/releases/v3.6.46.md +23 -0
- package/docs/zh/releases/README.md +1 -0
- package/docs/zh/releases/v3.6.46.md +23 -0
- package/package.json +4 -2
- package/scripts/auto-strategy-router.js +231 -0
- package/scripts/capability-mapping-report.js +339 -0
- package/scripts/check-branding-consistency.js +140 -0
- package/scripts/check-sce-tracking.js +54 -0
- package/scripts/check-skip-allowlist.js +94 -0
- package/scripts/errorbook-registry-health-gate.js +172 -0
- package/scripts/errorbook-release-gate.js +132 -0
- package/scripts/failure-attribution-repair.js +317 -0
- package/scripts/git-managed-gate.js +464 -0
- package/scripts/interactive-approval-event-projection.js +400 -0
- package/scripts/interactive-approval-workflow.js +829 -0
- package/scripts/interactive-authorization-tier-evaluate.js +413 -0
- package/scripts/interactive-change-plan-gate.js +225 -0
- package/scripts/interactive-context-bridge.js +617 -0
- package/scripts/interactive-customization-loop.js +1690 -0
- package/scripts/interactive-dialogue-governance.js +842 -0
- package/scripts/interactive-feedback-log.js +253 -0
- package/scripts/interactive-flow-smoke.js +238 -0
- package/scripts/interactive-flow.js +1059 -0
- package/scripts/interactive-governance-report.js +1112 -0
- package/scripts/interactive-intent-build.js +707 -0
- package/scripts/interactive-loop-smoke.js +215 -0
- package/scripts/interactive-moqui-adapter.js +304 -0
- package/scripts/interactive-plan-build.js +426 -0
- package/scripts/interactive-runtime-policy-evaluate.js +495 -0
- package/scripts/interactive-work-order-build.js +552 -0
- package/scripts/matrix-regression-gate.js +167 -0
- package/scripts/moqui-core-regression-suite.js +397 -0
- package/scripts/moqui-lexicon-audit.js +651 -0
- package/scripts/moqui-matrix-remediation-phased-runner.js +865 -0
- package/scripts/moqui-matrix-remediation-queue.js +852 -0
- package/scripts/moqui-metadata-extract.js +1340 -0
- package/scripts/moqui-rebuild-gate.js +167 -0
- package/scripts/moqui-release-summary.js +729 -0
- package/scripts/moqui-standard-rebuild.js +1370 -0
- package/scripts/moqui-template-baseline-report.js +682 -0
- package/scripts/npm-package-runtime-asset-check.js +221 -0
- package/scripts/problem-closure-gate.js +441 -0
- package/scripts/release-asset-integrity-check.js +216 -0
- package/scripts/release-asset-nonempty-normalize.js +166 -0
- package/scripts/release-drift-evaluate.js +223 -0
- package/scripts/release-drift-signals.js +255 -0
- package/scripts/release-governance-snapshot-export.js +132 -0
- package/scripts/release-ops-weekly-summary.js +934 -0
- package/scripts/release-risk-remediation-bundle.js +315 -0
- package/scripts/release-weekly-ops-gate.js +423 -0
- package/scripts/state-migration-reconciliation-gate.js +110 -0
- package/scripts/state-storage-tiering-audit.js +337 -0
- package/scripts/steering-content-audit.js +393 -0
- package/scripts/symbol-evidence-locate.js +366 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
function appendSummary(summaryPath, lines = []) {
|
|
8
|
+
if (!summaryPath) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
fs.appendFileSync(summaryPath, `${lines.join('\n')}\n\n`, 'utf8');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readValue(env, name, fallback = '') {
|
|
15
|
+
const value = env[name];
|
|
16
|
+
return value === undefined || value === null ? fallback : `${value}`.trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseBoolean(raw, fallback) {
|
|
20
|
+
const value = `${raw || ''}`.trim().toLowerCase();
|
|
21
|
+
if (!value) {
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(value)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(value)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeRequiredFiles(raw, tag) {
|
|
34
|
+
const defaultList = [
|
|
35
|
+
'release-gate-{tag}.json',
|
|
36
|
+
'release-gate-history-{tag}.json',
|
|
37
|
+
'release-gate-history-{tag}.md',
|
|
38
|
+
'governance-snapshot-{tag}.json',
|
|
39
|
+
'governance-snapshot-{tag}.md',
|
|
40
|
+
'weekly-ops-summary-{tag}.json',
|
|
41
|
+
'weekly-ops-summary-{tag}.md',
|
|
42
|
+
'release-risk-remediation-{tag}.json',
|
|
43
|
+
'release-risk-remediation-{tag}.md',
|
|
44
|
+
'release-risk-remediation-{tag}.lines'
|
|
45
|
+
];
|
|
46
|
+
const source = raw
|
|
47
|
+
? `${raw}`
|
|
48
|
+
.split(',')
|
|
49
|
+
.map(item => item.trim())
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
: defaultList;
|
|
52
|
+
const normalizedTag = `${tag || ''}`.trim();
|
|
53
|
+
return source.map(item => item.replace(/\{tag\}/g, normalizedTag));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function mergeGateReport(gateReportFile, payload) {
|
|
57
|
+
if (!gateReportFile) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
let gatePayload = {};
|
|
61
|
+
try {
|
|
62
|
+
if (fs.existsSync(gateReportFile)) {
|
|
63
|
+
gatePayload = JSON.parse(fs.readFileSync(gateReportFile, 'utf8'));
|
|
64
|
+
}
|
|
65
|
+
} catch (_error) {
|
|
66
|
+
gatePayload = {};
|
|
67
|
+
}
|
|
68
|
+
gatePayload.asset_integrity = payload;
|
|
69
|
+
gatePayload.updated_at = payload.evaluated_at;
|
|
70
|
+
fs.writeFileSync(gateReportFile, `${JSON.stringify(gatePayload, null, 2)}\n`, 'utf8');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function evaluateReleaseAssetIntegrity(options = {}) {
|
|
74
|
+
const env = options.env && typeof options.env === 'object'
|
|
75
|
+
? options.env
|
|
76
|
+
: process.env;
|
|
77
|
+
const now = typeof options.now === 'function'
|
|
78
|
+
? options.now
|
|
79
|
+
: () => new Date().toISOString();
|
|
80
|
+
|
|
81
|
+
const tag = readValue(env, 'RELEASE_TAG', '');
|
|
82
|
+
const baseDir = readValue(env, 'RELEASE_ASSET_INTEGRITY_DIR', '.sce/reports/release-evidence');
|
|
83
|
+
const required = normalizeRequiredFiles(readValue(env, 'RELEASE_ASSET_INTEGRITY_REQUIRED_FILES', ''), tag);
|
|
84
|
+
const enforce = parseBoolean(readValue(env, 'RELEASE_ASSET_INTEGRITY_ENFORCE', ''), true);
|
|
85
|
+
const requireNonEmpty = parseBoolean(readValue(env, 'RELEASE_ASSET_INTEGRITY_REQUIRE_NON_EMPTY', ''), true);
|
|
86
|
+
const reportJsonFile = readValue(env, 'RELEASE_ASSET_INTEGRITY_REPORT_JSON', '');
|
|
87
|
+
const reportMarkdownFile = readValue(env, 'RELEASE_ASSET_INTEGRITY_REPORT_MD', '');
|
|
88
|
+
const gateReportFile = readValue(env, 'RELEASE_GATE_REPORT_FILE', '');
|
|
89
|
+
const summaryPath = readValue(env, 'GITHUB_STEP_SUMMARY', '');
|
|
90
|
+
|
|
91
|
+
const missingFiles = [];
|
|
92
|
+
const emptyFiles = [];
|
|
93
|
+
const presentFiles = [];
|
|
94
|
+
|
|
95
|
+
for (const rel of required) {
|
|
96
|
+
const absolute = path.resolve(baseDir, rel);
|
|
97
|
+
const exists = fs.existsSync(absolute);
|
|
98
|
+
if (!exists) {
|
|
99
|
+
missingFiles.push(rel);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const stat = fs.statSync(absolute);
|
|
103
|
+
if (requireNonEmpty && (!stat || stat.size <= 0)) {
|
|
104
|
+
emptyFiles.push(rel);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
presentFiles.push({
|
|
108
|
+
file: rel,
|
|
109
|
+
size: stat.size
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const violations = [
|
|
114
|
+
...missingFiles.map(item => `missing asset: ${item}`),
|
|
115
|
+
...emptyFiles.map(item => `empty asset: ${item}`)
|
|
116
|
+
];
|
|
117
|
+
const passed = violations.length === 0;
|
|
118
|
+
const blocked = enforce && !passed;
|
|
119
|
+
const evaluatedAt = now();
|
|
120
|
+
|
|
121
|
+
const payload = {
|
|
122
|
+
mode: 'release-asset-integrity-check',
|
|
123
|
+
evaluated_at: evaluatedAt,
|
|
124
|
+
tag: tag || null,
|
|
125
|
+
dir: baseDir,
|
|
126
|
+
enforce,
|
|
127
|
+
require_non_empty: requireNonEmpty,
|
|
128
|
+
required_count: required.length,
|
|
129
|
+
required_files: required,
|
|
130
|
+
present_count: presentFiles.length,
|
|
131
|
+
present_files: presentFiles,
|
|
132
|
+
missing_files: missingFiles,
|
|
133
|
+
empty_files: emptyFiles,
|
|
134
|
+
violations,
|
|
135
|
+
passed,
|
|
136
|
+
blocked
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
if (reportJsonFile) {
|
|
140
|
+
fs.mkdirSync(path.dirname(reportJsonFile), { recursive: true });
|
|
141
|
+
fs.writeFileSync(reportJsonFile, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (reportMarkdownFile) {
|
|
145
|
+
const lines = [
|
|
146
|
+
'# Release Asset Integrity Check',
|
|
147
|
+
'',
|
|
148
|
+
`- Tag: ${payload.tag || 'n/a'}`,
|
|
149
|
+
`- Directory: ${payload.dir}`,
|
|
150
|
+
`- Enforce: ${payload.enforce}`,
|
|
151
|
+
`- Require non-empty: ${payload.require_non_empty}`,
|
|
152
|
+
`- Passed: ${payload.passed}`,
|
|
153
|
+
`- Required: ${payload.required_count}`,
|
|
154
|
+
`- Present: ${payload.present_count}`,
|
|
155
|
+
`- Missing: ${payload.missing_files.length}`,
|
|
156
|
+
`- Empty: ${payload.empty_files.length}`
|
|
157
|
+
];
|
|
158
|
+
if (payload.violations.length > 0) {
|
|
159
|
+
lines.push('', '## Violations');
|
|
160
|
+
payload.violations.forEach(item => lines.push(`- ${item}`));
|
|
161
|
+
}
|
|
162
|
+
fs.mkdirSync(path.dirname(reportMarkdownFile), { recursive: true });
|
|
163
|
+
fs.writeFileSync(reportMarkdownFile, `${lines.join('\n')}\n`, 'utf8');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
mergeGateReport(gateReportFile, payload);
|
|
167
|
+
|
|
168
|
+
const summaryLines = [
|
|
169
|
+
'## Release Asset Integrity',
|
|
170
|
+
'',
|
|
171
|
+
`- enforce: ${enforce}`,
|
|
172
|
+
`- required assets: ${required.length}`,
|
|
173
|
+
`- present assets: ${presentFiles.length}`,
|
|
174
|
+
`- missing assets: ${missingFiles.length}`,
|
|
175
|
+
`- empty assets: ${emptyFiles.length}`,
|
|
176
|
+
`- passed: ${passed}`
|
|
177
|
+
];
|
|
178
|
+
if (violations.length > 0) {
|
|
179
|
+
summaryLines.push('', '### Violations');
|
|
180
|
+
violations.forEach(item => summaryLines.push(`- ${item}`));
|
|
181
|
+
}
|
|
182
|
+
appendSummary(summaryPath, summaryLines);
|
|
183
|
+
|
|
184
|
+
console.log(
|
|
185
|
+
`[release-asset-integrity] enforce=${enforce} required=${required.length} present=${presentFiles.length} missing=${missingFiles.length} empty=${emptyFiles.length}`
|
|
186
|
+
);
|
|
187
|
+
if (!passed) {
|
|
188
|
+
console.error(`[release-asset-integrity] violations=${violations.join('; ')}`);
|
|
189
|
+
} else {
|
|
190
|
+
console.log('[release-asset-integrity] passed');
|
|
191
|
+
}
|
|
192
|
+
if (reportJsonFile) {
|
|
193
|
+
console.log(`[release-asset-integrity] json=${reportJsonFile}`);
|
|
194
|
+
}
|
|
195
|
+
if (reportMarkdownFile) {
|
|
196
|
+
console.log(`[release-asset-integrity] markdown=${reportMarkdownFile}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
exit_code: blocked ? 1 : 0,
|
|
201
|
+
blocked,
|
|
202
|
+
passed,
|
|
203
|
+
violations,
|
|
204
|
+
payload
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (require.main === module) {
|
|
209
|
+
const result = evaluateReleaseAssetIntegrity();
|
|
210
|
+
process.exit(result.exit_code);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
evaluateReleaseAssetIntegrity
|
|
215
|
+
};
|
|
216
|
+
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
function resolveKind(filePath, explicitKind = 'auto') {
|
|
8
|
+
const kind = `${explicitKind || 'auto'}`.trim().toLowerCase();
|
|
9
|
+
if (kind && kind !== 'auto') {
|
|
10
|
+
return kind;
|
|
11
|
+
}
|
|
12
|
+
if (/\.jsonl$/i.test(filePath)) {
|
|
13
|
+
return 'jsonl';
|
|
14
|
+
}
|
|
15
|
+
if (/\.json$/i.test(filePath)) {
|
|
16
|
+
return 'json';
|
|
17
|
+
}
|
|
18
|
+
if (/\.lines$/i.test(filePath)) {
|
|
19
|
+
return 'lines';
|
|
20
|
+
}
|
|
21
|
+
return 'text';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildPlaceholder(filePath, kind, options = {}) {
|
|
25
|
+
const note = `${options.note || ''}`.trim();
|
|
26
|
+
const event = `${options.event || 'release-asset-placeholder'}`.trim();
|
|
27
|
+
const now = typeof options.now === 'function'
|
|
28
|
+
? options.now
|
|
29
|
+
: () => new Date().toISOString();
|
|
30
|
+
const baseNote = note || `placeholder for ${path.basename(filePath)}`;
|
|
31
|
+
if (kind === 'json' || kind === 'jsonl') {
|
|
32
|
+
return `${JSON.stringify({
|
|
33
|
+
event,
|
|
34
|
+
note: baseNote,
|
|
35
|
+
generated_at: now()
|
|
36
|
+
})}\n`;
|
|
37
|
+
}
|
|
38
|
+
return `# ${baseNote}\n`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeNonEmptyAssets(options = {}) {
|
|
42
|
+
const files = Array.isArray(options.files)
|
|
43
|
+
? options.files.map(item => `${item || ''}`.trim()).filter(Boolean)
|
|
44
|
+
: [];
|
|
45
|
+
if (files.length === 0) {
|
|
46
|
+
throw new Error('at least one --file is required');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const dryRun = Boolean(options.dryRun);
|
|
50
|
+
const kindInput = options.kind || 'auto';
|
|
51
|
+
const details = [];
|
|
52
|
+
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
const absolute = path.resolve(file);
|
|
55
|
+
const kind = resolveKind(absolute, kindInput);
|
|
56
|
+
const exists = fs.existsSync(absolute);
|
|
57
|
+
const size = exists ? fs.statSync(absolute).size : 0;
|
|
58
|
+
const needsPlaceholder = !exists || size <= 0;
|
|
59
|
+
let action = 'kept';
|
|
60
|
+
let writtenBytes = 0;
|
|
61
|
+
|
|
62
|
+
if (needsPlaceholder) {
|
|
63
|
+
action = exists ? 'filled-empty' : 'created-placeholder';
|
|
64
|
+
const content = buildPlaceholder(absolute, kind, options);
|
|
65
|
+
writtenBytes = Buffer.byteLength(content, 'utf8');
|
|
66
|
+
if (!dryRun) {
|
|
67
|
+
fs.mkdirSync(path.dirname(absolute), { recursive: true });
|
|
68
|
+
fs.writeFileSync(absolute, content, 'utf8');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
details.push({
|
|
73
|
+
file,
|
|
74
|
+
absolute_path: absolute,
|
|
75
|
+
kind,
|
|
76
|
+
existed: exists,
|
|
77
|
+
previous_size: size,
|
|
78
|
+
action,
|
|
79
|
+
written_bytes: writtenBytes
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const createdCount = details.filter(item => item.action === 'created-placeholder').length;
|
|
84
|
+
const filledCount = details.filter(item => item.action === 'filled-empty').length;
|
|
85
|
+
const keptCount = details.filter(item => item.action === 'kept').length;
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
mode: 'release-asset-nonempty-normalize',
|
|
89
|
+
dry_run: dryRun,
|
|
90
|
+
total: details.length,
|
|
91
|
+
created_placeholders: createdCount,
|
|
92
|
+
filled_empty_files: filledCount,
|
|
93
|
+
kept_existing_files: keptCount,
|
|
94
|
+
details
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseArgs(argv = []) {
|
|
99
|
+
const options = {
|
|
100
|
+
files: [],
|
|
101
|
+
kind: 'auto',
|
|
102
|
+
note: '',
|
|
103
|
+
event: '',
|
|
104
|
+
dryRun: false,
|
|
105
|
+
json: false
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
109
|
+
const token = argv[i];
|
|
110
|
+
if (token === '--file') {
|
|
111
|
+
options.files.push(argv[i + 1] || '');
|
|
112
|
+
i += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (token === '--kind') {
|
|
116
|
+
options.kind = argv[i + 1] || 'auto';
|
|
117
|
+
i += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (token === '--note') {
|
|
121
|
+
options.note = argv[i + 1] || '';
|
|
122
|
+
i += 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (token === '--event') {
|
|
126
|
+
options.event = argv[i + 1] || '';
|
|
127
|
+
i += 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (token === '--dry-run') {
|
|
131
|
+
options.dryRun = true;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (token === '--json') {
|
|
135
|
+
options.json = true;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (token === '-h' || token === '--help') {
|
|
139
|
+
console.log([
|
|
140
|
+
'Usage:',
|
|
141
|
+
' node scripts/release-asset-nonempty-normalize.js --file <path> [--file <path> ...] [--kind auto|json|jsonl|lines|text] [--note <text>] [--event <event>] [--dry-run] [--json]'
|
|
142
|
+
].join('\n'));
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return options;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (require.main === module) {
|
|
150
|
+
const options = parseArgs(process.argv.slice(2));
|
|
151
|
+
const payload = normalizeNonEmptyAssets(options);
|
|
152
|
+
if (options.json) {
|
|
153
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
154
|
+
} else {
|
|
155
|
+
process.stdout.write(
|
|
156
|
+
`[release-asset-nonempty-normalize] total=${payload.total} created=${payload.created_placeholders} filled=${payload.filled_empty_files} kept=${payload.kept_existing_files}\n`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
buildPlaceholder,
|
|
163
|
+
normalizeNonEmptyAssets,
|
|
164
|
+
parseArgs,
|
|
165
|
+
resolveKind
|
|
166
|
+
};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const {
|
|
5
|
+
buildReleaseDriftSignals,
|
|
6
|
+
parseBoolean,
|
|
7
|
+
resolveReleaseDriftThresholds
|
|
8
|
+
} = require('./release-drift-signals');
|
|
9
|
+
|
|
10
|
+
function appendSummary(summaryPath, lines = []) {
|
|
11
|
+
if (!summaryPath) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
fs.appendFileSync(summaryPath, `${lines.join('\n')}\n\n`, 'utf8');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildThresholdPayload(thresholds) {
|
|
18
|
+
return {
|
|
19
|
+
fail_streak_min: thresholds.failStreakMin,
|
|
20
|
+
high_risk_share_min_percent: thresholds.highRiskShareMinPercent,
|
|
21
|
+
high_risk_share_delta_min_percent: thresholds.highRiskShareDeltaMinPercent,
|
|
22
|
+
preflight_block_rate_min_percent: thresholds.preflightBlockRateMinPercent,
|
|
23
|
+
hard_gate_block_streak_min: thresholds.hardGateBlockStreakMin,
|
|
24
|
+
preflight_unavailable_streak_min: thresholds.preflightUnavailableStreakMin,
|
|
25
|
+
capability_expected_unknown_rate_min_percent: thresholds.capabilityExpectedUnknownRateMinPercent,
|
|
26
|
+
capability_provided_unknown_rate_min_percent: thresholds.capabilityProvidedUnknownRateMinPercent
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function evaluateReleaseDrift(options = {}) {
|
|
31
|
+
const env = options.env && typeof options.env === 'object'
|
|
32
|
+
? options.env
|
|
33
|
+
: process.env;
|
|
34
|
+
const now = typeof options.now === 'function'
|
|
35
|
+
? options.now
|
|
36
|
+
: () => new Date().toISOString();
|
|
37
|
+
const historyFile = env.RELEASE_DRIFT_HISTORY_FILE;
|
|
38
|
+
const gateReportFile = env.RELEASE_GATE_REPORT_FILE;
|
|
39
|
+
const summaryPath = env.GITHUB_STEP_SUMMARY;
|
|
40
|
+
const enforce = parseBoolean(env.RELEASE_DRIFT_ENFORCE, false);
|
|
41
|
+
const thresholds = resolveReleaseDriftThresholds(env);
|
|
42
|
+
|
|
43
|
+
if (!historyFile || !fs.existsSync(historyFile)) {
|
|
44
|
+
const msg = `[release-drift] history summary missing: ${historyFile || 'n/a'}`;
|
|
45
|
+
console.warn(msg);
|
|
46
|
+
appendSummary(summaryPath, ['## Release Drift Alerts', '', `- ${msg}`]);
|
|
47
|
+
return {
|
|
48
|
+
exit_code: 0,
|
|
49
|
+
blocked: false,
|
|
50
|
+
alerts: [],
|
|
51
|
+
warning: msg,
|
|
52
|
+
drift: null
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let payload = null;
|
|
57
|
+
try {
|
|
58
|
+
payload = JSON.parse(fs.readFileSync(historyFile, 'utf8'));
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const msg = `[release-drift] failed to parse history summary: ${error.message}`;
|
|
61
|
+
console.warn(msg);
|
|
62
|
+
appendSummary(summaryPath, ['## Release Drift Alerts', '', `- ${msg}`]);
|
|
63
|
+
return {
|
|
64
|
+
exit_code: 0,
|
|
65
|
+
blocked: false,
|
|
66
|
+
alerts: [],
|
|
67
|
+
warning: msg,
|
|
68
|
+
drift: null
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const signals = buildReleaseDriftSignals(payload, { thresholds });
|
|
73
|
+
const {
|
|
74
|
+
alerts,
|
|
75
|
+
failedStreak,
|
|
76
|
+
hardGateBlockedStreak,
|
|
77
|
+
highRiskDeltaPercent,
|
|
78
|
+
highRiskShare,
|
|
79
|
+
preflightUnavailableStreak,
|
|
80
|
+
recentCapabilityExpectedUnknownKnown,
|
|
81
|
+
recentCapabilityExpectedUnknownPositive,
|
|
82
|
+
recentCapabilityExpectedUnknownRate,
|
|
83
|
+
recentCapabilityProvidedUnknownKnown,
|
|
84
|
+
recentCapabilityProvidedUnknownPositive,
|
|
85
|
+
recentCapabilityProvidedUnknownRate,
|
|
86
|
+
recentPreflightBlocked,
|
|
87
|
+
recentPreflightBlockedRate,
|
|
88
|
+
recentPreflightKnown,
|
|
89
|
+
windows
|
|
90
|
+
} = signals;
|
|
91
|
+
const blocked = enforce && alerts.length > 0;
|
|
92
|
+
const evaluatedAt = now();
|
|
93
|
+
|
|
94
|
+
const driftPayload = {
|
|
95
|
+
enforce,
|
|
96
|
+
thresholds: buildThresholdPayload(thresholds),
|
|
97
|
+
metrics: {
|
|
98
|
+
failed_streak_latest5: failedStreak,
|
|
99
|
+
high_risk_share_latest5_percent: highRiskShare,
|
|
100
|
+
high_risk_share_delta_percent: highRiskDeltaPercent,
|
|
101
|
+
preflight_known_latest5: recentPreflightKnown,
|
|
102
|
+
preflight_blocked_latest5: recentPreflightBlocked,
|
|
103
|
+
preflight_blocked_rate_latest5_percent: recentPreflightBlockedRate,
|
|
104
|
+
hard_gate_blocked_streak_latest5: hardGateBlockedStreak,
|
|
105
|
+
preflight_unavailable_streak_latest5: preflightUnavailableStreak,
|
|
106
|
+
capability_expected_unknown_known_latest5: recentCapabilityExpectedUnknownKnown,
|
|
107
|
+
capability_expected_unknown_positive_latest5: recentCapabilityExpectedUnknownPositive,
|
|
108
|
+
capability_expected_unknown_positive_rate_latest5_percent: recentCapabilityExpectedUnknownRate,
|
|
109
|
+
capability_provided_unknown_known_latest5: recentCapabilityProvidedUnknownKnown,
|
|
110
|
+
capability_provided_unknown_positive_latest5: recentCapabilityProvidedUnknownPositive,
|
|
111
|
+
capability_provided_unknown_positive_rate_latest5_percent: recentCapabilityProvidedUnknownRate,
|
|
112
|
+
recent_window_size: windows.recent,
|
|
113
|
+
short_window_size: windows.short,
|
|
114
|
+
long_window_size: windows.long
|
|
115
|
+
},
|
|
116
|
+
alerts,
|
|
117
|
+
alert_count: alerts.length,
|
|
118
|
+
blocked,
|
|
119
|
+
evaluated_at: evaluatedAt
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
console.log(
|
|
123
|
+
`[release-drift] enforce=${enforce} fail_streak_threshold=${thresholds.failStreakMin} `
|
|
124
|
+
+ `high_share_threshold=${thresholds.highRiskShareMinPercent}% `
|
|
125
|
+
+ `high_delta_threshold=${thresholds.highRiskShareDeltaMinPercent}% `
|
|
126
|
+
+ `preflight_block_rate_threshold=${thresholds.preflightBlockRateMinPercent}% `
|
|
127
|
+
+ `hard_gate_block_streak_threshold=${thresholds.hardGateBlockStreakMin} `
|
|
128
|
+
+ `preflight_unavailable_streak_threshold=${thresholds.preflightUnavailableStreakMin} `
|
|
129
|
+
+ `cap_expected_unknown_rate_threshold=${thresholds.capabilityExpectedUnknownRateMinPercent}% `
|
|
130
|
+
+ `cap_provided_unknown_rate_threshold=${thresholds.capabilityProvidedUnknownRateMinPercent}%`
|
|
131
|
+
);
|
|
132
|
+
console.log(
|
|
133
|
+
`[release-drift] metrics failed_streak=${failedStreak} high_share=${highRiskShare}% `
|
|
134
|
+
+ `high_delta=${highRiskDeltaPercent}% `
|
|
135
|
+
+ `preflight_block_rate=${recentPreflightBlockedRate === null ? 'n/a' : `${recentPreflightBlockedRate}%`} `
|
|
136
|
+
+ `hard_gate_block_streak=${hardGateBlockedStreak} `
|
|
137
|
+
+ `preflight_unavailable_streak=${preflightUnavailableStreak} `
|
|
138
|
+
+ `cap_expected_unknown_rate=${recentCapabilityExpectedUnknownRate === null ? 'n/a' : `${recentCapabilityExpectedUnknownRate}%`} `
|
|
139
|
+
+ `cap_provided_unknown_rate=${recentCapabilityProvidedUnknownRate === null ? 'n/a' : `${recentCapabilityProvidedUnknownRate}%`}`
|
|
140
|
+
);
|
|
141
|
+
if (alerts.length > 0) {
|
|
142
|
+
alerts.forEach(item => console.warn(`[release-drift] alert=${item}`));
|
|
143
|
+
} else {
|
|
144
|
+
console.log('[release-drift] no drift alerts');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (gateReportFile) {
|
|
148
|
+
let gatePayload = {};
|
|
149
|
+
try {
|
|
150
|
+
if (fs.existsSync(gateReportFile)) {
|
|
151
|
+
gatePayload = JSON.parse(fs.readFileSync(gateReportFile, 'utf8'));
|
|
152
|
+
}
|
|
153
|
+
} catch (_error) {
|
|
154
|
+
gatePayload = {};
|
|
155
|
+
}
|
|
156
|
+
gatePayload.drift = driftPayload;
|
|
157
|
+
gatePayload.updated_at = evaluatedAt;
|
|
158
|
+
fs.writeFileSync(gateReportFile, `${JSON.stringify(gatePayload, null, 2)}\n`, 'utf8');
|
|
159
|
+
console.log(`[release-drift] merged drift into gate report: ${gateReportFile}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const summaryLines = [
|
|
163
|
+
'## Release Drift Alerts',
|
|
164
|
+
'',
|
|
165
|
+
`- enforce: ${enforce}`,
|
|
166
|
+
`- fail streak threshold: ${thresholds.failStreakMin}`,
|
|
167
|
+
`- high risk share threshold: ${thresholds.highRiskShareMinPercent}%`,
|
|
168
|
+
`- high risk delta threshold: ${thresholds.highRiskShareDeltaMinPercent}%`,
|
|
169
|
+
`- release preflight blocked rate threshold: ${thresholds.preflightBlockRateMinPercent}%`,
|
|
170
|
+
`- hard-gate blocked streak threshold: ${thresholds.hardGateBlockStreakMin}`,
|
|
171
|
+
`- preflight unavailable streak threshold: ${thresholds.preflightUnavailableStreakMin}`,
|
|
172
|
+
`- capability expected unknown rate threshold: ${thresholds.capabilityExpectedUnknownRateMinPercent}%`,
|
|
173
|
+
`- capability provided unknown rate threshold: ${thresholds.capabilityProvidedUnknownRateMinPercent}%`,
|
|
174
|
+
`- failed streak (latest 5): ${failedStreak}`,
|
|
175
|
+
`- high risk share (latest 5): ${highRiskShare}%`,
|
|
176
|
+
`- high risk delta (short-long): ${highRiskDeltaPercent}%`,
|
|
177
|
+
`- release preflight blocked ratio (latest 5): ${
|
|
178
|
+
recentPreflightKnown === 0
|
|
179
|
+
? 'n/a'
|
|
180
|
+
: `${recentPreflightBlocked}/${recentPreflightKnown} (${recentPreflightBlockedRate}%)`
|
|
181
|
+
}`,
|
|
182
|
+
`- hard-gate blocked streak (latest 5): ${hardGateBlockedStreak}`,
|
|
183
|
+
`- preflight unavailable streak (latest 5): ${preflightUnavailableStreak}`,
|
|
184
|
+
`- capability expected unknown ratio (latest 5): ${
|
|
185
|
+
recentCapabilityExpectedUnknownKnown === 0
|
|
186
|
+
? 'n/a'
|
|
187
|
+
: `${recentCapabilityExpectedUnknownPositive}/${recentCapabilityExpectedUnknownKnown} (${recentCapabilityExpectedUnknownRate}%)`
|
|
188
|
+
}`,
|
|
189
|
+
`- capability provided unknown ratio (latest 5): ${
|
|
190
|
+
recentCapabilityProvidedUnknownKnown === 0
|
|
191
|
+
? 'n/a'
|
|
192
|
+
: `${recentCapabilityProvidedUnknownPositive}/${recentCapabilityProvidedUnknownKnown} (${recentCapabilityProvidedUnknownRate}%)`
|
|
193
|
+
}`
|
|
194
|
+
];
|
|
195
|
+
if (alerts.length === 0) {
|
|
196
|
+
summaryLines.push('', '- no alerts');
|
|
197
|
+
} else {
|
|
198
|
+
summaryLines.push('', '### Alerts');
|
|
199
|
+
alerts.forEach(item => summaryLines.push(`- ${item}`));
|
|
200
|
+
}
|
|
201
|
+
appendSummary(summaryPath, summaryLines);
|
|
202
|
+
|
|
203
|
+
if (blocked) {
|
|
204
|
+
console.error(`[release-drift] blocked by drift alerts: ${alerts.join('; ')}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
exit_code: blocked ? 1 : 0,
|
|
209
|
+
blocked,
|
|
210
|
+
alerts,
|
|
211
|
+
warning: null,
|
|
212
|
+
drift: driftPayload
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (require.main === module) {
|
|
217
|
+
const result = evaluateReleaseDrift();
|
|
218
|
+
process.exit(result.exit_code);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
evaluateReleaseDrift
|
|
223
|
+
};
|