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.
- package/CHANGELOG.md +11 -0
- package/docs/releases/README.md +1 -0
- package/docs/releases/v3.6.46.md +23 -0
- package/docs/zh/releases/README.md +1 -0
- package/docs/zh/releases/v3.6.46.md +23 -0
- package/package.json +4 -2
- package/scripts/auto-strategy-router.js +231 -0
- package/scripts/capability-mapping-report.js +339 -0
- package/scripts/check-branding-consistency.js +140 -0
- package/scripts/check-sce-tracking.js +54 -0
- package/scripts/check-skip-allowlist.js +94 -0
- package/scripts/errorbook-registry-health-gate.js +172 -0
- package/scripts/errorbook-release-gate.js +132 -0
- package/scripts/failure-attribution-repair.js +317 -0
- package/scripts/git-managed-gate.js +464 -0
- package/scripts/interactive-approval-event-projection.js +400 -0
- package/scripts/interactive-approval-workflow.js +829 -0
- package/scripts/interactive-authorization-tier-evaluate.js +413 -0
- package/scripts/interactive-change-plan-gate.js +225 -0
- package/scripts/interactive-context-bridge.js +617 -0
- package/scripts/interactive-customization-loop.js +1690 -0
- package/scripts/interactive-dialogue-governance.js +842 -0
- package/scripts/interactive-feedback-log.js +253 -0
- package/scripts/interactive-flow-smoke.js +238 -0
- package/scripts/interactive-flow.js +1059 -0
- package/scripts/interactive-governance-report.js +1112 -0
- package/scripts/interactive-intent-build.js +707 -0
- package/scripts/interactive-loop-smoke.js +215 -0
- package/scripts/interactive-moqui-adapter.js +304 -0
- package/scripts/interactive-plan-build.js +426 -0
- package/scripts/interactive-runtime-policy-evaluate.js +495 -0
- package/scripts/interactive-work-order-build.js +552 -0
- package/scripts/matrix-regression-gate.js +167 -0
- package/scripts/moqui-core-regression-suite.js +397 -0
- package/scripts/moqui-lexicon-audit.js +651 -0
- package/scripts/moqui-matrix-remediation-phased-runner.js +865 -0
- package/scripts/moqui-matrix-remediation-queue.js +852 -0
- package/scripts/moqui-metadata-extract.js +1340 -0
- package/scripts/moqui-rebuild-gate.js +167 -0
- package/scripts/moqui-release-summary.js +729 -0
- package/scripts/moqui-standard-rebuild.js +1370 -0
- package/scripts/moqui-template-baseline-report.js +682 -0
- package/scripts/npm-package-runtime-asset-check.js +221 -0
- package/scripts/problem-closure-gate.js +441 -0
- package/scripts/release-asset-integrity-check.js +216 -0
- package/scripts/release-asset-nonempty-normalize.js +166 -0
- package/scripts/release-drift-evaluate.js +223 -0
- package/scripts/release-drift-signals.js +255 -0
- package/scripts/release-governance-snapshot-export.js +132 -0
- package/scripts/release-ops-weekly-summary.js +934 -0
- package/scripts/release-risk-remediation-bundle.js +315 -0
- package/scripts/release-weekly-ops-gate.js +423 -0
- package/scripts/state-migration-reconciliation-gate.js +110 -0
- package/scripts/state-storage-tiering-audit.js +337 -0
- package/scripts/steering-content-audit.js +393 -0
- 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
|
+
};
|