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.
Files changed (56) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/docs/releases/README.md +1 -0
  3. package/docs/releases/v3.6.46.md +23 -0
  4. package/docs/zh/releases/README.md +1 -0
  5. package/docs/zh/releases/v3.6.46.md +23 -0
  6. package/package.json +4 -2
  7. package/scripts/auto-strategy-router.js +231 -0
  8. package/scripts/capability-mapping-report.js +339 -0
  9. package/scripts/check-branding-consistency.js +140 -0
  10. package/scripts/check-sce-tracking.js +54 -0
  11. package/scripts/check-skip-allowlist.js +94 -0
  12. package/scripts/errorbook-registry-health-gate.js +172 -0
  13. package/scripts/errorbook-release-gate.js +132 -0
  14. package/scripts/failure-attribution-repair.js +317 -0
  15. package/scripts/git-managed-gate.js +464 -0
  16. package/scripts/interactive-approval-event-projection.js +400 -0
  17. package/scripts/interactive-approval-workflow.js +829 -0
  18. package/scripts/interactive-authorization-tier-evaluate.js +413 -0
  19. package/scripts/interactive-change-plan-gate.js +225 -0
  20. package/scripts/interactive-context-bridge.js +617 -0
  21. package/scripts/interactive-customization-loop.js +1690 -0
  22. package/scripts/interactive-dialogue-governance.js +842 -0
  23. package/scripts/interactive-feedback-log.js +253 -0
  24. package/scripts/interactive-flow-smoke.js +238 -0
  25. package/scripts/interactive-flow.js +1059 -0
  26. package/scripts/interactive-governance-report.js +1112 -0
  27. package/scripts/interactive-intent-build.js +707 -0
  28. package/scripts/interactive-loop-smoke.js +215 -0
  29. package/scripts/interactive-moqui-adapter.js +304 -0
  30. package/scripts/interactive-plan-build.js +426 -0
  31. package/scripts/interactive-runtime-policy-evaluate.js +495 -0
  32. package/scripts/interactive-work-order-build.js +552 -0
  33. package/scripts/matrix-regression-gate.js +167 -0
  34. package/scripts/moqui-core-regression-suite.js +397 -0
  35. package/scripts/moqui-lexicon-audit.js +651 -0
  36. package/scripts/moqui-matrix-remediation-phased-runner.js +865 -0
  37. package/scripts/moqui-matrix-remediation-queue.js +852 -0
  38. package/scripts/moqui-metadata-extract.js +1340 -0
  39. package/scripts/moqui-rebuild-gate.js +167 -0
  40. package/scripts/moqui-release-summary.js +729 -0
  41. package/scripts/moqui-standard-rebuild.js +1370 -0
  42. package/scripts/moqui-template-baseline-report.js +682 -0
  43. package/scripts/npm-package-runtime-asset-check.js +221 -0
  44. package/scripts/problem-closure-gate.js +441 -0
  45. package/scripts/release-asset-integrity-check.js +216 -0
  46. package/scripts/release-asset-nonempty-normalize.js +166 -0
  47. package/scripts/release-drift-evaluate.js +223 -0
  48. package/scripts/release-drift-signals.js +255 -0
  49. package/scripts/release-governance-snapshot-export.js +132 -0
  50. package/scripts/release-ops-weekly-summary.js +934 -0
  51. package/scripts/release-risk-remediation-bundle.js +315 -0
  52. package/scripts/release-weekly-ops-gate.js +423 -0
  53. package/scripts/state-migration-reconciliation-gate.js +110 -0
  54. package/scripts/state-storage-tiering-audit.js +337 -0
  55. package/scripts/steering-content-audit.js +393 -0
  56. 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
+ };