scene-capability-engine 3.6.44 → 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 (61) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/bin/scene-capability-engine.js +36 -2
  3. package/docs/command-reference.md +5 -0
  4. package/docs/releases/README.md +2 -0
  5. package/docs/releases/v3.6.45.md +18 -0
  6. package/docs/releases/v3.6.46.md +23 -0
  7. package/docs/zh/releases/README.md +2 -0
  8. package/docs/zh/releases/v3.6.45.md +18 -0
  9. package/docs/zh/releases/v3.6.46.md +23 -0
  10. package/lib/workspace/collab-governance-audit.js +575 -0
  11. package/package.json +4 -2
  12. package/scripts/auto-strategy-router.js +231 -0
  13. package/scripts/capability-mapping-report.js +339 -0
  14. package/scripts/check-branding-consistency.js +140 -0
  15. package/scripts/check-sce-tracking.js +54 -0
  16. package/scripts/check-skip-allowlist.js +94 -0
  17. package/scripts/errorbook-registry-health-gate.js +172 -0
  18. package/scripts/errorbook-release-gate.js +132 -0
  19. package/scripts/failure-attribution-repair.js +317 -0
  20. package/scripts/git-managed-gate.js +464 -0
  21. package/scripts/interactive-approval-event-projection.js +400 -0
  22. package/scripts/interactive-approval-workflow.js +829 -0
  23. package/scripts/interactive-authorization-tier-evaluate.js +413 -0
  24. package/scripts/interactive-change-plan-gate.js +225 -0
  25. package/scripts/interactive-context-bridge.js +617 -0
  26. package/scripts/interactive-customization-loop.js +1690 -0
  27. package/scripts/interactive-dialogue-governance.js +842 -0
  28. package/scripts/interactive-feedback-log.js +253 -0
  29. package/scripts/interactive-flow-smoke.js +238 -0
  30. package/scripts/interactive-flow.js +1059 -0
  31. package/scripts/interactive-governance-report.js +1112 -0
  32. package/scripts/interactive-intent-build.js +707 -0
  33. package/scripts/interactive-loop-smoke.js +215 -0
  34. package/scripts/interactive-moqui-adapter.js +304 -0
  35. package/scripts/interactive-plan-build.js +426 -0
  36. package/scripts/interactive-runtime-policy-evaluate.js +495 -0
  37. package/scripts/interactive-work-order-build.js +552 -0
  38. package/scripts/matrix-regression-gate.js +167 -0
  39. package/scripts/moqui-core-regression-suite.js +397 -0
  40. package/scripts/moqui-lexicon-audit.js +651 -0
  41. package/scripts/moqui-matrix-remediation-phased-runner.js +865 -0
  42. package/scripts/moqui-matrix-remediation-queue.js +852 -0
  43. package/scripts/moqui-metadata-extract.js +1340 -0
  44. package/scripts/moqui-rebuild-gate.js +167 -0
  45. package/scripts/moqui-release-summary.js +729 -0
  46. package/scripts/moqui-standard-rebuild.js +1370 -0
  47. package/scripts/moqui-template-baseline-report.js +682 -0
  48. package/scripts/npm-package-runtime-asset-check.js +221 -0
  49. package/scripts/problem-closure-gate.js +441 -0
  50. package/scripts/release-asset-integrity-check.js +216 -0
  51. package/scripts/release-asset-nonempty-normalize.js +166 -0
  52. package/scripts/release-drift-evaluate.js +223 -0
  53. package/scripts/release-drift-signals.js +255 -0
  54. package/scripts/release-governance-snapshot-export.js +132 -0
  55. package/scripts/release-ops-weekly-summary.js +934 -0
  56. package/scripts/release-risk-remediation-bundle.js +315 -0
  57. package/scripts/release-weekly-ops-gate.js +423 -0
  58. package/scripts/state-migration-reconciliation-gate.js +110 -0
  59. package/scripts/state-storage-tiering-audit.js +337 -0
  60. package/scripts/steering-content-audit.js +393 -0
  61. package/scripts/symbol-evidence-locate.js +366 -0
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const fs = require('fs-extra');
6
+
7
+ const DEFAULT_BASELINE = '.sce/reports/release-evidence/moqui-template-baseline.json';
8
+ const DEFAULT_OUT = '.sce/reports/release-evidence/matrix-regression-gate.json';
9
+
10
+ function parseArgs(argv) {
11
+ const options = {
12
+ baseline: DEFAULT_BASELINE,
13
+ maxRegressions: 0,
14
+ enforce: false,
15
+ out: DEFAULT_OUT,
16
+ json: false
17
+ };
18
+
19
+ for (let index = 0; index < argv.length; index += 1) {
20
+ const token = argv[index];
21
+ const next = argv[index + 1];
22
+ if (token === '--baseline' && next) {
23
+ options.baseline = next;
24
+ index += 1;
25
+ } else if (token === '--max-regressions' && next) {
26
+ options.maxRegressions = Number(next);
27
+ index += 1;
28
+ } else if (token === '--enforce') {
29
+ options.enforce = true;
30
+ } else if (token === '--out' && next) {
31
+ options.out = next;
32
+ index += 1;
33
+ } else if (token === '--json') {
34
+ options.json = true;
35
+ } else if (token === '--help' || token === '-h') {
36
+ printHelpAndExit(0);
37
+ }
38
+ }
39
+
40
+ if (!Number.isFinite(options.maxRegressions) || options.maxRegressions < 0) {
41
+ throw new Error('--max-regressions must be a non-negative number.');
42
+ }
43
+
44
+ return options;
45
+ }
46
+
47
+ function printHelpAndExit(code) {
48
+ const lines = [
49
+ 'Usage: node scripts/matrix-regression-gate.js [options]',
50
+ '',
51
+ 'Options:',
52
+ ` --baseline <path> Baseline report JSON path (default: ${DEFAULT_BASELINE})`,
53
+ ' --max-regressions <n> Maximum allowed regression count (default: 0)',
54
+ ' --enforce Exit code 2 when regressions exceed max',
55
+ ` --out <path> Gate report output path (default: ${DEFAULT_OUT})`,
56
+ ' --json Print gate payload as JSON',
57
+ ' -h, --help Show this help'
58
+ ];
59
+ console.log(lines.join('\n'));
60
+ process.exit(code);
61
+ }
62
+
63
+ function resolvePath(cwd, value) {
64
+ return path.isAbsolute(value) ? value : path.resolve(cwd, value);
65
+ }
66
+
67
+ function pickRegressions(payload) {
68
+ const compare = payload && payload.compare && typeof payload.compare === 'object'
69
+ ? payload.compare
70
+ : {};
71
+ if (Array.isArray(compare.coverage_matrix_regressions)) {
72
+ return compare.coverage_matrix_regressions;
73
+ }
74
+ if (Array.isArray(compare.regressions)) {
75
+ return compare.regressions;
76
+ }
77
+ return [];
78
+ }
79
+
80
+ function formatRegression(item = {}) {
81
+ const metric = item && item.metric ? String(item.metric) : 'unknown';
82
+ const delta = Number(item && item.delta_rate_percent);
83
+ return {
84
+ metric,
85
+ delta_rate_percent: Number.isFinite(delta) ? Number(delta) : null
86
+ };
87
+ }
88
+
89
+ async function main() {
90
+ const options = parseArgs(process.argv.slice(2));
91
+ const cwd = process.cwd();
92
+ const baselinePath = resolvePath(cwd, options.baseline);
93
+ const outPath = resolvePath(cwd, options.out);
94
+ const baselineExists = await fs.pathExists(baselinePath);
95
+ let baselinePayload = null;
96
+ let parseError = null;
97
+
98
+ if (baselineExists) {
99
+ try {
100
+ baselinePayload = await fs.readJson(baselinePath);
101
+ } catch (error) {
102
+ parseError = error.message;
103
+ }
104
+ }
105
+
106
+ const regressionsRaw = baselinePayload ? pickRegressions(baselinePayload) : [];
107
+ const regressions = regressionsRaw.map(formatRegression);
108
+ const regressionCount = regressions.length;
109
+ const maxRegressions = Number(options.maxRegressions);
110
+ const check = parseError || !baselineExists
111
+ ? null
112
+ : regressionCount <= maxRegressions;
113
+
114
+ const payload = {
115
+ mode: 'matrix-regression-gate',
116
+ generated_at: new Date().toISOString(),
117
+ baseline: {
118
+ path: path.relative(cwd, baselinePath) || '.',
119
+ exists: baselineExists,
120
+ parse_error: parseError
121
+ },
122
+ policy: {
123
+ max_regressions: maxRegressions,
124
+ enforce: options.enforce === true
125
+ },
126
+ summary: {
127
+ regressions: regressionCount,
128
+ passed: check,
129
+ status: check === null ? 'incomplete' : (check ? 'passed' : 'failed')
130
+ },
131
+ regressions
132
+ };
133
+
134
+ await fs.ensureDir(path.dirname(outPath));
135
+ await fs.writeJson(outPath, payload, { spaces: 2 });
136
+
137
+ if (options.json) {
138
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
139
+ } else {
140
+ process.stdout.write(`Matrix regression gate: ${payload.summary.status}\n`);
141
+ process.stdout.write(`- Baseline: ${payload.baseline.path}\n`);
142
+ process.stdout.write(`- Regressions: ${payload.summary.regressions}\n`);
143
+ process.stdout.write(`- Max allowed: ${payload.policy.max_regressions}\n`);
144
+ process.stdout.write(`- Output: ${path.relative(cwd, outPath) || '.'}\n`);
145
+ }
146
+
147
+ if (options.enforce && check === false) {
148
+ process.exitCode = 2;
149
+ }
150
+ }
151
+
152
+ if (require.main === module) {
153
+ main().catch((error) => {
154
+ console.error(`Matrix regression gate failed: ${error.message}`);
155
+ process.exit(1);
156
+ });
157
+ }
158
+
159
+ module.exports = {
160
+ DEFAULT_BASELINE,
161
+ DEFAULT_OUT,
162
+ parseArgs,
163
+ resolvePath,
164
+ pickRegressions,
165
+ formatRegression,
166
+ main
167
+ };
@@ -0,0 +1,397 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+ const fs = require('fs-extra');
7
+
8
+ const DEFAULT_WORKSPACE = 'tests/fixtures/moqui-core-regression/workspace';
9
+ const DEFAULT_OUT = '.sce/reports/release-evidence/moqui-core-regression-suite.json';
10
+ const DEFAULT_MARKDOWN_OUT = '.sce/reports/release-evidence/moqui-core-regression-suite.md';
11
+
12
+ function parseArgs(argv) {
13
+ const options = {
14
+ workspace: DEFAULT_WORKSPACE,
15
+ out: DEFAULT_OUT,
16
+ markdownOut: DEFAULT_MARKDOWN_OUT,
17
+ json: false,
18
+ failOnError: true,
19
+ };
20
+
21
+ for (let i = 0; i < argv.length; i += 1) {
22
+ const token = argv[i];
23
+ const next = argv[i + 1];
24
+ if (token === '--workspace' && next) {
25
+ options.workspace = next;
26
+ i += 1;
27
+ } else if (token === '--out' && next) {
28
+ options.out = next;
29
+ i += 1;
30
+ } else if (token === '--markdown-out' && next) {
31
+ options.markdownOut = next;
32
+ i += 1;
33
+ } else if (token === '--json') {
34
+ options.json = true;
35
+ } else if (token === '--no-fail-on-error') {
36
+ options.failOnError = false;
37
+ } else if (token === '--help' || token === '-h') {
38
+ printHelpAndExit(0);
39
+ }
40
+ }
41
+
42
+ return options;
43
+ }
44
+
45
+ function printHelpAndExit(code) {
46
+ const lines = [
47
+ 'Usage: node scripts/moqui-core-regression-suite.js [options]',
48
+ '',
49
+ 'Options:',
50
+ ` --workspace <path> Regression fixture workspace (default: ${DEFAULT_WORKSPACE})`,
51
+ ` --out <path> JSON summary output path (default: ${DEFAULT_OUT})`,
52
+ ` --markdown-out <path> Markdown summary output path (default: ${DEFAULT_MARKDOWN_OUT})`,
53
+ ' --json Print JSON summary to stdout',
54
+ ' --no-fail-on-error Keep exit code 0 when suite fails',
55
+ ' -h, --help Show this help',
56
+ ];
57
+ console.log(lines.join('\n'));
58
+ process.exit(code);
59
+ }
60
+
61
+ function normalizePathForReport(projectRoot, filePath) {
62
+ const relative = path.relative(projectRoot, filePath);
63
+ return relative && !relative.startsWith('..') ? relative.replace(/\\/g, '/') : filePath;
64
+ }
65
+
66
+ function parseJsonFromStdout(stdout) {
67
+ const text = `${stdout || ''}`.trim();
68
+ if (!text) {
69
+ return null;
70
+ }
71
+ try {
72
+ return JSON.parse(text);
73
+ } catch (_error) {
74
+ // fall through
75
+ }
76
+
77
+ const firstBrace = text.indexOf('{');
78
+ const lastBrace = text.lastIndexOf('}');
79
+ if (firstBrace >= 0 && lastBrace > firstBrace) {
80
+ const candidate = text.slice(firstBrace, lastBrace + 1);
81
+ try {
82
+ return JSON.parse(candidate);
83
+ } catch (_error) {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ return null;
89
+ }
90
+
91
+ function runNodeCommand(args, cwd) {
92
+ const started = Date.now();
93
+ const result = spawnSync(process.execPath, args, {
94
+ cwd,
95
+ encoding: 'utf8',
96
+ stdio: ['ignore', 'pipe', 'pipe'],
97
+ });
98
+ const ended = Date.now();
99
+
100
+ return {
101
+ command: [process.execPath, ...args],
102
+ cwd,
103
+ exit_code: Number.isInteger(result.status) ? result.status : 1,
104
+ duration_ms: Math.max(0, ended - started),
105
+ stdout: result.stdout || '',
106
+ stderr: result.stderr || '',
107
+ json: parseJsonFromStdout(result.stdout || ''),
108
+ };
109
+ }
110
+
111
+ function summarizeStageOutput(stage) {
112
+ const payload = stage.json && typeof stage.json === 'object' ? stage.json : {};
113
+ if (stage.name === 'moqui-baseline') {
114
+ const summary = payload.summary && typeof payload.summary === 'object' ? payload.summary : {};
115
+ return {
116
+ portfolio_passed: summary.portfolio_passed === true,
117
+ avg_score: Number(summary.avg_score),
118
+ valid_rate_percent: Number(summary.valid_rate_percent),
119
+ baseline_failed: Number(summary.baseline_failed),
120
+ };
121
+ }
122
+ if (stage.name === 'scene-package-publish-batch-dry-run') {
123
+ const summary = payload.summary && typeof payload.summary === 'object' ? payload.summary : {};
124
+ const batchGate = payload.batch_ontology_gate && typeof payload.batch_ontology_gate === 'object'
125
+ ? payload.batch_ontology_gate
126
+ : {};
127
+ return {
128
+ success: payload.success === true,
129
+ failed: Number(summary.failed),
130
+ selected: Number(summary.selected),
131
+ batch_gate_passed: batchGate.passed === true,
132
+ };
133
+ }
134
+ if (stage.name === 'moqui-lexicon-audit') {
135
+ const summary = payload.summary && typeof payload.summary === 'object' ? payload.summary : {};
136
+ return {
137
+ passed: summary.passed === true,
138
+ expected_unknown_count: Number(summary.expected_unknown_count),
139
+ provided_unknown_count: Number(summary.provided_unknown_count),
140
+ uncovered_expected_count: Number(summary.uncovered_expected_count),
141
+ coverage_percent: Number(summary.coverage_percent),
142
+ };
143
+ }
144
+ if (stage.name === 'auto-handoff-dry-run') {
145
+ const gates = payload.gates && typeof payload.gates === 'object' ? payload.gates : {};
146
+ return {
147
+ status: typeof payload.status === 'string' ? payload.status : null,
148
+ gate_passed: gates.passed === true,
149
+ spec_success_rate_percent: gates.actual ? Number(gates.actual.spec_success_rate_percent) : null,
150
+ };
151
+ }
152
+ return {};
153
+ }
154
+
155
+ function evaluateStage(stage) {
156
+ if (stage.exit_code !== 0) {
157
+ return {
158
+ passed: false,
159
+ reason: `process exited with code ${stage.exit_code}`,
160
+ };
161
+ }
162
+
163
+ const payload = stage.json;
164
+ if (!payload || typeof payload !== 'object') {
165
+ return {
166
+ passed: false,
167
+ reason: 'stdout did not include a valid JSON payload',
168
+ };
169
+ }
170
+
171
+ if (stage.name === 'moqui-baseline') {
172
+ const summary = payload.summary && typeof payload.summary === 'object' ? payload.summary : {};
173
+ if (summary.portfolio_passed !== true) {
174
+ return {
175
+ passed: false,
176
+ reason: 'portfolio baseline gate failed',
177
+ };
178
+ }
179
+ } else if (stage.name === 'moqui-lexicon-audit') {
180
+ const summary = payload.summary && typeof payload.summary === 'object' ? payload.summary : {};
181
+ if (summary.passed !== true) {
182
+ return {
183
+ passed: false,
184
+ reason: (
185
+ 'moqui lexicon audit gate failed: ' +
186
+ `expected_unknown=${Number(summary.expected_unknown_count) || 0}, ` +
187
+ `provided_unknown=${Number(summary.provided_unknown_count) || 0}, ` +
188
+ `uncovered_expected=${Number(summary.uncovered_expected_count) || 0}`
189
+ ),
190
+ };
191
+ }
192
+ } else if (stage.name === 'scene-package-publish-batch-dry-run') {
193
+ const batchGate = payload.batch_ontology_gate && typeof payload.batch_ontology_gate === 'object'
194
+ ? payload.batch_ontology_gate
195
+ : {};
196
+ if (payload.success !== true || batchGate.passed !== true) {
197
+ return {
198
+ passed: false,
199
+ reason: 'scene package publish-batch dry-run gate failed',
200
+ };
201
+ }
202
+ } else if (stage.name === 'auto-handoff-dry-run') {
203
+ const status = typeof payload.status === 'string' ? payload.status : '';
204
+ const gates = payload.gates && typeof payload.gates === 'object' ? payload.gates : {};
205
+ if (!['dry-run', 'completed'].includes(status) || gates.passed !== true) {
206
+ return {
207
+ passed: false,
208
+ reason: 'auto handoff dry-run gate failed',
209
+ };
210
+ }
211
+ }
212
+
213
+ return { passed: true, reason: null };
214
+ }
215
+
216
+ function buildMarkdownSummary(report) {
217
+ const lines = [];
218
+ lines.push('# Moqui Core Regression Suite');
219
+ lines.push('');
220
+ lines.push(`- Generated at: ${report.generated_at}`);
221
+ lines.push(`- Workspace: ${report.workspace}`);
222
+ lines.push(`- Overall result: ${report.success ? 'pass' : 'fail'}`);
223
+ lines.push(`- Failed stages: ${report.failed_stages.length === 0 ? 'none' : report.failed_stages.join(', ')}`);
224
+ lines.push('');
225
+ lines.push('## Stages');
226
+ lines.push('');
227
+ lines.push('| Stage | Result | Exit | Duration(ms) | Notes |');
228
+ lines.push('| --- | --- | ---: | ---: | --- |');
229
+ for (const stage of report.stages) {
230
+ const note = stage.reason || 'ok';
231
+ lines.push(`| ${stage.name} | ${stage.passed ? 'pass' : 'fail'} | ${stage.exit_code} | ${stage.duration_ms} | ${note} |`);
232
+ }
233
+ lines.push('');
234
+ lines.push('## Artifacts');
235
+ lines.push('');
236
+ lines.push(`- JSON: ${report.output.json}`);
237
+ lines.push(`- Markdown: ${report.output.markdown}`);
238
+ for (const stage of report.stages) {
239
+ if (stage.artifact_json) {
240
+ lines.push(`- ${stage.name} JSON: ${stage.artifact_json}`);
241
+ }
242
+ if (stage.artifact_markdown) {
243
+ lines.push(`- ${stage.name} Markdown: ${stage.artifact_markdown}`);
244
+ }
245
+ }
246
+ return `${lines.join('\n')}\n`;
247
+ }
248
+
249
+ async function main() {
250
+ const options = parseArgs(process.argv.slice(2));
251
+ const projectRoot = path.resolve(__dirname, '..');
252
+ const workspace = path.resolve(projectRoot, options.workspace);
253
+ const outFile = path.resolve(projectRoot, options.out);
254
+ const markdownOutFile = path.resolve(projectRoot, options.markdownOut);
255
+
256
+ if (!(await fs.pathExists(workspace))) {
257
+ throw new Error(`regression workspace not found: ${workspace}`);
258
+ }
259
+
260
+ const stageArtifactDir = path.join(workspace, '.sce', 'reports', 'moqui-core-regression');
261
+ await fs.ensureDir(stageArtifactDir);
262
+
263
+ const baselineJsonFile = path.join(stageArtifactDir, 'moqui-template-baseline.json');
264
+ const baselineMarkdownFile = path.join(stageArtifactDir, 'moqui-template-baseline.md');
265
+ const lexiconAuditJsonFile = path.join(stageArtifactDir, 'moqui-lexicon-audit.json');
266
+ const lexiconAuditMarkdownFile = path.join(stageArtifactDir, 'moqui-lexicon-audit.md');
267
+
268
+ const stageDefinitions = [
269
+ {
270
+ name: 'moqui-baseline',
271
+ args: [
272
+ path.join(projectRoot, 'scripts', 'moqui-template-baseline-report.js'),
273
+ '--template-dir', '.sce/templates/scene-packages',
274
+ '--out', baselineJsonFile,
275
+ '--markdown-out', baselineMarkdownFile,
276
+ '--include-all',
277
+ '--fail-on-portfolio-fail',
278
+ '--json',
279
+ ],
280
+ artifact_json: baselineJsonFile,
281
+ artifact_markdown: baselineMarkdownFile,
282
+ },
283
+ {
284
+ name: 'scene-package-publish-batch-dry-run',
285
+ args: [
286
+ path.join(projectRoot, 'bin', 'scene-capability-engine.js'),
287
+ 'scene',
288
+ 'package-publish-batch',
289
+ '--manifest',
290
+ 'docs/handoffs/handoff-manifest.json',
291
+ '--dry-run',
292
+ '--json',
293
+ ],
294
+ artifact_json: null,
295
+ artifact_markdown: null,
296
+ },
297
+ {
298
+ name: 'moqui-lexicon-audit',
299
+ args: [
300
+ path.join(projectRoot, 'scripts', 'moqui-lexicon-audit.js'),
301
+ '--manifest', 'docs/handoffs/handoff-manifest.json',
302
+ '--template-dir', '.sce/templates/scene-packages',
303
+ '--lexicon', path.join(projectRoot, 'lib', 'data', 'moqui-capability-lexicon.json'),
304
+ '--out', lexiconAuditJsonFile,
305
+ '--markdown-out', lexiconAuditMarkdownFile,
306
+ '--json',
307
+ ],
308
+ artifact_json: lexiconAuditJsonFile,
309
+ artifact_markdown: lexiconAuditMarkdownFile,
310
+ },
311
+ {
312
+ name: 'auto-handoff-dry-run',
313
+ args: [
314
+ path.join(projectRoot, 'bin', 'scene-capability-engine.js'),
315
+ 'auto',
316
+ 'handoff',
317
+ 'run',
318
+ '--manifest',
319
+ 'docs/handoffs/handoff-manifest.json',
320
+ '--dry-run',
321
+ '--no-require-release-gate-preflight',
322
+ '--json',
323
+ ],
324
+ artifact_json: null,
325
+ artifact_markdown: null,
326
+ },
327
+ ];
328
+
329
+ const stages = [];
330
+ for (const definition of stageDefinitions) {
331
+ const rawStage = runNodeCommand(definition.args, workspace);
332
+ const gate = evaluateStage({
333
+ name: definition.name,
334
+ ...rawStage,
335
+ });
336
+
337
+ const stage = {
338
+ name: definition.name,
339
+ command: rawStage.command.join(' '),
340
+ cwd: normalizePathForReport(projectRoot, rawStage.cwd),
341
+ exit_code: rawStage.exit_code,
342
+ duration_ms: rawStage.duration_ms,
343
+ passed: gate.passed,
344
+ reason: gate.reason,
345
+ summary: summarizeStageOutput({
346
+ name: definition.name,
347
+ ...rawStage,
348
+ }),
349
+ artifact_json: definition.artifact_json
350
+ ? normalizePathForReport(projectRoot, definition.artifact_json)
351
+ : null,
352
+ artifact_markdown: definition.artifact_markdown
353
+ ? normalizePathForReport(projectRoot, definition.artifact_markdown)
354
+ : null,
355
+ stdout_preview: rawStage.stdout.trim().slice(0, 400),
356
+ stderr_preview: rawStage.stderr.trim().slice(0, 400),
357
+ };
358
+ stages.push(stage);
359
+ }
360
+
361
+ const failedStages = stages.filter((stage) => !stage.passed).map((stage) => stage.name);
362
+ const report = {
363
+ mode: 'moqui-core-regression-suite',
364
+ generated_at: new Date().toISOString(),
365
+ workspace: normalizePathForReport(projectRoot, workspace),
366
+ success: failedStages.length === 0,
367
+ failed_stages: failedStages,
368
+ stages,
369
+ output: {
370
+ json: normalizePathForReport(projectRoot, outFile),
371
+ markdown: normalizePathForReport(projectRoot, markdownOutFile),
372
+ },
373
+ };
374
+
375
+ await fs.ensureDir(path.dirname(outFile));
376
+ await fs.writeJson(outFile, report, { spaces: 2 });
377
+ await fs.ensureDir(path.dirname(markdownOutFile));
378
+ await fs.writeFile(markdownOutFile, buildMarkdownSummary(report), 'utf8');
379
+
380
+ if (options.json) {
381
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
382
+ } else {
383
+ const status = report.success ? 'PASS' : 'FAIL';
384
+ process.stdout.write(`Moqui core regression suite: ${status}\n`);
385
+ process.stdout.write(`- JSON: ${report.output.json}\n`);
386
+ process.stdout.write(`- Markdown: ${report.output.markdown}\n`);
387
+ }
388
+
389
+ if (!report.success && options.failOnError) {
390
+ process.exitCode = 1;
391
+ }
392
+ }
393
+
394
+ main().catch((error) => {
395
+ console.error(`Moqui core regression suite failed: ${error.message}`);
396
+ process.exit(1);
397
+ });