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,651 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_MANIFEST = 'docs/handoffs/handoff-manifest.json';
|
|
8
|
+
const DEFAULT_TEMPLATE_DIR = '.sce/templates/scene-packages';
|
|
9
|
+
const DEFAULT_LEXICON = 'lib/data/moqui-capability-lexicon.json';
|
|
10
|
+
const DEFAULT_OUT = '.sce/reports/release-evidence/moqui-lexicon-audit.json';
|
|
11
|
+
const DEFAULT_MARKDOWN_OUT = '.sce/reports/release-evidence/moqui-lexicon-audit.md';
|
|
12
|
+
|
|
13
|
+
function parseArgs(argv) {
|
|
14
|
+
const options = {
|
|
15
|
+
manifest: DEFAULT_MANIFEST,
|
|
16
|
+
templateDir: DEFAULT_TEMPLATE_DIR,
|
|
17
|
+
lexicon: DEFAULT_LEXICON,
|
|
18
|
+
out: DEFAULT_OUT,
|
|
19
|
+
markdownOut: DEFAULT_MARKDOWN_OUT,
|
|
20
|
+
failOnGap: false,
|
|
21
|
+
json: false,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
25
|
+
const token = argv[i];
|
|
26
|
+
const next = argv[i + 1];
|
|
27
|
+
if (token === '--manifest' && next) {
|
|
28
|
+
options.manifest = next;
|
|
29
|
+
i += 1;
|
|
30
|
+
} else if (token === '--template-dir' && next) {
|
|
31
|
+
options.templateDir = next;
|
|
32
|
+
i += 1;
|
|
33
|
+
} else if (token === '--lexicon' && next) {
|
|
34
|
+
options.lexicon = next;
|
|
35
|
+
i += 1;
|
|
36
|
+
} else if (token === '--out' && next) {
|
|
37
|
+
options.out = next;
|
|
38
|
+
i += 1;
|
|
39
|
+
} else if (token === '--markdown-out' && next) {
|
|
40
|
+
options.markdownOut = next;
|
|
41
|
+
i += 1;
|
|
42
|
+
} else if (token === '--fail-on-gap') {
|
|
43
|
+
options.failOnGap = true;
|
|
44
|
+
} else if (token === '--json') {
|
|
45
|
+
options.json = true;
|
|
46
|
+
} else if (token === '--help' || token === '-h') {
|
|
47
|
+
printHelpAndExit(0);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return options;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function printHelpAndExit(code) {
|
|
55
|
+
const lines = [
|
|
56
|
+
'Usage: node scripts/moqui-lexicon-audit.js [options]',
|
|
57
|
+
'',
|
|
58
|
+
'Options:',
|
|
59
|
+
` --manifest <path> Handoff manifest JSON path (default: ${DEFAULT_MANIFEST})`,
|
|
60
|
+
` --template-dir <path> Scene package template root (default: ${DEFAULT_TEMPLATE_DIR})`,
|
|
61
|
+
` --lexicon <path> Moqui capability lexicon JSON path (default: ${DEFAULT_LEXICON})`,
|
|
62
|
+
` --out <path> JSON report path (default: ${DEFAULT_OUT})`,
|
|
63
|
+
` --markdown-out <path> Markdown report path (default: ${DEFAULT_MARKDOWN_OUT})`,
|
|
64
|
+
' --fail-on-gap Exit non-zero when lexicon unknowns or coverage gaps exist',
|
|
65
|
+
' --json Print JSON payload to stdout',
|
|
66
|
+
' -h, --help Show this help',
|
|
67
|
+
];
|
|
68
|
+
console.log(lines.join('\n'));
|
|
69
|
+
process.exit(code);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeCapabilityToken(value) {
|
|
73
|
+
if (value === undefined || value === null) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const normalized = `${value}`
|
|
77
|
+
.trim()
|
|
78
|
+
.toLowerCase()
|
|
79
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
80
|
+
.replace(/^-+|-+$/g, '');
|
|
81
|
+
return normalized.length > 0 ? normalized : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function pickCapabilityIdentifier(value) {
|
|
85
|
+
if (typeof value === 'string') {
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const candidates = [
|
|
92
|
+
value.capability,
|
|
93
|
+
value.capability_name,
|
|
94
|
+
value.name,
|
|
95
|
+
value.id,
|
|
96
|
+
];
|
|
97
|
+
for (const candidate of candidates) {
|
|
98
|
+
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
99
|
+
return candidate;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function collectManifestCapabilities(manifestPayload) {
|
|
106
|
+
const entries = Array.isArray(manifestPayload && manifestPayload.capabilities)
|
|
107
|
+
? manifestPayload.capabilities
|
|
108
|
+
: [];
|
|
109
|
+
const capabilities = [];
|
|
110
|
+
const seen = new Set();
|
|
111
|
+
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
const raw = pickCapabilityIdentifier(entry);
|
|
114
|
+
const normalized = normalizeCapabilityToken(raw);
|
|
115
|
+
if (!normalized || seen.has(normalized)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
seen.add(normalized);
|
|
119
|
+
capabilities.push(raw.trim());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return capabilities;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeTemplateIdentifier(value) {
|
|
126
|
+
if (value === undefined || value === null) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const normalized = `${value}`
|
|
130
|
+
.trim()
|
|
131
|
+
.toLowerCase()
|
|
132
|
+
.replace(/\\/g, '/');
|
|
133
|
+
if (!normalized) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const base = normalized.split('/').pop() || normalized;
|
|
137
|
+
return base.replace(/^[a-z0-9-]+\.scene--/, 'scene--');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function collectManifestTemplateIdentifiers(manifestPayload) {
|
|
141
|
+
const entries = Array.isArray(manifestPayload && manifestPayload.templates)
|
|
142
|
+
? manifestPayload.templates
|
|
143
|
+
: [];
|
|
144
|
+
const identifiers = [];
|
|
145
|
+
const seen = new Set();
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
const candidate = typeof entry === 'string'
|
|
148
|
+
? entry
|
|
149
|
+
: (
|
|
150
|
+
entry && typeof entry === 'object'
|
|
151
|
+
? (entry.id || entry.template_id || entry.template || entry.name)
|
|
152
|
+
: null
|
|
153
|
+
);
|
|
154
|
+
const normalized = normalizeTemplateIdentifier(candidate);
|
|
155
|
+
if (!normalized || seen.has(normalized)) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
seen.add(normalized);
|
|
159
|
+
identifiers.push(normalized);
|
|
160
|
+
}
|
|
161
|
+
return identifiers;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizeTemplateCapabilityCandidate(value) {
|
|
165
|
+
const normalizedTemplate = normalizeTemplateIdentifier(value);
|
|
166
|
+
if (!normalizedTemplate) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
let candidate = normalizedTemplate.replace(/^scene--/, '');
|
|
170
|
+
candidate = candidate.replace(
|
|
171
|
+
/--\d+\.\d+\.\d+(?:-[a-z0-9.-]+)?(?:\+[a-z0-9.-]+)?$/,
|
|
172
|
+
''
|
|
173
|
+
);
|
|
174
|
+
candidate = candidate.replace(/--\d{4}(?:-\d{2}){1,2}(?:-[a-z0-9-]+)?$/, '');
|
|
175
|
+
return normalizeCapabilityToken(candidate);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function inferExpectedCapabilitiesFromTemplates(templateIdentifiers = [], lexiconIndex) {
|
|
179
|
+
const capabilities = [];
|
|
180
|
+
const capabilitySet = new Set();
|
|
181
|
+
const inferredFrom = [];
|
|
182
|
+
const unresolvedTemplates = [];
|
|
183
|
+
const unresolvedSet = new Set();
|
|
184
|
+
|
|
185
|
+
for (const templateId of templateIdentifiers) {
|
|
186
|
+
const candidate = normalizeTemplateCapabilityCandidate(templateId);
|
|
187
|
+
if (!candidate) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const descriptor = resolveCapabilityDescriptor(candidate, lexiconIndex);
|
|
191
|
+
if (descriptor && descriptor.is_known) {
|
|
192
|
+
if (!capabilitySet.has(descriptor.canonical)) {
|
|
193
|
+
capabilitySet.add(descriptor.canonical);
|
|
194
|
+
capabilities.push(descriptor.canonical);
|
|
195
|
+
}
|
|
196
|
+
inferredFrom.push({
|
|
197
|
+
template: templateId,
|
|
198
|
+
normalized_template: candidate,
|
|
199
|
+
capability: descriptor.canonical
|
|
200
|
+
});
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (!unresolvedSet.has(templateId)) {
|
|
204
|
+
unresolvedSet.add(templateId);
|
|
205
|
+
unresolvedTemplates.push(templateId);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
capabilities,
|
|
211
|
+
inferred_from: inferredFrom,
|
|
212
|
+
unresolved_templates: unresolvedTemplates
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function collectTemplateProvidedCapabilities(templateRoot) {
|
|
217
|
+
const results = [];
|
|
218
|
+
if (!(await fs.pathExists(templateRoot))) {
|
|
219
|
+
return results;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const names = await fs.readdir(templateRoot);
|
|
223
|
+
for (const name of names) {
|
|
224
|
+
const dirPath = path.join(templateRoot, name);
|
|
225
|
+
const stat = await fs.stat(dirPath).catch(() => null);
|
|
226
|
+
if (!stat || !stat.isDirectory()) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const contractPath = path.join(dirPath, 'scene-package.json');
|
|
230
|
+
if (!(await fs.pathExists(contractPath))) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
let contract = null;
|
|
234
|
+
try {
|
|
235
|
+
contract = await fs.readJson(contractPath);
|
|
236
|
+
} catch (_error) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const provides = Array.isArray(
|
|
240
|
+
contract && contract.capabilities && contract.capabilities.provides
|
|
241
|
+
)
|
|
242
|
+
? contract.capabilities.provides
|
|
243
|
+
: [];
|
|
244
|
+
|
|
245
|
+
const provided = [];
|
|
246
|
+
for (const item of provides) {
|
|
247
|
+
if (typeof item === 'string' && item.trim().length > 0) {
|
|
248
|
+
provided.push(item.trim());
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
results.push({
|
|
253
|
+
template_id: name,
|
|
254
|
+
contract_path: path.relative(process.cwd(), contractPath),
|
|
255
|
+
provides: provided,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return results;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function buildLexiconIndex(rawLexicon = {}) {
|
|
263
|
+
const aliasToCanonical = new Map();
|
|
264
|
+
const deprecatedAliasToCanonical = new Map();
|
|
265
|
+
const canonicalSet = new Set();
|
|
266
|
+
const entries = Array.isArray(rawLexicon && rawLexicon.capabilities)
|
|
267
|
+
? rawLexicon.capabilities
|
|
268
|
+
: [];
|
|
269
|
+
|
|
270
|
+
for (const entry of entries) {
|
|
271
|
+
const canonical = normalizeCapabilityToken(entry && entry.canonical);
|
|
272
|
+
if (!canonical) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
canonicalSet.add(canonical);
|
|
276
|
+
aliasToCanonical.set(canonical, canonical);
|
|
277
|
+
|
|
278
|
+
const aliases = Array.isArray(entry && entry.aliases) ? entry.aliases : [];
|
|
279
|
+
for (const alias of aliases) {
|
|
280
|
+
const normalizedAlias = normalizeCapabilityToken(alias);
|
|
281
|
+
if (!normalizedAlias) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
aliasToCanonical.set(normalizedAlias, canonical);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const deprecatedAliases = Array.isArray(entry && entry.deprecated_aliases)
|
|
288
|
+
? entry.deprecated_aliases
|
|
289
|
+
: [];
|
|
290
|
+
for (const deprecatedAlias of deprecatedAliases) {
|
|
291
|
+
const normalizedAlias = normalizeCapabilityToken(deprecatedAlias);
|
|
292
|
+
if (!normalizedAlias) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
aliasToCanonical.set(normalizedAlias, canonical);
|
|
296
|
+
deprecatedAliasToCanonical.set(normalizedAlias, canonical);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
version: rawLexicon && rawLexicon.version ? `${rawLexicon.version}` : null,
|
|
302
|
+
source: rawLexicon && rawLexicon.source ? `${rawLexicon.source}` : null,
|
|
303
|
+
canonical_set: canonicalSet,
|
|
304
|
+
alias_to_canonical: aliasToCanonical,
|
|
305
|
+
deprecated_alias_to_canonical: deprecatedAliasToCanonical,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function resolveCapabilityDescriptor(value, lexiconIndex) {
|
|
310
|
+
const raw = `${value || ''}`.trim();
|
|
311
|
+
const normalized = normalizeCapabilityToken(raw);
|
|
312
|
+
if (!raw || !normalized) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const canonical = lexiconIndex.alias_to_canonical.get(normalized) || normalized;
|
|
317
|
+
const isDeprecatedAlias = lexiconIndex.deprecated_alias_to_canonical.has(normalized);
|
|
318
|
+
const isAlias = !isDeprecatedAlias && normalized !== canonical;
|
|
319
|
+
const isKnown = lexiconIndex.canonical_set.has(canonical);
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
raw,
|
|
323
|
+
normalized,
|
|
324
|
+
canonical,
|
|
325
|
+
is_known: isKnown,
|
|
326
|
+
is_alias: isAlias,
|
|
327
|
+
is_deprecated_alias: isDeprecatedAlias,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function collectUniqueDescriptors(values, lexiconIndex) {
|
|
332
|
+
const descriptors = [];
|
|
333
|
+
const seen = new Set();
|
|
334
|
+
for (const value of values) {
|
|
335
|
+
const descriptor = resolveCapabilityDescriptor(value, lexiconIndex);
|
|
336
|
+
if (!descriptor || seen.has(descriptor.normalized)) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
seen.add(descriptor.normalized);
|
|
340
|
+
descriptors.push(descriptor);
|
|
341
|
+
}
|
|
342
|
+
return descriptors;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function toRate(numerator, denominator) {
|
|
346
|
+
if (!Number.isFinite(Number(denominator)) || Number(denominator) <= 0) {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
return Number(((Number(numerator) / Number(denominator)) * 100).toFixed(2));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function buildMarkdownReport(report) {
|
|
353
|
+
const summary = report.summary && typeof report.summary === 'object' ? report.summary : {};
|
|
354
|
+
const coverage = report.coverage && typeof report.coverage === 'object' ? report.coverage : {};
|
|
355
|
+
const expectedScope = report.expected_scope && typeof report.expected_scope === 'object'
|
|
356
|
+
? report.expected_scope
|
|
357
|
+
: {};
|
|
358
|
+
const lines = [];
|
|
359
|
+
lines.push('# Moqui Lexicon Audit');
|
|
360
|
+
lines.push('');
|
|
361
|
+
lines.push(`- Generated at: ${report.generated_at}`);
|
|
362
|
+
lines.push(`- Manifest: ${report.manifest_path}`);
|
|
363
|
+
lines.push(`- Template root: ${report.template_root}`);
|
|
364
|
+
lines.push(`- Lexicon version: ${report.lexicon && report.lexicon.version ? report.lexicon.version : 'n/a'}`);
|
|
365
|
+
lines.push(`- Result: ${summary.passed ? 'pass' : 'fail'}`);
|
|
366
|
+
lines.push('');
|
|
367
|
+
lines.push('## Summary');
|
|
368
|
+
lines.push('');
|
|
369
|
+
lines.push(`- Expected capabilities: ${summary.expected_total}`);
|
|
370
|
+
lines.push(`- Expected source: ${expectedScope.source || 'none'}`);
|
|
371
|
+
lines.push(`- Expected inferred count: ${expectedScope.inferred_count || 0}`);
|
|
372
|
+
lines.push(`- Expected unresolved templates: ${expectedScope.unresolved_template_count || 0}`);
|
|
373
|
+
lines.push(`- Expected unknown: ${summary.expected_unknown_count}`);
|
|
374
|
+
lines.push(`- Provided capabilities: ${summary.provided_total}`);
|
|
375
|
+
lines.push(`- Provided unknown: ${summary.provided_unknown_count}`);
|
|
376
|
+
lines.push(`- Canonical coverage: ${coverage.coverage_percent === null ? 'n/a' : `${coverage.coverage_percent}%`}`);
|
|
377
|
+
lines.push(`- Uncovered expected: ${summary.uncovered_expected_count}`);
|
|
378
|
+
lines.push('');
|
|
379
|
+
|
|
380
|
+
if (Array.isArray(coverage.uncovered_expected) && coverage.uncovered_expected.length > 0) {
|
|
381
|
+
lines.push('## Uncovered Expected Capabilities');
|
|
382
|
+
lines.push('');
|
|
383
|
+
for (const item of coverage.uncovered_expected) {
|
|
384
|
+
lines.push(`- ${item}`);
|
|
385
|
+
}
|
|
386
|
+
lines.push('');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const expectedUnknown = report.normalization && report.normalization.expected
|
|
390
|
+
? report.normalization.expected.unknown
|
|
391
|
+
: [];
|
|
392
|
+
if (Array.isArray(expectedUnknown) && expectedUnknown.length > 0) {
|
|
393
|
+
lines.push('## Expected Unknown');
|
|
394
|
+
lines.push('');
|
|
395
|
+
for (const item of expectedUnknown) {
|
|
396
|
+
lines.push(`- ${item.raw}`);
|
|
397
|
+
}
|
|
398
|
+
lines.push('');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const providedUnknown = report.normalization && report.normalization.provided
|
|
402
|
+
? report.normalization.provided.unknown
|
|
403
|
+
: [];
|
|
404
|
+
if (Array.isArray(providedUnknown) && providedUnknown.length > 0) {
|
|
405
|
+
lines.push('## Provided Unknown');
|
|
406
|
+
lines.push('');
|
|
407
|
+
for (const item of providedUnknown) {
|
|
408
|
+
lines.push(`- ${item.raw}`);
|
|
409
|
+
}
|
|
410
|
+
lines.push('');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (
|
|
414
|
+
Array.isArray(expectedScope.unresolved_templates) &&
|
|
415
|
+
expectedScope.unresolved_templates.length > 0
|
|
416
|
+
) {
|
|
417
|
+
lines.push('## Expected Inference Unresolved Templates');
|
|
418
|
+
lines.push('');
|
|
419
|
+
for (const templateId of expectedScope.unresolved_templates) {
|
|
420
|
+
lines.push(`- ${templateId}`);
|
|
421
|
+
}
|
|
422
|
+
lines.push('');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
lines.push('## Recommendations');
|
|
426
|
+
lines.push('');
|
|
427
|
+
const recommendations = Array.isArray(report.recommendations) ? report.recommendations : [];
|
|
428
|
+
if (recommendations.length === 0) {
|
|
429
|
+
lines.push('- none');
|
|
430
|
+
} else {
|
|
431
|
+
for (const item of recommendations) {
|
|
432
|
+
lines.push(`- ${item}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return `${lines.join('\n')}\n`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function buildRecommendations(report) {
|
|
440
|
+
const recommendations = [];
|
|
441
|
+
const summary = report.summary && typeof report.summary === 'object' ? report.summary : {};
|
|
442
|
+
const coverage = report.coverage && typeof report.coverage === 'object' ? report.coverage : {};
|
|
443
|
+
const expectedScope = report.expected_scope && typeof report.expected_scope === 'object'
|
|
444
|
+
? report.expected_scope
|
|
445
|
+
: {};
|
|
446
|
+
|
|
447
|
+
if (summary.expected_unknown_count > 0) {
|
|
448
|
+
recommendations.push(
|
|
449
|
+
'Normalize manifest capabilities to canonical Moqui IDs and remove unknown capability names.'
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
if (summary.provided_unknown_count > 0) {
|
|
453
|
+
recommendations.push(
|
|
454
|
+
'Update scene-package capabilities.provides to canonical IDs or extend lexicon aliases/deprecations.'
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
if (summary.uncovered_expected_count > 0) {
|
|
458
|
+
recommendations.push(
|
|
459
|
+
`Add/align template capability coverage for: ${(coverage.uncovered_expected || []).join(', ')}.`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (
|
|
463
|
+
summary.expected_deprecated_alias_count > 0 ||
|
|
464
|
+
summary.provided_deprecated_alias_count > 0
|
|
465
|
+
) {
|
|
466
|
+
recommendations.push(
|
|
467
|
+
'Replace deprecated capability aliases with canonical capability IDs in manifests and templates.'
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
if (expectedScope.source === 'none') {
|
|
471
|
+
recommendations.push(
|
|
472
|
+
'Declare manifest capabilities or use lexicon-aligned scene template names so expected scope is not empty.'
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
if (
|
|
476
|
+
expectedScope.source === 'manifest.templates' &&
|
|
477
|
+
expectedScope.unresolved_template_count > 0
|
|
478
|
+
) {
|
|
479
|
+
recommendations.push(
|
|
480
|
+
'Template-based capability inference has unresolved template names; align template IDs with lexicon capability names or declare manifest capabilities explicitly.'
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return recommendations;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function main() {
|
|
488
|
+
const options = parseArgs(process.argv.slice(2));
|
|
489
|
+
const manifestPath = path.resolve(process.cwd(), options.manifest);
|
|
490
|
+
const templateRoot = path.resolve(process.cwd(), options.templateDir);
|
|
491
|
+
const lexiconPath = path.resolve(process.cwd(), options.lexicon);
|
|
492
|
+
const outPath = path.resolve(process.cwd(), options.out);
|
|
493
|
+
const markdownPath = path.resolve(process.cwd(), options.markdownOut);
|
|
494
|
+
|
|
495
|
+
if (!(await fs.pathExists(manifestPath))) {
|
|
496
|
+
throw new Error(`manifest not found: ${path.relative(process.cwd(), manifestPath)}`);
|
|
497
|
+
}
|
|
498
|
+
if (!(await fs.pathExists(lexiconPath))) {
|
|
499
|
+
throw new Error(`lexicon file not found: ${path.relative(process.cwd(), lexiconPath)}`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const manifestPayload = await fs.readJson(manifestPath);
|
|
503
|
+
const lexiconPayload = await fs.readJson(lexiconPath);
|
|
504
|
+
const lexiconIndex = buildLexiconIndex(lexiconPayload);
|
|
505
|
+
const manifestCapabilities = collectManifestCapabilities(manifestPayload);
|
|
506
|
+
const manifestTemplateIdentifiers = collectManifestTemplateIdentifiers(manifestPayload);
|
|
507
|
+
const capabilityInference = inferExpectedCapabilitiesFromTemplates(
|
|
508
|
+
manifestTemplateIdentifiers,
|
|
509
|
+
lexiconIndex
|
|
510
|
+
);
|
|
511
|
+
let expectedValues = manifestCapabilities;
|
|
512
|
+
let expectedSource = 'manifest.capabilities';
|
|
513
|
+
if (manifestCapabilities.length === 0) {
|
|
514
|
+
if (capabilityInference.capabilities.length > 0) {
|
|
515
|
+
expectedValues = capabilityInference.capabilities;
|
|
516
|
+
expectedSource = 'manifest.templates';
|
|
517
|
+
} else {
|
|
518
|
+
expectedSource = 'none';
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const templates = await collectTemplateProvidedCapabilities(templateRoot);
|
|
522
|
+
const manifestTemplateIdentifierSet = new Set(manifestTemplateIdentifiers);
|
|
523
|
+
const scopedTemplates = manifestTemplateIdentifierSet.size > 0
|
|
524
|
+
? templates.filter(item => manifestTemplateIdentifierSet.has(
|
|
525
|
+
normalizeTemplateIdentifier(item && item.template_id)
|
|
526
|
+
))
|
|
527
|
+
: templates;
|
|
528
|
+
const selectedTemplates = scopedTemplates.length > 0 ? scopedTemplates : templates;
|
|
529
|
+
const providedValues = selectedTemplates.flatMap(item => item.provides || []);
|
|
530
|
+
|
|
531
|
+
const expectedDescriptors = collectUniqueDescriptors(expectedValues, lexiconIndex);
|
|
532
|
+
const providedDescriptors = collectUniqueDescriptors(providedValues, lexiconIndex);
|
|
533
|
+
|
|
534
|
+
const expectedCanonical = Array.from(new Set(expectedDescriptors.map(item => item.canonical))).sort();
|
|
535
|
+
const providedCanonical = Array.from(new Set(providedDescriptors.map(item => item.canonical))).sort();
|
|
536
|
+
const providedCanonicalSet = new Set(providedCanonical);
|
|
537
|
+
const uncoveredExpected = expectedCanonical.filter(item => !providedCanonicalSet.has(item));
|
|
538
|
+
|
|
539
|
+
const expectedUnknown = expectedDescriptors.filter(item => !item.is_known);
|
|
540
|
+
const providedUnknown = providedDescriptors.filter(item => !item.is_known);
|
|
541
|
+
const expectedDeprecated = expectedDescriptors.filter(item => item.is_deprecated_alias);
|
|
542
|
+
const providedDeprecated = providedDescriptors.filter(item => item.is_deprecated_alias);
|
|
543
|
+
const expectedAliases = expectedDescriptors.filter(item => item.is_alias);
|
|
544
|
+
const providedAliases = providedDescriptors.filter(item => item.is_alias);
|
|
545
|
+
|
|
546
|
+
const coveragePercent = toRate(
|
|
547
|
+
expectedCanonical.length - uncoveredExpected.length,
|
|
548
|
+
expectedCanonical.length
|
|
549
|
+
);
|
|
550
|
+
const summary = {
|
|
551
|
+
expected_total: expectedDescriptors.length,
|
|
552
|
+
expected_source: expectedSource,
|
|
553
|
+
expected_known_count: expectedDescriptors.length - expectedUnknown.length,
|
|
554
|
+
expected_unknown_count: expectedUnknown.length,
|
|
555
|
+
expected_alias_count: expectedAliases.length,
|
|
556
|
+
expected_deprecated_alias_count: expectedDeprecated.length,
|
|
557
|
+
provided_total: providedDescriptors.length,
|
|
558
|
+
provided_known_count: providedDescriptors.length - providedUnknown.length,
|
|
559
|
+
provided_unknown_count: providedUnknown.length,
|
|
560
|
+
provided_alias_count: providedAliases.length,
|
|
561
|
+
provided_deprecated_alias_count: providedDeprecated.length,
|
|
562
|
+
uncovered_expected_count: uncoveredExpected.length,
|
|
563
|
+
coverage_percent: coveragePercent,
|
|
564
|
+
passed: expectedUnknown.length === 0 && providedUnknown.length === 0 && uncoveredExpected.length === 0,
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const report = {
|
|
568
|
+
mode: 'moqui-lexicon-audit',
|
|
569
|
+
generated_at: new Date().toISOString(),
|
|
570
|
+
manifest_path: path.relative(process.cwd(), manifestPath),
|
|
571
|
+
template_root: path.relative(process.cwd(), templateRoot),
|
|
572
|
+
template_scope: {
|
|
573
|
+
manifest_templates_total: manifestTemplateIdentifiers.length,
|
|
574
|
+
matched_templates_count: scopedTemplates.length,
|
|
575
|
+
using_manifest_scope: manifestTemplateIdentifierSet.size > 0 && scopedTemplates.length > 0,
|
|
576
|
+
},
|
|
577
|
+
expected_scope: {
|
|
578
|
+
source: expectedSource,
|
|
579
|
+
declared_count: manifestCapabilities.length,
|
|
580
|
+
effective_count: expectedValues.length,
|
|
581
|
+
inferred_count: capabilityInference.capabilities.length,
|
|
582
|
+
inferred_capabilities: capabilityInference.capabilities,
|
|
583
|
+
inferred_from_templates: capabilityInference.inferred_from,
|
|
584
|
+
unresolved_template_count: capabilityInference.unresolved_templates.length,
|
|
585
|
+
unresolved_templates: capabilityInference.unresolved_templates
|
|
586
|
+
},
|
|
587
|
+
lexicon: {
|
|
588
|
+
file: path.relative(process.cwd(), lexiconPath),
|
|
589
|
+
version: lexiconIndex.version,
|
|
590
|
+
source: lexiconIndex.source,
|
|
591
|
+
canonical_count: lexiconIndex.canonical_set.size,
|
|
592
|
+
},
|
|
593
|
+
summary,
|
|
594
|
+
normalization: {
|
|
595
|
+
expected: {
|
|
596
|
+
aliases: expectedAliases,
|
|
597
|
+
deprecated_aliases: expectedDeprecated,
|
|
598
|
+
unknown: expectedUnknown,
|
|
599
|
+
},
|
|
600
|
+
provided: {
|
|
601
|
+
aliases: providedAliases,
|
|
602
|
+
deprecated_aliases: providedDeprecated,
|
|
603
|
+
unknown: providedUnknown,
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
coverage: {
|
|
607
|
+
expected_canonical: expectedCanonical,
|
|
608
|
+
provided_canonical: providedCanonical,
|
|
609
|
+
uncovered_expected: uncoveredExpected,
|
|
610
|
+
coverage_percent: coveragePercent,
|
|
611
|
+
},
|
|
612
|
+
templates: selectedTemplates,
|
|
613
|
+
recommendations: [],
|
|
614
|
+
};
|
|
615
|
+
report.recommendations = buildRecommendations(report);
|
|
616
|
+
|
|
617
|
+
await fs.ensureDir(path.dirname(outPath));
|
|
618
|
+
await fs.writeJson(outPath, report, { spaces: 2 });
|
|
619
|
+
await fs.ensureDir(path.dirname(markdownPath));
|
|
620
|
+
await fs.writeFile(markdownPath, buildMarkdownReport(report), 'utf8');
|
|
621
|
+
|
|
622
|
+
if (options.json) {
|
|
623
|
+
console.log(JSON.stringify({
|
|
624
|
+
...report,
|
|
625
|
+
output: {
|
|
626
|
+
json: path.relative(process.cwd(), outPath),
|
|
627
|
+
markdown: path.relative(process.cwd(), markdownPath),
|
|
628
|
+
},
|
|
629
|
+
}, null, 2));
|
|
630
|
+
} else {
|
|
631
|
+
console.log('Moqui lexicon audit generated.');
|
|
632
|
+
console.log(` JSON: ${path.relative(process.cwd(), outPath)}`);
|
|
633
|
+
console.log(` Markdown: ${path.relative(process.cwd(), markdownPath)}`);
|
|
634
|
+
console.log(` Result: ${summary.passed ? 'pass' : 'fail'}`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (options.failOnGap && !summary.passed) {
|
|
638
|
+
console.error(
|
|
639
|
+
'Moqui lexicon audit failed: ' +
|
|
640
|
+
`expected_unknown=${summary.expected_unknown_count}, ` +
|
|
641
|
+
`provided_unknown=${summary.provided_unknown_count}, ` +
|
|
642
|
+
`uncovered_expected=${summary.uncovered_expected_count}`
|
|
643
|
+
);
|
|
644
|
+
process.exitCode = 2;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
main().catch((error) => {
|
|
649
|
+
console.error(`Failed to run moqui lexicon audit: ${error.message}`);
|
|
650
|
+
process.exitCode = 1;
|
|
651
|
+
});
|