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.
- package/CHANGELOG.md +25 -0
- package/bin/scene-capability-engine.js +36 -2
- package/docs/command-reference.md +5 -0
- package/docs/releases/README.md +2 -0
- package/docs/releases/v3.6.45.md +18 -0
- package/docs/releases/v3.6.46.md +23 -0
- package/docs/zh/releases/README.md +2 -0
- package/docs/zh/releases/v3.6.45.md +18 -0
- package/docs/zh/releases/v3.6.46.md +23 -0
- package/lib/workspace/collab-governance-audit.js +575 -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,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
|
+
});
|