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,707 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const fs = require('fs-extra');
6
+ const crypto = require('crypto');
7
+
8
+ const DEFAULT_OUT_INTENT = '.sce/reports/interactive-change-intent.json';
9
+ const DEFAULT_OUT_EXPLAIN = '.sce/reports/interactive-page-explain.md';
10
+ const DEFAULT_AUDIT_FILE = '.sce/reports/interactive-copilot-audit.jsonl';
11
+ const DEFAULT_CONTEXT_CONTRACT = 'docs/interactive-customization/moqui-copilot-context-contract.json';
12
+ const DEFAULT_MASK_KEYWORDS = [
13
+ 'password',
14
+ 'secret',
15
+ 'token',
16
+ 'api_key',
17
+ 'apikey',
18
+ 'credential',
19
+ 'ssn',
20
+ 'bank',
21
+ 'card',
22
+ 'email',
23
+ 'phone'
24
+ ];
25
+ const BUILTIN_CONTEXT_CONTRACT = {
26
+ version: '1.1.0',
27
+ product: 'scene-capability-engine',
28
+ context_contract: {
29
+ required_fields: ['product', 'module', 'page'],
30
+ optional_fields: [
31
+ 'entity',
32
+ 'scene_id',
33
+ 'workflow_node',
34
+ 'fields',
35
+ 'current_state',
36
+ 'scene_workspace',
37
+ 'assistant_panel'
38
+ ],
39
+ max_field_count: 400,
40
+ max_payload_kb: 512
41
+ },
42
+ security_contract: {
43
+ mode: 'read-only',
44
+ masking_required: true,
45
+ sensitive_key_patterns: [
46
+ 'password',
47
+ 'secret',
48
+ 'token',
49
+ 'api_key',
50
+ 'apikey',
51
+ 'credential',
52
+ 'email',
53
+ 'phone',
54
+ 'bank',
55
+ 'card'
56
+ ],
57
+ forbidden_keys: [
58
+ 'raw_password',
59
+ 'private_key',
60
+ 'access_token_plaintext'
61
+ ]
62
+ },
63
+ runtime_contract: {
64
+ provider: 'ui-context-provider',
65
+ transport: 'json',
66
+ schema: 'docs/interactive-customization/page-context.schema.json',
67
+ consumer: 'scripts/interactive-intent-build.js'
68
+ }
69
+ };
70
+
71
+ function parseArgs(argv) {
72
+ const options = {
73
+ context: null,
74
+ goal: null,
75
+ goalFile: null,
76
+ userId: 'anonymous-user',
77
+ sessionId: null,
78
+ outIntent: DEFAULT_OUT_INTENT,
79
+ outExplain: DEFAULT_OUT_EXPLAIN,
80
+ auditFile: DEFAULT_AUDIT_FILE,
81
+ contextContract: DEFAULT_CONTEXT_CONTRACT,
82
+ contextContractExplicit: false,
83
+ strictContract: true,
84
+ maskKeys: [],
85
+ json: false
86
+ };
87
+
88
+ for (let i = 0; i < argv.length; i += 1) {
89
+ const token = argv[i];
90
+ const next = argv[i + 1];
91
+ if (token === '--context' && next) {
92
+ options.context = next;
93
+ i += 1;
94
+ } else if (token === '--goal' && next) {
95
+ options.goal = next;
96
+ i += 1;
97
+ } else if (token === '--goal-file' && next) {
98
+ options.goalFile = next;
99
+ i += 1;
100
+ } else if (token === '--user-id' && next) {
101
+ options.userId = next;
102
+ i += 1;
103
+ } else if (token === '--session-id' && next) {
104
+ options.sessionId = next;
105
+ i += 1;
106
+ } else if (token === '--out-intent' && next) {
107
+ options.outIntent = next;
108
+ i += 1;
109
+ } else if (token === '--out-explain' && next) {
110
+ options.outExplain = next;
111
+ i += 1;
112
+ } else if (token === '--audit-file' && next) {
113
+ options.auditFile = next;
114
+ i += 1;
115
+ } else if (token === '--context-contract' && next) {
116
+ options.contextContract = next;
117
+ options.contextContractExplicit = true;
118
+ i += 1;
119
+ } else if (token === '--no-strict-contract') {
120
+ options.strictContract = false;
121
+ } else if (token === '--mask-keys' && next) {
122
+ options.maskKeys = next.split(',').map(item => item.trim()).filter(Boolean);
123
+ i += 1;
124
+ } else if (token === '--json') {
125
+ options.json = true;
126
+ } else if (token === '--help' || token === '-h') {
127
+ printHelpAndExit(0);
128
+ }
129
+ }
130
+
131
+ if (!options.context) {
132
+ throw new Error('--context is required.');
133
+ }
134
+ if (!options.goal && !options.goalFile) {
135
+ throw new Error('either --goal or --goal-file is required.');
136
+ }
137
+
138
+ return options;
139
+ }
140
+
141
+ function printHelpAndExit(code) {
142
+ const lines = [
143
+ 'Usage: node scripts/interactive-intent-build.js --context <path> (--goal <text> | --goal-file <path>) [options]',
144
+ '',
145
+ 'Options:',
146
+ ' --context <path> Page context JSON file (required)',
147
+ ' --goal <text> Business goal text',
148
+ ' --goal-file <path> File containing business goal text',
149
+ ' --user-id <id> User identifier (default: anonymous-user)',
150
+ ' --session-id <id> Optional session identifier',
151
+ ` --out-intent <path> Intent output JSON file (default: ${DEFAULT_OUT_INTENT})`,
152
+ ` --out-explain <path> Explain output markdown file (default: ${DEFAULT_OUT_EXPLAIN})`,
153
+ ` --audit-file <path> Audit events JSONL file (default: ${DEFAULT_AUDIT_FILE})`,
154
+ ` --context-contract <path> Context contract JSON file (default: ${DEFAULT_CONTEXT_CONTRACT}, fallback to built-in baseline when missing)`,
155
+ ' --no-strict-contract Do not fail when context contract validation has issues',
156
+ ' --mask-keys <csv> Additional sensitive key names to mask',
157
+ ' --json Print result JSON to stdout',
158
+ ' -h, --help Show this help'
159
+ ];
160
+ console.log(lines.join('\n'));
161
+ process.exit(code);
162
+ }
163
+
164
+ function resolveFile(cwd, value) {
165
+ return path.isAbsolute(value) ? value : path.resolve(cwd, value);
166
+ }
167
+
168
+ async function readJsonFile(absolutePath, label) {
169
+ if (!(await fs.pathExists(absolutePath))) {
170
+ throw new Error(`${label} not found: ${absolutePath}`);
171
+ }
172
+ const text = await fs.readFile(absolutePath, 'utf8');
173
+ try {
174
+ return JSON.parse(text);
175
+ } catch (error) {
176
+ throw new Error(`invalid JSON in ${label}: ${error.message}`);
177
+ }
178
+ }
179
+
180
+ async function readGoal(options, cwd) {
181
+ if (typeof options.goal === 'string' && options.goal.trim().length > 0) {
182
+ return options.goal.trim();
183
+ }
184
+ const goalFilePath = resolveFile(cwd, options.goalFile);
185
+ if (!(await fs.pathExists(goalFilePath))) {
186
+ throw new Error(`goal file not found: ${goalFilePath}`);
187
+ }
188
+ const text = await fs.readFile(goalFilePath, 'utf8');
189
+ const goal = `${text || ''}`.trim();
190
+ if (!goal) {
191
+ throw new Error('goal text is empty.');
192
+ }
193
+ return goal;
194
+ }
195
+
196
+ function toStringArray(input) {
197
+ if (!Array.isArray(input)) {
198
+ return [];
199
+ }
200
+ return Array.from(
201
+ new Set(
202
+ input
203
+ .map(item => `${item || ''}`.trim())
204
+ .filter(Boolean)
205
+ )
206
+ );
207
+ }
208
+
209
+ function toNumberOrNull(value) {
210
+ const numeric = Number(value);
211
+ return Number.isFinite(numeric) ? numeric : null;
212
+ }
213
+
214
+ function normalizeContextContract(rawContract = {}) {
215
+ const contract = rawContract && typeof rawContract === 'object'
216
+ ? rawContract
217
+ : {};
218
+ const contextContract = contract.context_contract && typeof contract.context_contract === 'object'
219
+ ? contract.context_contract
220
+ : {};
221
+ const securityContract = contract.security_contract && typeof contract.security_contract === 'object'
222
+ ? contract.security_contract
223
+ : {};
224
+
225
+ return {
226
+ version: `${contract.version || BUILTIN_CONTEXT_CONTRACT.version}`,
227
+ product: `${contract.product || BUILTIN_CONTEXT_CONTRACT.product}`,
228
+ context_contract: {
229
+ required_fields: toStringArray(
230
+ contextContract.required_fields || BUILTIN_CONTEXT_CONTRACT.context_contract.required_fields
231
+ ),
232
+ optional_fields: toStringArray(
233
+ contextContract.optional_fields || BUILTIN_CONTEXT_CONTRACT.context_contract.optional_fields
234
+ ),
235
+ max_field_count: toNumberOrNull(
236
+ contextContract.max_field_count != null
237
+ ? contextContract.max_field_count
238
+ : BUILTIN_CONTEXT_CONTRACT.context_contract.max_field_count
239
+ ),
240
+ max_payload_kb: toNumberOrNull(
241
+ contextContract.max_payload_kb != null
242
+ ? contextContract.max_payload_kb
243
+ : BUILTIN_CONTEXT_CONTRACT.context_contract.max_payload_kb
244
+ )
245
+ },
246
+ security_contract: {
247
+ mode: `${securityContract.mode || BUILTIN_CONTEXT_CONTRACT.security_contract.mode}`,
248
+ masking_required: securityContract.masking_required !== false,
249
+ sensitive_key_patterns: toStringArray(
250
+ securityContract.sensitive_key_patterns || BUILTIN_CONTEXT_CONTRACT.security_contract.sensitive_key_patterns
251
+ ).map(item => item.toLowerCase()),
252
+ forbidden_keys: toStringArray(
253
+ securityContract.forbidden_keys || BUILTIN_CONTEXT_CONTRACT.security_contract.forbidden_keys
254
+ ).map(item => item.toLowerCase())
255
+ },
256
+ runtime_contract: contract.runtime_contract && typeof contract.runtime_contract === 'object'
257
+ ? contract.runtime_contract
258
+ : BUILTIN_CONTEXT_CONTRACT.runtime_contract
259
+ };
260
+ }
261
+
262
+ async function loadContextContract(options, cwd) {
263
+ const defaultContractPath = resolveFile(cwd, DEFAULT_CONTEXT_CONTRACT);
264
+ const contractPath = options.contextContract
265
+ ? resolveFile(cwd, options.contextContract)
266
+ : defaultContractPath;
267
+
268
+ const contractExists = await fs.pathExists(contractPath);
269
+ if (contractExists) {
270
+ const contract = await readJsonFile(contractPath, 'context contract');
271
+ return {
272
+ source: path.relative(cwd, contractPath) || '.',
273
+ source_abs: contractPath,
274
+ from_file: true,
275
+ contract: normalizeContextContract(contract)
276
+ };
277
+ }
278
+
279
+ if (options.contextContractExplicit === true) {
280
+ throw new Error(`context contract not found: ${contractPath}`);
281
+ }
282
+
283
+ return {
284
+ source: 'builtin-default',
285
+ source_abs: null,
286
+ from_file: false,
287
+ contract: normalizeContextContract(BUILTIN_CONTEXT_CONTRACT)
288
+ };
289
+ }
290
+
291
+ function hasContextFieldValue(value) {
292
+ if (value === null || value === undefined) {
293
+ return false;
294
+ }
295
+ if (typeof value === 'string') {
296
+ return value.trim().length > 0;
297
+ }
298
+ if (Array.isArray(value)) {
299
+ return value.length > 0;
300
+ }
301
+ return true;
302
+ }
303
+
304
+ function collectForbiddenKeyHits(input, forbiddenKeys, prefix = []) {
305
+ if (input === null || input === undefined || typeof input !== 'object') {
306
+ return [];
307
+ }
308
+
309
+ const hits = [];
310
+ for (const [key, value] of Object.entries(input)) {
311
+ const keyLower = `${key || ''}`.trim().toLowerCase();
312
+ const nextPrefix = [...prefix, key];
313
+ if (forbiddenKeys.includes(keyLower)) {
314
+ hits.push(nextPrefix.join('.'));
315
+ }
316
+
317
+ if (Array.isArray(value)) {
318
+ value.forEach((item, index) => {
319
+ hits.push(...collectForbiddenKeyHits(item, forbiddenKeys, [...nextPrefix, String(index)]));
320
+ });
321
+ } else if (value && typeof value === 'object') {
322
+ hits.push(...collectForbiddenKeyHits(value, forbiddenKeys, nextPrefix));
323
+ }
324
+ }
325
+
326
+ return hits;
327
+ }
328
+
329
+ function validateContextAgainstContract(context, contract) {
330
+ const contextContract = contract && contract.context_contract
331
+ ? contract.context_contract
332
+ : {};
333
+ const securityContract = contract && contract.security_contract
334
+ ? contract.security_contract
335
+ : {};
336
+ const requiredFields = toStringArray(contextContract.required_fields);
337
+ const maxFieldCount = toNumberOrNull(contextContract.max_field_count);
338
+ const maxPayloadKb = toNumberOrNull(contextContract.max_payload_kb);
339
+ const forbiddenKeys = toStringArray(securityContract.forbidden_keys).map(item => item.toLowerCase());
340
+
341
+ const issues = [];
342
+ const fields = Array.isArray(context && context.fields) ? context.fields : [];
343
+ const payloadBytes = Buffer.byteLength(JSON.stringify(context || {}), 'utf8');
344
+ const payloadKb = Number((payloadBytes / 1024).toFixed(2));
345
+
346
+ const missingRequired = requiredFields.filter(field => !hasContextFieldValue(context && context[field]));
347
+ if (missingRequired.length > 0) {
348
+ issues.push(`missing required fields: ${missingRequired.join(', ')}`);
349
+ }
350
+ if (maxFieldCount !== null && fields.length > maxFieldCount) {
351
+ issues.push(`fields count ${fields.length} exceeds max_field_count ${maxFieldCount}`);
352
+ }
353
+ if (maxPayloadKb !== null && payloadKb > maxPayloadKb) {
354
+ issues.push(`payload size ${payloadKb}KB exceeds max_payload_kb ${maxPayloadKb}KB`);
355
+ }
356
+
357
+ const forbiddenKeyHits = collectForbiddenKeyHits(context, forbiddenKeys);
358
+ if (forbiddenKeyHits.length > 0) {
359
+ issues.push(`forbidden keys present: ${forbiddenKeyHits.slice(0, 8).join(', ')}`);
360
+ }
361
+
362
+ return {
363
+ valid: issues.length === 0,
364
+ issues,
365
+ metrics: {
366
+ required_fields_total: requiredFields.length,
367
+ field_total: fields.length,
368
+ payload_kb: payloadKb,
369
+ max_field_count: maxFieldCount,
370
+ max_payload_kb: maxPayloadKb,
371
+ forbidden_key_hits: forbiddenKeyHits.length
372
+ }
373
+ };
374
+ }
375
+
376
+ function normalizeMaskKeywords(extraKeywords = [], contractKeywords = []) {
377
+ const normalized = [
378
+ ...DEFAULT_MASK_KEYWORDS,
379
+ ...contractKeywords,
380
+ ...extraKeywords
381
+ ]
382
+ .map(item => `${item || ''}`.trim().toLowerCase())
383
+ .filter(Boolean);
384
+ return Array.from(new Set(normalized));
385
+ }
386
+
387
+ function isSensitiveKeyName(key, keywords) {
388
+ const lower = `${key || ''}`.trim().toLowerCase();
389
+ if (!lower) {
390
+ return false;
391
+ }
392
+ return keywords.some(keyword => lower.includes(keyword));
393
+ }
394
+
395
+ function maskContextValue(input, keywords, parentSensitive = false) {
396
+ if (input === null || input === undefined) {
397
+ return input;
398
+ }
399
+
400
+ if (Array.isArray(input)) {
401
+ return input.map(item => maskContextValue(item, keywords, parentSensitive));
402
+ }
403
+
404
+ if (typeof input !== 'object') {
405
+ return parentSensitive ? '[REDACTED]' : input;
406
+ }
407
+
408
+ const output = {};
409
+ for (const [key, value] of Object.entries(input)) {
410
+ const childSensitive = parentSensitive || isSensitiveKeyName(key, keywords);
411
+ if (typeof value === 'object' && value !== null) {
412
+ output[key] = maskContextValue(value, keywords, childSensitive);
413
+ } else {
414
+ output[key] = childSensitive ? '[REDACTED]' : value;
415
+ }
416
+ }
417
+ return output;
418
+ }
419
+
420
+ function parseConstraints(goal) {
421
+ const text = `${goal || ''}`.trim();
422
+ if (!text) {
423
+ return [];
424
+ }
425
+
426
+ const patterns = [
427
+ /\bmust\b[^.?!]*/ig,
428
+ /\bcannot\b[^.?!]*/ig,
429
+ /\bwithout\b[^.?!]*/ig,
430
+ /\bneed to\b[^.?!]*/ig,
431
+ /\bshould\b[^.?!]*/ig
432
+ ];
433
+ const found = [];
434
+ for (const pattern of patterns) {
435
+ const matches = text.match(pattern) || [];
436
+ matches.forEach(item => {
437
+ const normalized = item.trim().replace(/\s+/g, ' ');
438
+ if (normalized.length > 0 && !found.includes(normalized)) {
439
+ found.push(normalized);
440
+ }
441
+ });
442
+ }
443
+ return found.slice(0, 8);
444
+ }
445
+
446
+ function inferPriority(goal) {
447
+ const text = `${goal || ''}`.toLowerCase();
448
+ if (/(urgent|asap|immediately|critical)/.test(text)) {
449
+ return 'high';
450
+ }
451
+ if (/(later|eventually|optional|nice to have)/.test(text)) {
452
+ return 'low';
453
+ }
454
+ return 'medium';
455
+ }
456
+
457
+ function inferRiskHint(goal, context = {}, contextAnalysis = {}) {
458
+ const merged = `${goal || ''} ${(context.module || '')} ${(context.entity || '')}`.toLowerCase();
459
+ if (/(delete|drop|permission|privilege|payment|credential|secret|token)/.test(merged)) {
460
+ return 'high';
461
+ }
462
+ if (Number(contextAnalysis.forbidden_key_hits || 0) > 0) {
463
+ return 'high';
464
+ }
465
+ if (Number(contextAnalysis.ontology_decision_total || 0) > 0 || Number(contextAnalysis.ontology_business_rule_total || 0) > 0) {
466
+ return 'medium';
467
+ }
468
+ if (/(approval|workflow|inventory|customer|order|pricing|refund)/.test(merged)) {
469
+ return 'medium';
470
+ }
471
+ return 'low';
472
+ }
473
+
474
+ function analyzeContext(context, keywords) {
475
+ const fields = Array.isArray(context && context.fields) ? context.fields : [];
476
+ const workspace = context && context.scene_workspace && typeof context.scene_workspace === 'object'
477
+ ? context.scene_workspace
478
+ : {};
479
+ const ontology = workspace.ontology && typeof workspace.ontology === 'object'
480
+ ? workspace.ontology
481
+ : (context && context.ontology && typeof context.ontology === 'object' ? context.ontology : {});
482
+ const assistantPanel = workspace.assistant_panel && typeof workspace.assistant_panel === 'object'
483
+ ? workspace.assistant_panel
484
+ : (context && context.assistant_panel && typeof context.assistant_panel === 'object' ? context.assistant_panel : {});
485
+ const explorer = workspace.screen_explorer && typeof workspace.screen_explorer === 'object'
486
+ ? workspace.screen_explorer
487
+ : {};
488
+ const sensitiveFieldCount = fields.filter(field => {
489
+ if (!field || typeof field !== 'object') {
490
+ return false;
491
+ }
492
+ if (field.sensitive === true) {
493
+ return true;
494
+ }
495
+ return isSensitiveKeyName(field.name || '', keywords);
496
+ }).length;
497
+
498
+ return {
499
+ field_total: fields.length,
500
+ sensitive_field_total: sensitiveFieldCount,
501
+ workflow_node: context && context.workflow_node ? context.workflow_node : null,
502
+ ontology_entity_total: Array.isArray(ontology.entities) ? ontology.entities.length : 0,
503
+ ontology_relation_total: Array.isArray(ontology.relations) ? ontology.relations.length : 0,
504
+ ontology_business_rule_total: Array.isArray(ontology.business_rules) ? ontology.business_rules.length : 0,
505
+ ontology_decision_total: Array.isArray(ontology.decision_policies) ? ontology.decision_policies.length : 0,
506
+ has_scene_workspace: Boolean(context && context.scene_workspace),
507
+ explorer_result_total: Number.isFinite(Number(explorer.result_total)) ? Number(explorer.result_total) : null,
508
+ explorer_selected_component: explorer.selected_component || null,
509
+ assistant_session_id: assistantPanel.session_id || null
510
+ };
511
+ }
512
+
513
+ function sha256Hex(input) {
514
+ return crypto.createHash('sha256').update(String(input)).digest('hex');
515
+ }
516
+
517
+ function buildExplainMarkdown(payload) {
518
+ const lines = [];
519
+ const contract = payload.contract_validation || {};
520
+ lines.push('# Interactive Copilot Read-Only Explain');
521
+ lines.push('');
522
+ lines.push(`- Generated at: ${payload.generated_at}`);
523
+ lines.push(`- Session: ${payload.session_id}`);
524
+ lines.push(`- User: ${payload.user_id}`);
525
+ lines.push(`- Read-only mode: yes`);
526
+ lines.push(`- Product/module/page: ${payload.intent.context_ref.product}/${payload.intent.context_ref.module}/${payload.intent.context_ref.page || 'n/a'}`);
527
+ lines.push(`- Entity: ${payload.intent.context_ref.entity || 'n/a'}`);
528
+ lines.push(`- Scene: ${payload.intent.context_ref.scene_id || 'n/a'}`);
529
+ lines.push(`- Workflow node: ${payload.intent.context_ref.workflow_node || 'n/a'}`);
530
+ lines.push('');
531
+ lines.push('## Goal');
532
+ lines.push('');
533
+ lines.push(payload.goal);
534
+ lines.push('');
535
+ lines.push('## Constraints');
536
+ lines.push('');
537
+ if (!Array.isArray(payload.intent.constraints) || payload.intent.constraints.length === 0) {
538
+ lines.push('- none detected');
539
+ } else {
540
+ payload.intent.constraints.forEach(item => lines.push(`- ${item}`));
541
+ }
542
+ lines.push('');
543
+ lines.push('## Context Summary');
544
+ lines.push('');
545
+ lines.push(`- Fields: ${payload.context_analysis.field_total}`);
546
+ lines.push(`- Sensitive fields: ${payload.context_analysis.sensitive_field_total}`);
547
+ lines.push(`- Ontology entities: ${payload.context_analysis.ontology_entity_total}`);
548
+ lines.push(`- Ontology relations: ${payload.context_analysis.ontology_relation_total}`);
549
+ lines.push(`- Business rules: ${payload.context_analysis.ontology_business_rule_total}`);
550
+ lines.push(`- Decision policies: ${payload.context_analysis.ontology_decision_total}`);
551
+ lines.push(`- Explorer selected component: ${payload.context_analysis.explorer_selected_component || 'n/a'}`);
552
+ lines.push(`- Assistant session: ${payload.context_analysis.assistant_session_id || 'n/a'}`);
553
+ lines.push(`- Risk hint: ${payload.risk_hint}`);
554
+ lines.push('');
555
+ lines.push('## Contract Validation');
556
+ lines.push('');
557
+ lines.push(`- Source: ${contract.source || 'n/a'}`);
558
+ lines.push(`- Strict mode: ${contract.strict ? 'yes' : 'no'}`);
559
+ lines.push(`- Result: ${contract.valid ? 'pass' : 'fail'}`);
560
+ if (contract.metrics) {
561
+ lines.push(`- Payload size: ${contract.metrics.payload_kb}KB`);
562
+ lines.push(`- Max payload: ${contract.metrics.max_payload_kb == null ? 'n/a' : `${contract.metrics.max_payload_kb}KB`}`);
563
+ lines.push(`- Forbidden key hits: ${contract.metrics.forbidden_key_hits}`);
564
+ }
565
+ if (Array.isArray(contract.issues) && contract.issues.length > 0) {
566
+ lines.push('- Issues:');
567
+ contract.issues.forEach(item => lines.push(` - ${item}`));
568
+ }
569
+ lines.push('');
570
+ lines.push('## Execution Policy');
571
+ lines.push('');
572
+ lines.push('- This output is read-only and suggestion-first.');
573
+ lines.push('- No write operation is executed by this script.');
574
+ return `${lines.join('\n')}\n`;
575
+ }
576
+
577
+ async function main() {
578
+ const options = parseArgs(process.argv.slice(2));
579
+ const cwd = process.cwd();
580
+ const contextPath = resolveFile(cwd, options.context);
581
+ const outIntentPath = resolveFile(cwd, options.outIntent);
582
+ const outExplainPath = resolveFile(cwd, options.outExplain);
583
+ const auditFilePath = resolveFile(cwd, options.auditFile);
584
+ const goal = await readGoal(options, cwd);
585
+ const rawContext = await readJsonFile(contextPath, 'context');
586
+ const contractRuntime = await loadContextContract(options, cwd);
587
+ const contractValidation = validateContextAgainstContract(rawContext, contractRuntime.contract);
588
+ if (options.strictContract && !contractValidation.valid) {
589
+ throw new Error(`context contract validation failed: ${contractValidation.issues.join(' | ')}`);
590
+ }
591
+ const contractMaskKeywords = contractRuntime.contract
592
+ && contractRuntime.contract.security_contract
593
+ && Array.isArray(contractRuntime.contract.security_contract.sensitive_key_patterns)
594
+ ? contractRuntime.contract.security_contract.sensitive_key_patterns
595
+ : [];
596
+ const maskKeywords = normalizeMaskKeywords(options.maskKeys, contractMaskKeywords);
597
+ const sanitizedContext = maskContextValue(rawContext, maskKeywords, false);
598
+ const contextAnalysis = {
599
+ ...analyzeContext(rawContext, maskKeywords),
600
+ forbidden_key_hits: contractValidation.metrics.forbidden_key_hits
601
+ };
602
+ const createdAt = new Date().toISOString();
603
+ const sessionId = options.sessionId || `session-${crypto.randomUUID()}`;
604
+ const intentId = `intent-${crypto.randomUUID()}`;
605
+ const riskHint = inferRiskHint(goal, rawContext, contextAnalysis);
606
+ const workspace = rawContext && rawContext.scene_workspace && typeof rawContext.scene_workspace === 'object'
607
+ ? rawContext.scene_workspace
608
+ : {};
609
+ const explorer = workspace.screen_explorer && typeof workspace.screen_explorer === 'object'
610
+ ? workspace.screen_explorer
611
+ : {};
612
+
613
+ const contextRef = {
614
+ product: `${rawContext.product || rawContext.app || 'unknown-product'}`.trim(),
615
+ module: `${rawContext.module || 'unknown-module'}`.trim(),
616
+ page: rawContext.page || null,
617
+ entity: rawContext.entity || null,
618
+ scene_id: rawContext.scene_id || null,
619
+ workflow_node: rawContext.workflow_node || null,
620
+ screen: rawContext.screen || explorer.selected_screen || null,
621
+ component: rawContext.component || explorer.selected_component || null
622
+ };
623
+
624
+ const intent = {
625
+ intent_id: intentId,
626
+ session_id: sessionId,
627
+ user_id: options.userId,
628
+ context_ref: contextRef,
629
+ business_goal: goal,
630
+ constraints: parseConstraints(goal),
631
+ priority: inferPriority(goal),
632
+ created_at: createdAt,
633
+ metadata: {
634
+ mode: 'read-only',
635
+ source: 'interactive-intent-build',
636
+ context_summary: contextAnalysis,
637
+ risk_hint: riskHint,
638
+ contract_validation: {
639
+ valid: contractValidation.valid,
640
+ issues_count: contractValidation.issues.length,
641
+ source: contractRuntime.source
642
+ }
643
+ }
644
+ };
645
+
646
+ const auditEvent = {
647
+ event_id: `event-${crypto.randomUUID()}`,
648
+ event_type: 'interactive.intent.generated',
649
+ timestamp: createdAt,
650
+ readonly: true,
651
+ user_id: options.userId,
652
+ session_id: sessionId,
653
+ intent_id: intentId,
654
+ context_ref: contextRef,
655
+ risk_hint: riskHint,
656
+ context_hash: sha256Hex(JSON.stringify(sanitizedContext)),
657
+ contract_valid: contractValidation.valid,
658
+ contract_source: contractRuntime.source
659
+ };
660
+
661
+ const payload = {
662
+ mode: 'interactive-intent-build',
663
+ generated_at: createdAt,
664
+ readonly: true,
665
+ user_id: options.userId,
666
+ session_id: sessionId,
667
+ goal,
668
+ risk_hint: riskHint,
669
+ context_analysis: contextAnalysis,
670
+ contract_validation: {
671
+ source: contractRuntime.source,
672
+ from_file: contractRuntime.from_file,
673
+ strict: options.strictContract,
674
+ valid: contractValidation.valid,
675
+ issues: contractValidation.issues,
676
+ metrics: contractValidation.metrics
677
+ },
678
+ intent,
679
+ sanitized_context_preview: sanitizedContext,
680
+ output: {
681
+ intent: path.relative(cwd, outIntentPath) || '.',
682
+ explain: path.relative(cwd, outExplainPath) || '.',
683
+ audit: path.relative(cwd, auditFilePath) || '.'
684
+ }
685
+ };
686
+
687
+ await fs.ensureDir(path.dirname(outIntentPath));
688
+ await fs.writeJson(outIntentPath, intent, { spaces: 2 });
689
+ await fs.ensureDir(path.dirname(outExplainPath));
690
+ await fs.writeFile(outExplainPath, buildExplainMarkdown(payload), 'utf8');
691
+ await fs.ensureDir(path.dirname(auditFilePath));
692
+ await fs.appendFile(auditFilePath, `${JSON.stringify(auditEvent)}\n`, 'utf8');
693
+
694
+ if (options.json) {
695
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
696
+ } else {
697
+ process.stdout.write('Interactive intent built (read-only).\n');
698
+ process.stdout.write(`- Intent: ${payload.output.intent}\n`);
699
+ process.stdout.write(`- Explain: ${payload.output.explain}\n`);
700
+ process.stdout.write(`- Audit: ${payload.output.audit}\n`);
701
+ }
702
+ }
703
+
704
+ main().catch((error) => {
705
+ console.error(`Interactive intent build failed: ${error.message}`);
706
+ process.exit(1);
707
+ });