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,413 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const fs = require('fs-extra');
6
+
7
+ const DEFAULT_POLICY = 'docs/interactive-customization/authorization-tier-policy-baseline.json';
8
+ const DEFAULT_OUT = '.sce/reports/interactive-authorization-tier.json';
9
+ const DIALOGUE_PROFILES = new Set(['business-user', 'system-maintainer']);
10
+ const RUNTIME_ENVIRONMENTS = new Set(['dev', 'staging', 'prod']);
11
+
12
+ const BUILTIN_POLICY = {
13
+ version: '1.0.0',
14
+ defaults: {
15
+ profile: 'business-user'
16
+ },
17
+ profiles: {
18
+ 'business-user': {
19
+ allow_execution_modes: ['suggestion'],
20
+ auto_execute_allowed: false,
21
+ allow_live_apply: false
22
+ },
23
+ 'system-maintainer': {
24
+ allow_execution_modes: ['suggestion', 'apply'],
25
+ auto_execute_allowed: true,
26
+ allow_live_apply: true
27
+ }
28
+ },
29
+ environments: {
30
+ dev: {
31
+ require_secondary_authorization: false,
32
+ require_password_for_apply: false,
33
+ require_role_policy: false,
34
+ require_distinct_actor_roles: false,
35
+ manual_review_required_for_apply: false
36
+ },
37
+ staging: {
38
+ require_secondary_authorization: true,
39
+ require_password_for_apply: true,
40
+ require_role_policy: false,
41
+ require_distinct_actor_roles: false,
42
+ manual_review_required_for_apply: false
43
+ },
44
+ prod: {
45
+ require_secondary_authorization: true,
46
+ require_password_for_apply: true,
47
+ require_role_policy: true,
48
+ require_distinct_actor_roles: true,
49
+ manual_review_required_for_apply: true
50
+ }
51
+ }
52
+ };
53
+
54
+ function parseArgs(argv) {
55
+ const options = {
56
+ executionMode: 'suggestion',
57
+ dialogueProfile: 'business-user',
58
+ runtimeMode: null,
59
+ runtimeEnvironment: 'staging',
60
+ autoExecuteLowRisk: false,
61
+ liveApply: false,
62
+ policy: DEFAULT_POLICY,
63
+ out: DEFAULT_OUT,
64
+ failOnNonAllow: false,
65
+ json: false
66
+ };
67
+
68
+ for (let index = 0; index < argv.length; index += 1) {
69
+ const token = argv[index];
70
+ const next = argv[index + 1];
71
+ if (token === '--execution-mode' && next) {
72
+ options.executionMode = next;
73
+ index += 1;
74
+ } else if (token === '--dialogue-profile' && next) {
75
+ options.dialogueProfile = next;
76
+ index += 1;
77
+ } else if (token === '--runtime-mode' && next) {
78
+ options.runtimeMode = next;
79
+ index += 1;
80
+ } else if (token === '--runtime-environment' && next) {
81
+ options.runtimeEnvironment = next;
82
+ index += 1;
83
+ } else if (token === '--auto-execute-low-risk') {
84
+ options.autoExecuteLowRisk = true;
85
+ } else if (token === '--live-apply') {
86
+ options.liveApply = true;
87
+ } else if (token === '--policy' && next) {
88
+ options.policy = next;
89
+ index += 1;
90
+ } else if (token === '--out' && next) {
91
+ options.out = next;
92
+ index += 1;
93
+ } else if (token === '--fail-on-non-allow') {
94
+ options.failOnNonAllow = true;
95
+ } else if (token === '--json') {
96
+ options.json = true;
97
+ } else if (token === '--help' || token === '-h') {
98
+ printHelpAndExit(0);
99
+ }
100
+ }
101
+
102
+ options.executionMode = `${options.executionMode || ''}`.trim().toLowerCase() || 'suggestion';
103
+ options.dialogueProfile = `${options.dialogueProfile || ''}`.trim().toLowerCase() || 'business-user';
104
+ options.runtimeEnvironment = `${options.runtimeEnvironment || ''}`.trim().toLowerCase() || 'staging';
105
+
106
+ if (!['suggestion', 'apply'].includes(options.executionMode)) {
107
+ throw new Error('--execution-mode must be one of: suggestion, apply');
108
+ }
109
+ if (!DIALOGUE_PROFILES.has(options.dialogueProfile)) {
110
+ throw new Error(`--dialogue-profile must be one of: ${Array.from(DIALOGUE_PROFILES).join(', ')}`);
111
+ }
112
+ if (!RUNTIME_ENVIRONMENTS.has(options.runtimeEnvironment)) {
113
+ throw new Error(`--runtime-environment must be one of: ${Array.from(RUNTIME_ENVIRONMENTS).join(', ')}`);
114
+ }
115
+
116
+ return options;
117
+ }
118
+
119
+ function printHelpAndExit(code) {
120
+ const lines = [
121
+ 'Usage: node scripts/interactive-authorization-tier-evaluate.js [options]',
122
+ '',
123
+ 'Options:',
124
+ ' --execution-mode <mode> suggestion|apply (default: suggestion)',
125
+ ' --dialogue-profile <name> business-user|system-maintainer (default: business-user)',
126
+ ' --runtime-mode <name> Runtime mode context (optional)',
127
+ ' --runtime-environment <name> dev|staging|prod (default: staging)',
128
+ ' --auto-execute-low-risk Evaluate low-risk auto execute request',
129
+ ' --live-apply Evaluate live apply request',
130
+ ` --policy <path> Authorization tier policy JSON (default: ${DEFAULT_POLICY})`,
131
+ ` --out <path> Output JSON report path (default: ${DEFAULT_OUT})`,
132
+ ' --fail-on-non-allow Exit code 2 when decision is deny/review-required',
133
+ ' --json Print payload as JSON',
134
+ ' -h, --help Show this help'
135
+ ];
136
+ console.log(lines.join('\n'));
137
+ process.exit(code);
138
+ }
139
+
140
+ function resolvePath(cwd, value) {
141
+ return path.isAbsolute(value) ? value : path.resolve(cwd, value);
142
+ }
143
+
144
+ function normalizeStringList(values = []) {
145
+ return Array.from(new Set(
146
+ (Array.isArray(values) ? values : [])
147
+ .map(item => `${item || ''}`.trim().toLowerCase())
148
+ .filter(Boolean)
149
+ ));
150
+ }
151
+
152
+ async function readJsonFile(filePath, label) {
153
+ if (!(await fs.pathExists(filePath))) {
154
+ throw new Error(`${label} not found: ${filePath}`);
155
+ }
156
+ const raw = await fs.readFile(filePath, 'utf8');
157
+ try {
158
+ return JSON.parse(raw);
159
+ } catch (error) {
160
+ throw new Error(`invalid JSON in ${label}: ${error.message}`);
161
+ }
162
+ }
163
+
164
+ function normalizeProfilePolicy(rawProfile = {}) {
165
+ const profile = rawProfile && typeof rawProfile === 'object' ? rawProfile : {};
166
+ return {
167
+ allow_execution_modes: normalizeStringList(profile.allow_execution_modes),
168
+ auto_execute_allowed: profile.auto_execute_allowed === true,
169
+ allow_live_apply: profile.allow_live_apply === true
170
+ };
171
+ }
172
+
173
+ function normalizeEnvironmentPolicy(rawEnvironment = {}) {
174
+ const environment = rawEnvironment && typeof rawEnvironment === 'object' ? rawEnvironment : {};
175
+ return {
176
+ require_secondary_authorization: environment.require_secondary_authorization === true,
177
+ require_password_for_apply: environment.require_password_for_apply === true,
178
+ require_role_policy: environment.require_role_policy === true,
179
+ require_distinct_actor_roles: environment.require_distinct_actor_roles === true,
180
+ manual_review_required_for_apply: environment.manual_review_required_for_apply === true
181
+ };
182
+ }
183
+
184
+ function normalizePolicy(rawPolicy = {}) {
185
+ const policy = rawPolicy && typeof rawPolicy === 'object' ? rawPolicy : {};
186
+ const defaults = policy.defaults && typeof policy.defaults === 'object' ? policy.defaults : {};
187
+ const inputProfiles = policy.profiles && typeof policy.profiles === 'object' ? policy.profiles : {};
188
+ const inputEnvironments = policy.environments && typeof policy.environments === 'object' ? policy.environments : {};
189
+
190
+ const profiles = {};
191
+ for (const [name, config] of Object.entries(BUILTIN_POLICY.profiles)) {
192
+ profiles[name] = normalizeProfilePolicy(config);
193
+ }
194
+ for (const [name, config] of Object.entries(inputProfiles)) {
195
+ const key = `${name || ''}`.trim().toLowerCase();
196
+ if (!key) {
197
+ continue;
198
+ }
199
+ const normalizedConfig = normalizeProfilePolicy(config);
200
+ profiles[key] = {
201
+ ...profiles[key],
202
+ ...normalizedConfig,
203
+ allow_execution_modes: normalizedConfig.allow_execution_modes.length > 0
204
+ ? normalizedConfig.allow_execution_modes
205
+ : (profiles[key] && profiles[key].allow_execution_modes) || []
206
+ };
207
+ }
208
+
209
+ const environments = {};
210
+ for (const [name, config] of Object.entries(BUILTIN_POLICY.environments)) {
211
+ environments[name] = normalizeEnvironmentPolicy(config);
212
+ }
213
+ for (const [name, config] of Object.entries(inputEnvironments)) {
214
+ const key = `${name || ''}`.trim().toLowerCase();
215
+ if (!key) {
216
+ continue;
217
+ }
218
+ environments[key] = {
219
+ ...environments[key],
220
+ ...normalizeEnvironmentPolicy(config)
221
+ };
222
+ }
223
+
224
+ const defaultProfile = `${defaults.profile || BUILTIN_POLICY.defaults.profile || 'business-user'}`.trim().toLowerCase() || 'business-user';
225
+ return {
226
+ version: `${policy.version || BUILTIN_POLICY.version || '1.0.0'}`.trim() || '1.0.0',
227
+ defaults: {
228
+ profile: profiles[defaultProfile] ? defaultProfile : 'business-user'
229
+ },
230
+ profiles,
231
+ environments
232
+ };
233
+ }
234
+
235
+ async function loadPolicy(policyPath, cwd) {
236
+ const resolved = resolvePath(cwd, policyPath || DEFAULT_POLICY);
237
+ if (!(await fs.pathExists(resolved))) {
238
+ return {
239
+ source: 'builtin-default',
240
+ from_file: false,
241
+ policy: normalizePolicy(BUILTIN_POLICY)
242
+ };
243
+ }
244
+ const raw = await readJsonFile(resolved, 'authorization tier policy');
245
+ return {
246
+ source: path.relative(cwd, resolved) || '.',
247
+ from_file: true,
248
+ policy: normalizePolicy(raw)
249
+ };
250
+ }
251
+
252
+ function addViolation(violations, severity, code, message, details = {}) {
253
+ violations.push({
254
+ severity,
255
+ code,
256
+ message,
257
+ details
258
+ });
259
+ }
260
+
261
+ function decideFromViolations(violations = []) {
262
+ if (violations.some(item => item && item.severity === 'deny')) {
263
+ return 'deny';
264
+ }
265
+ if (violations.some(item => item && item.severity === 'review')) {
266
+ return 'review-required';
267
+ }
268
+ return 'allow';
269
+ }
270
+
271
+ function evaluateAuthorizationTier(options, policy) {
272
+ const activeProfile = options.dialogueProfile || policy.defaults.profile || 'business-user';
273
+ const profileConfig = policy.profiles[activeProfile];
274
+ if (!profileConfig) {
275
+ throw new Error(`profile not found in policy: ${activeProfile}`);
276
+ }
277
+ const environmentConfig = policy.environments[options.runtimeEnvironment];
278
+ if (!environmentConfig) {
279
+ throw new Error(`runtime environment not found in policy: ${options.runtimeEnvironment}`);
280
+ }
281
+
282
+ const violations = [];
283
+ const executionModeAllowed = profileConfig.allow_execution_modes.includes(options.executionMode);
284
+
285
+ if (!executionModeAllowed) {
286
+ addViolation(
287
+ violations,
288
+ 'deny',
289
+ 'profile-execution-mode-not-allowed',
290
+ `dialogue profile "${activeProfile}" does not allow execution_mode "${options.executionMode}"`,
291
+ {
292
+ dialogue_profile: activeProfile,
293
+ execution_mode: options.executionMode,
294
+ allow_execution_modes: profileConfig.allow_execution_modes
295
+ }
296
+ );
297
+ }
298
+ if (options.autoExecuteLowRisk && profileConfig.auto_execute_allowed !== true) {
299
+ addViolation(
300
+ violations,
301
+ 'deny',
302
+ 'profile-auto-execute-not-allowed',
303
+ `dialogue profile "${activeProfile}" does not allow low-risk auto execute`,
304
+ { dialogue_profile: activeProfile }
305
+ );
306
+ }
307
+ if (options.liveApply && profileConfig.allow_live_apply !== true) {
308
+ addViolation(
309
+ violations,
310
+ 'deny',
311
+ 'profile-live-apply-not-allowed',
312
+ `dialogue profile "${activeProfile}" does not allow live apply`,
313
+ { dialogue_profile: activeProfile }
314
+ );
315
+ }
316
+ if (options.executionMode === 'apply' && environmentConfig.manual_review_required_for_apply === true) {
317
+ addViolation(
318
+ violations,
319
+ 'review',
320
+ 'environment-manual-review-required',
321
+ `runtime environment "${options.runtimeEnvironment}" requires manual review for apply`,
322
+ { runtime_environment: options.runtimeEnvironment }
323
+ );
324
+ }
325
+
326
+ const decision = decideFromViolations(violations);
327
+ const requirements = {
328
+ apply_allowed: executionModeAllowed,
329
+ auto_execute_allowed: decision === 'allow' && profileConfig.auto_execute_allowed === true,
330
+ live_apply_allowed: decision === 'allow' && profileConfig.allow_live_apply === true,
331
+ require_secondary_authorization: environmentConfig.require_secondary_authorization === true,
332
+ require_password_for_apply: environmentConfig.require_password_for_apply === true,
333
+ require_role_policy: environmentConfig.require_role_policy === true,
334
+ require_distinct_actor_roles: environmentConfig.require_distinct_actor_roles === true,
335
+ manual_review_required_for_apply: environmentConfig.manual_review_required_for_apply === true
336
+ };
337
+
338
+ return {
339
+ decision,
340
+ reasons: violations.map(item => item.message),
341
+ violations,
342
+ context: {
343
+ execution_mode: options.executionMode,
344
+ dialogue_profile: activeProfile,
345
+ runtime_mode: options.runtimeMode || null,
346
+ runtime_environment: options.runtimeEnvironment,
347
+ auto_execute_low_risk: options.autoExecuteLowRisk === true,
348
+ live_apply: options.liveApply === true
349
+ },
350
+ requirements
351
+ };
352
+ }
353
+
354
+ async function main() {
355
+ const options = parseArgs(process.argv.slice(2));
356
+ const cwd = process.cwd();
357
+ const outPath = resolvePath(cwd, options.out || DEFAULT_OUT);
358
+ const policyRuntime = await loadPolicy(options.policy, cwd);
359
+ const evaluation = evaluateAuthorizationTier(options, policyRuntime.policy);
360
+
361
+ const payload = {
362
+ mode: 'interactive-authorization-tier-evaluate',
363
+ generated_at: new Date().toISOString(),
364
+ policy: {
365
+ source: policyRuntime.source,
366
+ from_file: policyRuntime.from_file,
367
+ version: policyRuntime.policy.version,
368
+ default_profile: policyRuntime.policy.defaults.profile
369
+ },
370
+ ...evaluation,
371
+ output: {
372
+ json: path.relative(cwd, outPath) || '.'
373
+ }
374
+ };
375
+
376
+ await fs.ensureDir(path.dirname(outPath));
377
+ await fs.writeJson(outPath, payload, { spaces: 2 });
378
+
379
+ if (options.json) {
380
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
381
+ } else {
382
+ process.stdout.write(`Interactive authorization-tier decision: ${payload.decision}\n`);
383
+ process.stdout.write(`- Profile: ${payload.context.dialogue_profile}\n`);
384
+ process.stdout.write(`- Runtime: ${payload.context.runtime_environment}\n`);
385
+ process.stdout.write(`- Output: ${payload.output.json}\n`);
386
+ }
387
+
388
+ if (options.failOnNonAllow && payload.decision !== 'allow') {
389
+ process.exitCode = 2;
390
+ }
391
+ }
392
+
393
+ if (require.main === module) {
394
+ main().catch((error) => {
395
+ console.error(`Interactive authorization-tier evaluate failed: ${error.message}`);
396
+ process.exit(1);
397
+ });
398
+ }
399
+
400
+ module.exports = {
401
+ DEFAULT_POLICY,
402
+ DEFAULT_OUT,
403
+ DIALOGUE_PROFILES,
404
+ RUNTIME_ENVIRONMENTS,
405
+ BUILTIN_POLICY,
406
+ parseArgs,
407
+ resolvePath,
408
+ readJsonFile,
409
+ normalizePolicy,
410
+ loadPolicy,
411
+ evaluateAuthorizationTier,
412
+ main
413
+ };
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const fs = require('fs-extra');
6
+
7
+ const {
8
+ DEFAULT_POLICY,
9
+ DEFAULT_CATALOG,
10
+ toUniqueList,
11
+ normalizeRiskLevel,
12
+ buildCheck,
13
+ evaluatePlanGate
14
+ } = require('../lib/interactive-customization/change-plan-gate-core');
15
+ const DEFAULT_OUT = '.sce/reports/interactive-change-plan-gate.json';
16
+ const DEFAULT_MARKDOWN_OUT = '.sce/reports/interactive-change-plan-gate.md';
17
+
18
+ function parseArgs(argv) {
19
+ const options = {
20
+ plan: null,
21
+ policy: DEFAULT_POLICY,
22
+ catalog: null,
23
+ out: DEFAULT_OUT,
24
+ markdownOut: DEFAULT_MARKDOWN_OUT,
25
+ failOnBlock: false,
26
+ failOnNonAllow: false,
27
+ json: false
28
+ };
29
+
30
+ for (let i = 0; i < argv.length; i += 1) {
31
+ const token = argv[i];
32
+ const next = argv[i + 1];
33
+ if (token === '--plan' && next) {
34
+ options.plan = next;
35
+ i += 1;
36
+ } else if (token === '--policy' && next) {
37
+ options.policy = next;
38
+ i += 1;
39
+ } else if (token === '--catalog' && next) {
40
+ options.catalog = next;
41
+ i += 1;
42
+ } else if (token === '--out' && next) {
43
+ options.out = next;
44
+ i += 1;
45
+ } else if (token === '--markdown-out' && next) {
46
+ options.markdownOut = next;
47
+ i += 1;
48
+ } else if (token === '--fail-on-block') {
49
+ options.failOnBlock = true;
50
+ } else if (token === '--fail-on-non-allow') {
51
+ options.failOnNonAllow = true;
52
+ } else if (token === '--json') {
53
+ options.json = true;
54
+ } else if (token === '--help' || token === '-h') {
55
+ printHelpAndExit(0);
56
+ }
57
+ }
58
+
59
+ if (!options.plan) {
60
+ throw new Error('--plan is required.');
61
+ }
62
+
63
+ return options;
64
+ }
65
+
66
+ function printHelpAndExit(code) {
67
+ const lines = [
68
+ 'Usage: node scripts/interactive-change-plan-gate.js --plan <path> [options]',
69
+ '',
70
+ 'Options:',
71
+ ' --plan <path> Change plan JSON file (required)',
72
+ ` --policy <path> Guardrail policy JSON file (default: ${DEFAULT_POLICY})`,
73
+ ` --catalog <path> High-risk action catalog JSON file (default: ${DEFAULT_CATALOG})`,
74
+ ` --out <path> Report JSON output path (default: ${DEFAULT_OUT})`,
75
+ ` --markdown-out <path> Report markdown output path (default: ${DEFAULT_MARKDOWN_OUT})`,
76
+ ' --fail-on-block Exit code 2 when decision is deny',
77
+ ' --fail-on-non-allow Exit code 2 when decision is deny/review-required',
78
+ ' --json Print JSON report to stdout',
79
+ ' -h, --help Show this help'
80
+ ];
81
+ console.log(lines.join('\n'));
82
+ process.exit(code);
83
+ }
84
+
85
+ function parseJson(text, sourceLabel) {
86
+ try {
87
+ return JSON.parse(text);
88
+ } catch (error) {
89
+ throw new Error(`invalid JSON in ${sourceLabel}: ${error.message}`);
90
+ }
91
+ }
92
+
93
+ async function readJsonFile(filePath, sourceLabel) {
94
+ if (!(await fs.pathExists(filePath))) {
95
+ throw new Error(`${sourceLabel} not found: ${filePath}`);
96
+ }
97
+ const content = await fs.readFile(filePath, 'utf8');
98
+ return parseJson(content, sourceLabel);
99
+ }
100
+
101
+ function buildMarkdown(report) {
102
+ const lines = [];
103
+ lines.push('# Interactive Change Plan Gate');
104
+ lines.push('');
105
+ lines.push(`- Generated at: ${report.generated_at}`);
106
+ lines.push(`- Decision: ${report.decision}`);
107
+ lines.push(`- Plan: ${report.inputs.plan}`);
108
+ lines.push(`- Policy: ${report.inputs.policy}`);
109
+ lines.push(`- Catalog: ${report.inputs.catalog}`);
110
+ lines.push('');
111
+ lines.push('## Summary');
112
+ lines.push('');
113
+ lines.push(`- Checks: ${report.summary.check_total}`);
114
+ lines.push(`- Failed checks: ${report.summary.failed_total}`);
115
+ lines.push(`- Failed deny checks: ${report.summary.failed_deny_total}`);
116
+ lines.push(`- Failed review checks: ${report.summary.failed_review_total}`);
117
+ lines.push(`- Action count: ${report.summary.action_count}`);
118
+ lines.push(`- Risk level: ${report.summary.risk_level || 'n/a'}`);
119
+ lines.push('');
120
+ lines.push('## Checks');
121
+ lines.push('');
122
+ lines.push('| Check | Result | Severity | Details |');
123
+ lines.push('| --- | --- | --- | --- |');
124
+ for (const check of report.checks) {
125
+ lines.push(
126
+ `| ${check.id} | ${check.passed ? 'pass' : 'fail'} | ${check.severity} | ${check.details || 'n/a'} |`
127
+ );
128
+ }
129
+ lines.push('');
130
+ lines.push('## Reasons');
131
+ lines.push('');
132
+ if (!Array.isArray(report.reasons) || report.reasons.length === 0) {
133
+ lines.push('- none');
134
+ } else {
135
+ for (const reason of report.reasons) {
136
+ lines.push(`- ${reason}`);
137
+ }
138
+ }
139
+ return `${lines.join('\n')}\n`;
140
+ }
141
+
142
+ function resolveReportPath(cwd, value) {
143
+ return path.isAbsolute(value) ? value : path.resolve(cwd, value);
144
+ }
145
+
146
+ async function main() {
147
+ const options = parseArgs(process.argv.slice(2));
148
+ const cwd = process.cwd();
149
+
150
+ const planPath = resolveReportPath(cwd, options.plan);
151
+ const policyPath = resolveReportPath(cwd, options.policy);
152
+ const policy = await readJsonFile(policyPath, 'policy');
153
+ const catalogFromPolicy = policy && policy.catalog_policy && typeof policy.catalog_policy.catalog_file === 'string'
154
+ ? policy.catalog_policy.catalog_file
155
+ : null;
156
+ const catalogPath = resolveReportPath(
157
+ cwd,
158
+ options.catalog || catalogFromPolicy || DEFAULT_CATALOG
159
+ );
160
+ const [plan, catalog] = await Promise.all([
161
+ readJsonFile(planPath, 'plan'),
162
+ readJsonFile(catalogPath, 'catalog')
163
+ ]);
164
+
165
+ const evaluation = evaluatePlanGate(plan, policy, catalog);
166
+ const outPath = resolveReportPath(cwd, options.out);
167
+ const markdownOutPath = resolveReportPath(cwd, options.markdownOut);
168
+ const report = {
169
+ mode: 'interactive-change-plan-gate',
170
+ generated_at: new Date().toISOString(),
171
+ inputs: {
172
+ plan: path.relative(cwd, planPath) || '.',
173
+ policy: path.relative(cwd, policyPath) || '.',
174
+ catalog: path.relative(cwd, catalogPath) || '.'
175
+ },
176
+ ...evaluation,
177
+ output: {
178
+ json: path.relative(cwd, outPath) || '.',
179
+ markdown: path.relative(cwd, markdownOutPath) || '.'
180
+ }
181
+ };
182
+
183
+ await fs.ensureDir(path.dirname(outPath));
184
+ await fs.writeJson(outPath, report, { spaces: 2 });
185
+ await fs.ensureDir(path.dirname(markdownOutPath));
186
+ await fs.writeFile(markdownOutPath, buildMarkdown(report), 'utf8');
187
+
188
+ if (options.json) {
189
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
190
+ } else {
191
+ process.stdout.write(`Interactive change plan gate: ${report.decision}\n`);
192
+ process.stdout.write(`- JSON: ${report.output.json}\n`);
193
+ process.stdout.write(`- Markdown: ${report.output.markdown}\n`);
194
+ }
195
+
196
+ if (options.failOnBlock && report.decision === 'deny') {
197
+ process.exitCode = 2;
198
+ } else if (options.failOnNonAllow && report.decision !== 'allow') {
199
+ process.exitCode = 2;
200
+ }
201
+ }
202
+
203
+ if (require.main === module) {
204
+ main().catch((error) => {
205
+ console.error(`Interactive change plan gate failed: ${error.message}`);
206
+ process.exit(1);
207
+ });
208
+ }
209
+
210
+ module.exports = {
211
+ DEFAULT_POLICY,
212
+ DEFAULT_CATALOG,
213
+ DEFAULT_OUT,
214
+ DEFAULT_MARKDOWN_OUT,
215
+ parseArgs,
216
+ parseJson,
217
+ readJsonFile,
218
+ toUniqueList,
219
+ normalizeRiskLevel,
220
+ buildCheck,
221
+ evaluatePlanGate,
222
+ buildMarkdown,
223
+ resolveReportPath,
224
+ main
225
+ };