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,617 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const fs = require('fs-extra');
6
+
7
+ const DEFAULT_PROVIDER = 'moqui';
8
+ const DEFAULT_CONTEXT_CONTRACT = 'docs/interactive-customization/moqui-copilot-context-contract.json';
9
+ const DEFAULT_OUT_CONTEXT = '.sce/reports/interactive-page-context.normalized.json';
10
+ const DEFAULT_OUT_REPORT = '.sce/reports/interactive-context-bridge.json';
11
+ const SUPPORTED_PROVIDERS = new Set(['moqui', 'generic']);
12
+ const SENSITIVE_NAME_PATTERN = /(password|secret|token|api[_-]?key|credential|email|phone|bank|card)/i;
13
+ const BUILTIN_CONTEXT_CONTRACT = {
14
+ version: '1.1.0',
15
+ product: 'scene-capability-engine',
16
+ context_contract: {
17
+ required_fields: ['product', 'module', 'page'],
18
+ optional_fields: [
19
+ 'entity',
20
+ 'scene_id',
21
+ 'workflow_node',
22
+ 'fields',
23
+ 'current_state',
24
+ 'scene_workspace',
25
+ 'assistant_panel'
26
+ ],
27
+ max_field_count: 400,
28
+ max_payload_kb: 512
29
+ },
30
+ security_contract: {
31
+ mode: 'read-only',
32
+ masking_required: true,
33
+ sensitive_key_patterns: [
34
+ 'password',
35
+ 'secret',
36
+ 'token',
37
+ 'api_key',
38
+ 'apikey',
39
+ 'credential',
40
+ 'email',
41
+ 'phone',
42
+ 'bank',
43
+ 'card'
44
+ ],
45
+ forbidden_keys: [
46
+ 'raw_password',
47
+ 'private_key',
48
+ 'access_token_plaintext'
49
+ ]
50
+ },
51
+ runtime_contract: {
52
+ provider: 'ui-context-provider',
53
+ transport: 'json',
54
+ schema: 'docs/interactive-customization/page-context.schema.json',
55
+ consumer: 'scripts/interactive-intent-build.js'
56
+ }
57
+ };
58
+
59
+ function parseArgs(argv) {
60
+ const options = {
61
+ input: null,
62
+ provider: DEFAULT_PROVIDER,
63
+ outContext: DEFAULT_OUT_CONTEXT,
64
+ outReport: DEFAULT_OUT_REPORT,
65
+ contextContract: DEFAULT_CONTEXT_CONTRACT,
66
+ strictContract: true,
67
+ json: false
68
+ };
69
+
70
+ for (let index = 0; index < argv.length; index += 1) {
71
+ const token = argv[index];
72
+ const next = argv[index + 1];
73
+
74
+ if (token === '--input' && next) {
75
+ options.input = next;
76
+ index += 1;
77
+ } else if (token === '--provider' && next) {
78
+ options.provider = next;
79
+ index += 1;
80
+ } else if (token === '--out-context' && next) {
81
+ options.outContext = next;
82
+ index += 1;
83
+ } else if (token === '--out-report' && next) {
84
+ options.outReport = next;
85
+ index += 1;
86
+ } else if (token === '--context-contract' && next) {
87
+ options.contextContract = next;
88
+ index += 1;
89
+ } else if (token === '--no-strict-contract') {
90
+ options.strictContract = false;
91
+ } else if (token === '--json') {
92
+ options.json = true;
93
+ } else if (token === '--help' || token === '-h') {
94
+ printHelpAndExit(0);
95
+ }
96
+ }
97
+
98
+ options.input = `${options.input || ''}`.trim();
99
+ options.provider = `${options.provider || ''}`.trim().toLowerCase() || DEFAULT_PROVIDER;
100
+ options.outContext = `${options.outContext || ''}`.trim() || DEFAULT_OUT_CONTEXT;
101
+ options.outReport = `${options.outReport || ''}`.trim() || DEFAULT_OUT_REPORT;
102
+ options.contextContract = `${options.contextContract || ''}`.trim() || DEFAULT_CONTEXT_CONTRACT;
103
+
104
+ if (!options.input) {
105
+ throw new Error('--input is required.');
106
+ }
107
+ if (!SUPPORTED_PROVIDERS.has(options.provider)) {
108
+ throw new Error(`--provider must be one of: ${Array.from(SUPPORTED_PROVIDERS).join(', ')}`);
109
+ }
110
+
111
+ return options;
112
+ }
113
+
114
+ function printHelpAndExit(code) {
115
+ const lines = [
116
+ 'Usage: node scripts/interactive-context-bridge.js --input <path> [options]',
117
+ '',
118
+ 'Options:',
119
+ ' --input <path> Raw provider payload JSON path (required)',
120
+ ` --provider <name> Provider dialect (moqui|generic, default: ${DEFAULT_PROVIDER})`,
121
+ ` --out-context <path> Normalized page-context output (default: ${DEFAULT_OUT_CONTEXT})`,
122
+ ` --out-report <path> Bridge report output (default: ${DEFAULT_OUT_REPORT})`,
123
+ ` --context-contract <path> Context contract JSON (default: ${DEFAULT_CONTEXT_CONTRACT}, fallback built-in baseline when absent)`,
124
+ ' --no-strict-contract Keep exit code 0 even when contract validation fails',
125
+ ' --json Print bridge report as JSON',
126
+ ' -h, --help Show this help'
127
+ ];
128
+ console.log(lines.join('\n'));
129
+ process.exit(code);
130
+ }
131
+
132
+ function resolvePath(cwd, value) {
133
+ return path.isAbsolute(value) ? value : path.resolve(cwd, value);
134
+ }
135
+
136
+ function toStringArray(input) {
137
+ if (!Array.isArray(input)) {
138
+ return [];
139
+ }
140
+ return Array.from(
141
+ new Set(
142
+ input
143
+ .map(item => `${item || ''}`.trim())
144
+ .filter(Boolean)
145
+ )
146
+ );
147
+ }
148
+
149
+ function toNumberOrNull(value) {
150
+ const numeric = Number(value);
151
+ return Number.isFinite(numeric) ? numeric : null;
152
+ }
153
+
154
+ function firstNonEmpty(...values) {
155
+ for (const value of values) {
156
+ const candidate = `${value || ''}`.trim();
157
+ if (candidate) {
158
+ return candidate;
159
+ }
160
+ }
161
+ return null;
162
+ }
163
+
164
+ function normalizeNameToken(value) {
165
+ return `${value || ''}`.trim().toLowerCase();
166
+ }
167
+
168
+ function inferSensitiveFromName(name) {
169
+ return SENSITIVE_NAME_PATTERN.test(`${name || ''}`);
170
+ }
171
+
172
+ function normalizeFieldItem(item) {
173
+ if (typeof item === 'string') {
174
+ const name = `${item}`.trim();
175
+ if (!name) {
176
+ return null;
177
+ }
178
+ return {
179
+ name,
180
+ type: 'string',
181
+ sensitive: inferSensitiveFromName(name)
182
+ };
183
+ }
184
+ if (!item || typeof item !== 'object') {
185
+ return null;
186
+ }
187
+
188
+ const name = firstNonEmpty(item.name, item.key, item.id, item.field, item.field_name);
189
+ if (!name) {
190
+ return null;
191
+ }
192
+
193
+ const type = firstNonEmpty(item.type, item.data_type, item.datatype, item.kind, 'string');
194
+ const explicitSensitive = item.sensitive === true || item.is_sensitive === true;
195
+
196
+ return {
197
+ name,
198
+ type,
199
+ sensitive: explicitSensitive || inferSensitiveFromName(name),
200
+ description: firstNonEmpty(item.description, item.label, item.hint)
201
+ };
202
+ }
203
+
204
+ function normalizeFieldArray(input) {
205
+ const list = Array.isArray(input) ? input : [];
206
+ const seen = new Set();
207
+ const result = [];
208
+ for (const entry of list) {
209
+ const normalized = normalizeFieldItem(entry);
210
+ if (!normalized) {
211
+ continue;
212
+ }
213
+ const key = normalizeNameToken(normalized.name);
214
+ if (!key || seen.has(key)) {
215
+ continue;
216
+ }
217
+ seen.add(key);
218
+ result.push(normalized);
219
+ }
220
+ return result;
221
+ }
222
+
223
+ function normalizeListValues(input) {
224
+ if (!Array.isArray(input)) {
225
+ return [];
226
+ }
227
+ const values = [];
228
+ for (const item of input) {
229
+ if (typeof item === 'string') {
230
+ const value = `${item}`.trim();
231
+ if (value) {
232
+ values.push(value);
233
+ }
234
+ continue;
235
+ }
236
+ if (item && typeof item === 'object') {
237
+ const value = firstNonEmpty(item.name, item.id, item.code, item.key, item.label);
238
+ if (value) {
239
+ values.push(value);
240
+ }
241
+ }
242
+ }
243
+ return Array.from(new Set(values));
244
+ }
245
+
246
+ function normalizeAssistantPanel(raw = {}) {
247
+ const workspace = raw.workspace && typeof raw.workspace === 'object' ? raw.workspace : {};
248
+ const source = (
249
+ (raw.assistant_panel && typeof raw.assistant_panel === 'object' && raw.assistant_panel)
250
+ || (raw.assistant && typeof raw.assistant === 'object' && raw.assistant)
251
+ || (workspace.assistant_panel && typeof workspace.assistant_panel === 'object' && workspace.assistant_panel)
252
+ || {}
253
+ );
254
+
255
+ return {
256
+ session_id: firstNonEmpty(source.session_id, source.sessionId),
257
+ agent_id: firstNonEmpty(source.agent_id, source.agentId, source.agent, source.codename),
258
+ model: firstNonEmpty(source.model, source.model_id, source.modelId),
259
+ mode: firstNonEmpty(source.mode, source.permission_mode, source.permissionMode),
260
+ current_page_context: firstNonEmpty(
261
+ source.current_page_context,
262
+ source.currentPageContext,
263
+ source.prompt,
264
+ source.initial_prompt
265
+ )
266
+ };
267
+ }
268
+
269
+ function normalizeScreenExplorer(raw = {}) {
270
+ const workspace = raw.workspace && typeof raw.workspace === 'object' ? raw.workspace : {};
271
+ const sceneWorkspace = raw.scene_workspace && typeof raw.scene_workspace === 'object' ? raw.scene_workspace : {};
272
+ const source = (
273
+ (sceneWorkspace.screen_explorer && typeof sceneWorkspace.screen_explorer === 'object' && sceneWorkspace.screen_explorer)
274
+ || (workspace.screen_explorer && typeof workspace.screen_explorer === 'object' && workspace.screen_explorer)
275
+ || (raw.screen_explorer && typeof raw.screen_explorer === 'object' && raw.screen_explorer)
276
+ || {}
277
+ );
278
+
279
+ return {
280
+ active_tab: firstNonEmpty(source.active_tab, source.activeTab),
281
+ selected_screen: firstNonEmpty(source.selected_screen, source.selectedScreen),
282
+ selected_component: firstNonEmpty(source.selected_component, source.selectedComponent),
283
+ filters: toStringArray(source.filters),
284
+ result_total: toNumberOrNull(source.result_total != null ? source.result_total : source.resultTotal)
285
+ };
286
+ }
287
+
288
+ function normalizeOntology(raw = {}) {
289
+ const workspace = raw.workspace && typeof raw.workspace === 'object' ? raw.workspace : {};
290
+ const sceneWorkspace = raw.scene_workspace && typeof raw.scene_workspace === 'object' ? raw.scene_workspace : {};
291
+ const source = (
292
+ (sceneWorkspace.ontology && typeof sceneWorkspace.ontology === 'object' && sceneWorkspace.ontology)
293
+ || (workspace.ontology && typeof workspace.ontology === 'object' && workspace.ontology)
294
+ || (raw.ontology && typeof raw.ontology === 'object' && raw.ontology)
295
+ || {}
296
+ );
297
+
298
+ return {
299
+ entities: normalizeListValues(source.entities),
300
+ relations: normalizeListValues(source.relations),
301
+ business_rules: normalizeListValues(source.business_rules || source.businessRules),
302
+ decision_policies: normalizeListValues(source.decision_policies || source.decisionPolicies)
303
+ };
304
+ }
305
+
306
+ function pickCurrentState(raw = {}) {
307
+ const workspace = raw.workspace && typeof raw.workspace === 'object' ? raw.workspace : {};
308
+ return raw.current_state
309
+ || workspace.current_state
310
+ || raw.page_state
311
+ || raw.state
312
+ || {};
313
+ }
314
+
315
+ function buildNormalizedContext(raw = {}, provider = DEFAULT_PROVIDER) {
316
+ const workspace = raw.workspace && typeof raw.workspace === 'object' ? raw.workspace : {};
317
+ const scene = (
318
+ (workspace.scene && typeof workspace.scene === 'object' && workspace.scene)
319
+ || (raw.scene && typeof raw.scene === 'object' && raw.scene)
320
+ || {}
321
+ );
322
+ const explorer = normalizeScreenExplorer(raw);
323
+ const ontology = normalizeOntology(raw);
324
+ const assistantPanel = normalizeAssistantPanel(raw);
325
+
326
+ const candidateFields = [
327
+ raw.fields,
328
+ raw.page_fields,
329
+ workspace.fields,
330
+ workspace.field_catalog,
331
+ raw.field_catalog,
332
+ raw.scene_workspace && raw.scene_workspace.fields
333
+ ];
334
+
335
+ let fields = [];
336
+ for (const candidate of candidateFields) {
337
+ fields = normalizeFieldArray(candidate);
338
+ if (fields.length > 0) {
339
+ break;
340
+ }
341
+ }
342
+
343
+ if (provider === 'generic' && fields.length === 0) {
344
+ fields = normalizeFieldArray(raw.attributes);
345
+ }
346
+
347
+ return {
348
+ product: firstNonEmpty(raw.product, raw.app, workspace.product),
349
+ module: firstNonEmpty(raw.module, workspace.module, raw.domain),
350
+ page: firstNonEmpty(raw.page, workspace.page, raw.route, explorer.selected_screen),
351
+ entity: firstNonEmpty(raw.entity, workspace.entity, explorer.selected_component),
352
+ scene_id: firstNonEmpty(raw.scene_id, scene.id, scene.scene_id),
353
+ workflow_node: firstNonEmpty(raw.workflow_node, scene.workflow_node, workspace.workflow_node),
354
+ fields,
355
+ current_state: pickCurrentState(raw),
356
+ scene_workspace: {
357
+ scene_name: firstNonEmpty(scene.name, raw.scene_name, workspace.scene_name),
358
+ scene_type: firstNonEmpty(scene.type, raw.scene_type, workspace.scene_type),
359
+ screen_explorer: explorer,
360
+ ontology
361
+ },
362
+ assistant_panel: assistantPanel
363
+ };
364
+ }
365
+
366
+ function pruneObject(input) {
367
+ if (Array.isArray(input)) {
368
+ return input.map(item => pruneObject(item)).filter(item => item !== undefined);
369
+ }
370
+ if (!input || typeof input !== 'object') {
371
+ if (input === null || input === undefined || input === '') {
372
+ return undefined;
373
+ }
374
+ return input;
375
+ }
376
+
377
+ const output = {};
378
+ for (const [key, value] of Object.entries(input)) {
379
+ const normalized = pruneObject(value);
380
+ if (normalized === undefined) {
381
+ continue;
382
+ }
383
+ if (Array.isArray(normalized) && normalized.length === 0) {
384
+ continue;
385
+ }
386
+ if (normalized && typeof normalized === 'object' && !Array.isArray(normalized) && Object.keys(normalized).length === 0) {
387
+ continue;
388
+ }
389
+ output[key] = normalized;
390
+ }
391
+ return Object.keys(output).length === 0 ? undefined : output;
392
+ }
393
+
394
+ function normalizeContextContract(rawContract = {}) {
395
+ const contract = rawContract && typeof rawContract === 'object'
396
+ ? rawContract
397
+ : {};
398
+ const contextContract = contract.context_contract && typeof contract.context_contract === 'object'
399
+ ? contract.context_contract
400
+ : {};
401
+ const securityContract = contract.security_contract && typeof contract.security_contract === 'object'
402
+ ? contract.security_contract
403
+ : {};
404
+
405
+ return {
406
+ version: `${contract.version || BUILTIN_CONTEXT_CONTRACT.version}`,
407
+ context_contract: {
408
+ required_fields: toStringArray(
409
+ contextContract.required_fields || BUILTIN_CONTEXT_CONTRACT.context_contract.required_fields
410
+ ),
411
+ max_field_count: toNumberOrNull(
412
+ contextContract.max_field_count != null
413
+ ? contextContract.max_field_count
414
+ : BUILTIN_CONTEXT_CONTRACT.context_contract.max_field_count
415
+ ),
416
+ max_payload_kb: toNumberOrNull(
417
+ contextContract.max_payload_kb != null
418
+ ? contextContract.max_payload_kb
419
+ : BUILTIN_CONTEXT_CONTRACT.context_contract.max_payload_kb
420
+ )
421
+ },
422
+ security_contract: {
423
+ forbidden_keys: toStringArray(
424
+ securityContract.forbidden_keys || BUILTIN_CONTEXT_CONTRACT.security_contract.forbidden_keys
425
+ ).map(item => item.toLowerCase())
426
+ }
427
+ };
428
+ }
429
+
430
+ async function loadContextContract(contractPath) {
431
+ if (await fs.pathExists(contractPath)) {
432
+ const content = await fs.readFile(contractPath, 'utf8');
433
+ let parsed = {};
434
+ try {
435
+ parsed = JSON.parse(content);
436
+ } catch (error) {
437
+ throw new Error(`invalid JSON in context contract: ${error.message}`);
438
+ }
439
+ return {
440
+ source: contractPath,
441
+ from_file: true,
442
+ contract: normalizeContextContract(parsed)
443
+ };
444
+ }
445
+
446
+ return {
447
+ source: 'builtin-default',
448
+ from_file: false,
449
+ contract: normalizeContextContract(BUILTIN_CONTEXT_CONTRACT)
450
+ };
451
+ }
452
+
453
+ function hasContextFieldValue(value) {
454
+ if (value === null || value === undefined) {
455
+ return false;
456
+ }
457
+ if (typeof value === 'string') {
458
+ return value.trim().length > 0;
459
+ }
460
+ if (Array.isArray(value)) {
461
+ return value.length > 0;
462
+ }
463
+ return true;
464
+ }
465
+
466
+ function collectForbiddenKeyHits(input, forbiddenKeys, prefix = []) {
467
+ if (input === null || input === undefined || typeof input !== 'object') {
468
+ return [];
469
+ }
470
+
471
+ const hits = [];
472
+ for (const [key, value] of Object.entries(input)) {
473
+ const keyLower = `${key || ''}`.trim().toLowerCase();
474
+ const nextPrefix = [...prefix, key];
475
+ if (forbiddenKeys.includes(keyLower)) {
476
+ hits.push(nextPrefix.join('.'));
477
+ }
478
+ if (Array.isArray(value)) {
479
+ value.forEach((item, index) => {
480
+ hits.push(...collectForbiddenKeyHits(item, forbiddenKeys, [...nextPrefix, String(index)]));
481
+ });
482
+ } else if (value && typeof value === 'object') {
483
+ hits.push(...collectForbiddenKeyHits(value, forbiddenKeys, nextPrefix));
484
+ }
485
+ }
486
+ return hits;
487
+ }
488
+
489
+ function validateContextAgainstContract(context, contract) {
490
+ const requiredFields = toStringArray(contract.context_contract && contract.context_contract.required_fields);
491
+ const maxFieldCount = toNumberOrNull(contract.context_contract && contract.context_contract.max_field_count);
492
+ const maxPayloadKb = toNumberOrNull(contract.context_contract && contract.context_contract.max_payload_kb);
493
+ const forbiddenKeys = toStringArray(contract.security_contract && contract.security_contract.forbidden_keys).map(item => item.toLowerCase());
494
+ const fields = Array.isArray(context && context.fields) ? context.fields : [];
495
+ const payloadBytes = Buffer.byteLength(JSON.stringify(context || {}), 'utf8');
496
+ const payloadKb = Number((payloadBytes / 1024).toFixed(2));
497
+
498
+ const issues = [];
499
+ const missingRequired = requiredFields.filter(field => !hasContextFieldValue(context && context[field]));
500
+ if (missingRequired.length > 0) {
501
+ issues.push(`missing required fields: ${missingRequired.join(', ')}`);
502
+ }
503
+ if (maxFieldCount !== null && fields.length > maxFieldCount) {
504
+ issues.push(`fields count ${fields.length} exceeds max_field_count ${maxFieldCount}`);
505
+ }
506
+ if (maxPayloadKb !== null && payloadKb > maxPayloadKb) {
507
+ issues.push(`payload size ${payloadKb}KB exceeds max_payload_kb ${maxPayloadKb}KB`);
508
+ }
509
+
510
+ const forbiddenKeyHits = collectForbiddenKeyHits(context, forbiddenKeys);
511
+ if (forbiddenKeyHits.length > 0) {
512
+ issues.push(`forbidden keys present: ${forbiddenKeyHits.slice(0, 8).join(', ')}`);
513
+ }
514
+
515
+ return {
516
+ valid: issues.length === 0,
517
+ issues,
518
+ metrics: {
519
+ required_fields_total: requiredFields.length,
520
+ field_total: fields.length,
521
+ payload_kb: payloadKb,
522
+ max_field_count: maxFieldCount,
523
+ max_payload_kb: maxPayloadKb,
524
+ forbidden_key_hits: forbiddenKeyHits.length
525
+ }
526
+ };
527
+ }
528
+
529
+ async function main() {
530
+ const options = parseArgs(process.argv.slice(2));
531
+ const cwd = process.cwd();
532
+ const inputPath = resolvePath(cwd, options.input);
533
+ const outContextPath = resolvePath(cwd, options.outContext);
534
+ const outReportPath = resolvePath(cwd, options.outReport);
535
+ const contextContractPath = resolvePath(cwd, options.contextContract);
536
+
537
+ if (!(await fs.pathExists(inputPath))) {
538
+ throw new Error(`input not found: ${inputPath}`);
539
+ }
540
+
541
+ const rawText = await fs.readFile(inputPath, 'utf8');
542
+ let rawPayload;
543
+ try {
544
+ rawPayload = JSON.parse(rawText);
545
+ } catch (error) {
546
+ throw new Error(`invalid JSON in input payload: ${error.message}`);
547
+ }
548
+
549
+ const normalizedContext = pruneObject(buildNormalizedContext(rawPayload, options.provider)) || {};
550
+ const contractRuntime = await loadContextContract(contextContractPath);
551
+ const validation = validateContextAgainstContract(normalizedContext, contractRuntime.contract);
552
+
553
+ if (options.strictContract && !validation.valid) {
554
+ throw new Error(`context contract validation failed: ${validation.issues.join(' | ')}`);
555
+ }
556
+
557
+ const generatedAt = new Date().toISOString();
558
+ const report = {
559
+ mode: 'interactive-context-bridge',
560
+ generated_at: generatedAt,
561
+ provider: options.provider,
562
+ strict_contract: options.strictContract,
563
+ contract_source: contractRuntime.from_file
564
+ ? (path.relative(cwd, contractRuntime.source) || '.')
565
+ : contractRuntime.source,
566
+ validation,
567
+ summary: {
568
+ has_scene_workspace: Boolean(normalizedContext.scene_workspace),
569
+ has_assistant_panel: Boolean(normalizedContext.assistant_panel),
570
+ ontology_entity_total: Array.isArray(
571
+ normalizedContext.scene_workspace
572
+ && normalizedContext.scene_workspace.ontology
573
+ && normalizedContext.scene_workspace.ontology.entities
574
+ ) ? normalizedContext.scene_workspace.ontology.entities.length : 0
575
+ },
576
+ input: path.relative(cwd, inputPath) || '.',
577
+ output: {
578
+ context: path.relative(cwd, outContextPath) || '.',
579
+ report: path.relative(cwd, outReportPath) || '.'
580
+ }
581
+ };
582
+
583
+ await fs.ensureDir(path.dirname(outContextPath));
584
+ await fs.writeJson(outContextPath, normalizedContext, { spaces: 2 });
585
+ await fs.ensureDir(path.dirname(outReportPath));
586
+ await fs.writeJson(outReportPath, report, { spaces: 2 });
587
+
588
+ if (options.json) {
589
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
590
+ } else {
591
+ process.stdout.write('Interactive context bridge completed.\n');
592
+ process.stdout.write(`- Provider: ${report.provider}\n`);
593
+ process.stdout.write(`- Contract valid: ${validation.valid ? 'yes' : 'no'}\n`);
594
+ process.stdout.write(`- Context: ${report.output.context}\n`);
595
+ process.stdout.write(`- Report: ${report.output.report}\n`);
596
+ }
597
+ }
598
+
599
+ if (require.main === module) {
600
+ main().catch((error) => {
601
+ console.error(`Interactive context bridge failed: ${error.message}`);
602
+ process.exit(1);
603
+ });
604
+ }
605
+
606
+ module.exports = {
607
+ DEFAULT_PROVIDER,
608
+ DEFAULT_CONTEXT_CONTRACT,
609
+ DEFAULT_OUT_CONTEXT,
610
+ DEFAULT_OUT_REPORT,
611
+ SUPPORTED_PROVIDERS,
612
+ parseArgs,
613
+ resolvePath,
614
+ buildNormalizedContext,
615
+ validateContextAgainstContract,
616
+ main
617
+ };