@principal-ai/principal-view-cli 0.3.8 → 0.4.0
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/commands/narrative/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/commands/narrative/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgBpC,wBAAgB,qBAAqB,IAAI,OAAO,CA6N/C"}
|
|
@@ -2,14 +2,15 @@ import { Command } from 'commander';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { resolve, dirname } from 'node:path';
|
|
4
4
|
import { readFileSync } from 'node:fs';
|
|
5
|
-
import { NarrativeValidator } from '@principal-ai/principal-view-core';
|
|
6
|
-
import { loadNarrative, resolvePath } from './utils.js';
|
|
5
|
+
import { NarrativeValidator, computeAggregates } from '@principal-ai/principal-view-core';
|
|
6
|
+
import { loadNarrative, resolvePath, loadExecution, executionToEvents } from './utils.js';
|
|
7
7
|
export function createValidateCommand() {
|
|
8
8
|
const command = new Command('validate');
|
|
9
9
|
command
|
|
10
10
|
.description('Validate narrative template syntax, schema, and references')
|
|
11
11
|
.argument('<file>', 'Path to .narrative.json file')
|
|
12
12
|
.option('--canvas <path>', 'Override canvas file path for validation')
|
|
13
|
+
.option('--execution <path>', 'Execution file (.otel.json) for validating attribute references')
|
|
13
14
|
.option('--json', 'Output violations as JSON')
|
|
14
15
|
.option('-q, --quiet', 'Only show errors, suppress warnings')
|
|
15
16
|
.option('-d, --dir <path>', 'Project directory (default: cwd)')
|
|
@@ -40,6 +41,33 @@ export function createValidateCommand() {
|
|
|
40
41
|
canvas = undefined;
|
|
41
42
|
}
|
|
42
43
|
}
|
|
44
|
+
// Load execution data if provided
|
|
45
|
+
let executionData;
|
|
46
|
+
if (options.execution) {
|
|
47
|
+
try {
|
|
48
|
+
const executionPath = resolvePath(options.execution, baseDir);
|
|
49
|
+
const execution = await loadExecution(executionPath);
|
|
50
|
+
const events = executionToEvents(execution);
|
|
51
|
+
const aggregates = computeAggregates(events);
|
|
52
|
+
// Build event-specific attribute map
|
|
53
|
+
const eventAttributes = new Map();
|
|
54
|
+
for (const event of events) {
|
|
55
|
+
if (!eventAttributes.has(event.name)) {
|
|
56
|
+
eventAttributes.set(event.name, {});
|
|
57
|
+
}
|
|
58
|
+
const attrs = eventAttributes.get(event.name);
|
|
59
|
+
// Merge attributes from this event occurrence
|
|
60
|
+
if (event.attributes) {
|
|
61
|
+
Object.assign(attrs, event.attributes);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
executionData = { aggregates, eventAttributes };
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.error(chalk.yellow('Warning:'), `Failed to load execution file: ${error.message}`);
|
|
68
|
+
console.error(chalk.gray(' Attribute validation will be skipped'));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
43
71
|
// Create validator
|
|
44
72
|
const validator = new NarrativeValidator();
|
|
45
73
|
// Validate
|
|
@@ -49,6 +77,7 @@ export function createValidateCommand() {
|
|
|
49
77
|
canvasPath,
|
|
50
78
|
canvas,
|
|
51
79
|
basePath: baseDir,
|
|
80
|
+
executionData,
|
|
52
81
|
};
|
|
53
82
|
const result = await validator.validate(context);
|
|
54
83
|
// Filter violations if quiet mode
|
|
@@ -77,6 +106,7 @@ export function createValidateCommand() {
|
|
|
77
106
|
warnings: warnings.length,
|
|
78
107
|
scenarioCount: narrative.scenarios.length,
|
|
79
108
|
hasDefault: narrative.scenarios.some((s) => s.condition.default),
|
|
109
|
+
attributeValidation: executionData ? 'enabled' : 'skipped',
|
|
80
110
|
},
|
|
81
111
|
};
|
|
82
112
|
console.log(JSON.stringify(output, null, 2));
|
|
@@ -133,6 +163,12 @@ export function createValidateCommand() {
|
|
|
133
163
|
if (canvasPath) {
|
|
134
164
|
console.log(chalk.gray(` • Canvas: ${narrative.canvas || canvasPath}`));
|
|
135
165
|
}
|
|
166
|
+
if (executionData) {
|
|
167
|
+
console.log(chalk.gray(' • Attribute validation:'), chalk.green('enabled'));
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
console.log(chalk.gray(' • Attribute validation:'), chalk.gray('skipped (use --execution to enable)'));
|
|
171
|
+
}
|
|
136
172
|
console.log();
|
|
137
173
|
}
|
|
138
174
|
// Exit with error code if validation failed
|
package/dist/index.cjs
CHANGED
|
@@ -8971,9 +8971,9 @@ var require_out4 = __commonJS({
|
|
|
8971
8971
|
}
|
|
8972
8972
|
});
|
|
8973
8973
|
|
|
8974
|
-
// node_modules/
|
|
8974
|
+
// node_modules/ignore/index.js
|
|
8975
8975
|
var require_ignore = __commonJS({
|
|
8976
|
-
"node_modules/
|
|
8976
|
+
"node_modules/ignore/index.js"(exports2, module2) {
|
|
8977
8977
|
function makeArray(subject) {
|
|
8978
8978
|
return Array.isArray(subject) ? subject : [subject];
|
|
8979
8979
|
}
|
|
@@ -241233,6 +241233,21 @@ function computeAggregates(events) {
|
|
|
241233
241233
|
|
|
241234
241234
|
// node_modules/@principal-ai/principal-view-core/dist/narrative/template-parser.js
|
|
241235
241235
|
var import_handlebars = __toESM(require_lib());
|
|
241236
|
+
import_handlebars.default.registerHelper("eq", (a, b) => a === b);
|
|
241237
|
+
import_handlebars.default.registerHelper("ne", (a, b) => a !== b);
|
|
241238
|
+
import_handlebars.default.registerHelper("lt", (a, b) => a < b);
|
|
241239
|
+
import_handlebars.default.registerHelper("gt", (a, b) => a > b);
|
|
241240
|
+
import_handlebars.default.registerHelper("lte", (a, b) => a <= b);
|
|
241241
|
+
import_handlebars.default.registerHelper("gte", (a, b) => a >= b);
|
|
241242
|
+
import_handlebars.default.registerHelper("and", (...args) => {
|
|
241243
|
+
const values = args.slice(0, -1);
|
|
241244
|
+
return values.every(Boolean);
|
|
241245
|
+
});
|
|
241246
|
+
import_handlebars.default.registerHelper("or", (...args) => {
|
|
241247
|
+
const values = args.slice(0, -1);
|
|
241248
|
+
return values.some(Boolean);
|
|
241249
|
+
});
|
|
241250
|
+
import_handlebars.default.registerHelper("not", (a) => !a);
|
|
241236
241251
|
function parseTemplate(template, context) {
|
|
241237
241252
|
try {
|
|
241238
241253
|
const handlebarTemplate = import_handlebars.default.compile(template, { noEscape: true });
|
|
@@ -242112,12 +242127,137 @@ var NarrativeValidator = class {
|
|
|
242112
242127
|
return violations;
|
|
242113
242128
|
}
|
|
242114
242129
|
/**
|
|
242115
|
-
* Check attribute references
|
|
242130
|
+
* Check attribute references against execution data
|
|
242131
|
+
*
|
|
242132
|
+
* Validates that:
|
|
242133
|
+
* - Attributes referenced in templates exist in execution data
|
|
242134
|
+
* - Object attributes are accessed via properties (not used directly)
|
|
242135
|
+
* - Attribute names are correct (catches typos)
|
|
242116
242136
|
*/
|
|
242117
|
-
checkAttributeReferences(
|
|
242137
|
+
checkAttributeReferences(context) {
|
|
242118
242138
|
const violations = [];
|
|
242139
|
+
const { narrative, narrativePath, executionData } = context;
|
|
242140
|
+
if (!executionData) {
|
|
242141
|
+
return violations;
|
|
242142
|
+
}
|
|
242143
|
+
const { aggregates, eventAttributes } = executionData;
|
|
242144
|
+
for (const scenario of narrative.scenarios) {
|
|
242145
|
+
const scenarioPath = `scenarios[${scenario.id}]`;
|
|
242146
|
+
if (scenario.template.introduction) {
|
|
242147
|
+
const attrs = this.extractAttributeReferences(scenario.template.introduction);
|
|
242148
|
+
violations.push(...this.validateAttributes(
|
|
242149
|
+
attrs,
|
|
242150
|
+
aggregates,
|
|
242151
|
+
null,
|
|
242152
|
+
// introduction doesn't have specific event context
|
|
242153
|
+
narrativePath,
|
|
242154
|
+
`${scenarioPath}.template.introduction`
|
|
242155
|
+
));
|
|
242156
|
+
}
|
|
242157
|
+
if (scenario.template.events) {
|
|
242158
|
+
for (const [eventName, eventTemplate] of Object.entries(scenario.template.events)) {
|
|
242159
|
+
const attrs = this.extractAttributeReferences(eventTemplate);
|
|
242160
|
+
const eventAttrs = eventAttributes.get(eventName);
|
|
242161
|
+
violations.push(...this.validateAttributes(attrs, aggregates, eventAttrs || null, narrativePath, `${scenarioPath}.template.events.${eventName}`, eventName));
|
|
242162
|
+
}
|
|
242163
|
+
}
|
|
242164
|
+
if (scenario.template.summary) {
|
|
242165
|
+
const attrs = this.extractAttributeReferences(scenario.template.summary);
|
|
242166
|
+
violations.push(...this.validateAttributes(
|
|
242167
|
+
attrs,
|
|
242168
|
+
aggregates,
|
|
242169
|
+
null,
|
|
242170
|
+
// summary uses global aggregates
|
|
242171
|
+
narrativePath,
|
|
242172
|
+
`${scenarioPath}.template.summary`
|
|
242173
|
+
));
|
|
242174
|
+
}
|
|
242175
|
+
}
|
|
242119
242176
|
return violations;
|
|
242120
242177
|
}
|
|
242178
|
+
/**
|
|
242179
|
+
* Validate a list of attribute references against available data
|
|
242180
|
+
*
|
|
242181
|
+
* @param attributes - Attribute paths to validate
|
|
242182
|
+
* @param aggregates - Global aggregate attributes
|
|
242183
|
+
* @param eventAttributes - Event-specific attributes (if validating event template)
|
|
242184
|
+
* @param file - File path for violation reporting
|
|
242185
|
+
* @param path - JSON path for violation reporting
|
|
242186
|
+
* @param eventName - Event name (if validating event template)
|
|
242187
|
+
* @returns Array of violations found
|
|
242188
|
+
*/
|
|
242189
|
+
validateAttributes(attributes, aggregates, eventAttributes, file, path4, eventName) {
|
|
242190
|
+
const violations = [];
|
|
242191
|
+
for (const attr of attributes) {
|
|
242192
|
+
const globalValue = aggregates[attr];
|
|
242193
|
+
const eventValue = eventAttributes?.[attr];
|
|
242194
|
+
if (globalValue === void 0 && eventValue === void 0) {
|
|
242195
|
+
const allKeys = [
|
|
242196
|
+
...Object.keys(aggregates),
|
|
242197
|
+
...eventAttributes ? Object.keys(eventAttributes) : []
|
|
242198
|
+
];
|
|
242199
|
+
const similar = this.findSimilarAttributes(attr, allKeys);
|
|
242200
|
+
violations.push({
|
|
242201
|
+
ruleId: "narrative-attribute-undefined",
|
|
242202
|
+
severity: "warn",
|
|
242203
|
+
file,
|
|
242204
|
+
path: path4,
|
|
242205
|
+
message: eventName ? `Attribute "{{${attr}}}" not found in event "${eventName}" or global aggregates` : `Attribute "{{${attr}}}" not found in execution data`,
|
|
242206
|
+
impact: 'Template will render as empty or "undefined"',
|
|
242207
|
+
suggestion: similar.length > 0 ? `Did you mean: ${similar.join(", ")}?` : void 0,
|
|
242208
|
+
fixable: false
|
|
242209
|
+
});
|
|
242210
|
+
continue;
|
|
242211
|
+
}
|
|
242212
|
+
const value = eventValue !== void 0 ? eventValue : globalValue;
|
|
242213
|
+
if (this.isObjectType(value)) {
|
|
242214
|
+
const objectKeys = Object.keys(value);
|
|
242215
|
+
const suggestions = objectKeys.slice(0, 3).map((k) => `{{${attr}.${k}}}`);
|
|
242216
|
+
violations.push({
|
|
242217
|
+
ruleId: "narrative-attribute-object",
|
|
242218
|
+
severity: "warn",
|
|
242219
|
+
file,
|
|
242220
|
+
path: path4,
|
|
242221
|
+
message: `Attribute "{{${attr}}}" is an object and will render as "[object Object]"`,
|
|
242222
|
+
impact: 'Template will show "[object Object]" instead of useful data',
|
|
242223
|
+
suggestion: `Access a property instead: ${suggestions.join(", ")}`,
|
|
242224
|
+
fixable: false
|
|
242225
|
+
});
|
|
242226
|
+
}
|
|
242227
|
+
}
|
|
242228
|
+
return violations;
|
|
242229
|
+
}
|
|
242230
|
+
/**
|
|
242231
|
+
* Find similar attribute names for helpful suggestions
|
|
242232
|
+
*
|
|
242233
|
+
* Uses simple string similarity (Levenshtein-like) to find typos
|
|
242234
|
+
*
|
|
242235
|
+
* @param target - The attribute being searched for
|
|
242236
|
+
* @param available - Available attribute names
|
|
242237
|
+
* @returns Array of similar attribute names (max 3)
|
|
242238
|
+
*/
|
|
242239
|
+
findSimilarAttributes(target, available) {
|
|
242240
|
+
const similar = [];
|
|
242241
|
+
for (const attr of available) {
|
|
242242
|
+
if (attr.startsWith(target) || target.startsWith(attr)) {
|
|
242243
|
+
similar.push({ attr, score: 10 });
|
|
242244
|
+
continue;
|
|
242245
|
+
}
|
|
242246
|
+
if (attr.includes(target) || target.includes(attr)) {
|
|
242247
|
+
similar.push({ attr, score: 5 });
|
|
242248
|
+
continue;
|
|
242249
|
+
}
|
|
242250
|
+
const targetParts = target.split(".");
|
|
242251
|
+
const attrParts = attr.split(".");
|
|
242252
|
+
if (targetParts.length === attrParts.length) {
|
|
242253
|
+
const matchingParts = targetParts.filter((p, i) => p === attrParts[i]).length;
|
|
242254
|
+
if (matchingParts > 0) {
|
|
242255
|
+
similar.push({ attr, score: matchingParts });
|
|
242256
|
+
}
|
|
242257
|
+
}
|
|
242258
|
+
}
|
|
242259
|
+
return similar.sort((a, b) => b.score - a.score).slice(0, 3).map((s) => s.attr);
|
|
242260
|
+
}
|
|
242121
242261
|
/**
|
|
242122
242262
|
* Check formatting options
|
|
242123
242263
|
*/
|
|
@@ -242219,6 +242359,71 @@ var NarrativeValidator = class {
|
|
|
242219
242359
|
}
|
|
242220
242360
|
return match[1];
|
|
242221
242361
|
}
|
|
242362
|
+
/**
|
|
242363
|
+
* Extract attribute references from Handlebars template
|
|
242364
|
+
*
|
|
242365
|
+
* Parses template strings like:
|
|
242366
|
+
* - "{{source}}" -> ["source"]
|
|
242367
|
+
* - "{{source.url}}" -> ["source.url"]
|
|
242368
|
+
* - "{{#if options.global}}" -> ["options.global"]
|
|
242369
|
+
* - "{{#if (eq install.mode 'symlink')}}" -> ["install.mode"]
|
|
242370
|
+
*
|
|
242371
|
+
* @param template - Handlebars template string
|
|
242372
|
+
* @returns Array of attribute paths referenced in the template
|
|
242373
|
+
*/
|
|
242374
|
+
extractAttributeReferences(template) {
|
|
242375
|
+
const attributes = /* @__PURE__ */ new Set();
|
|
242376
|
+
const expressionPattern = /\{\{([^}]+)\}\}/g;
|
|
242377
|
+
let match;
|
|
242378
|
+
while ((match = expressionPattern.exec(template)) !== null) {
|
|
242379
|
+
const expression = match[1].trim();
|
|
242380
|
+
if (expression.startsWith("/")) {
|
|
242381
|
+
continue;
|
|
242382
|
+
}
|
|
242383
|
+
if (expression.startsWith("#")) {
|
|
242384
|
+
const helperMatch = expression.match(/^#\w+\s+(.+)$/);
|
|
242385
|
+
if (helperMatch) {
|
|
242386
|
+
this.extractAttributesFromExpression(helperMatch[1], attributes);
|
|
242387
|
+
}
|
|
242388
|
+
continue;
|
|
242389
|
+
}
|
|
242390
|
+
this.extractAttributesFromExpression(expression, attributes);
|
|
242391
|
+
}
|
|
242392
|
+
return Array.from(attributes);
|
|
242393
|
+
}
|
|
242394
|
+
/**
|
|
242395
|
+
* Extract attribute references from a single Handlebars expression
|
|
242396
|
+
*
|
|
242397
|
+
* Handles:
|
|
242398
|
+
* - Simple references: source.url
|
|
242399
|
+
* - Helper calls: (eq install.mode 'symlink')
|
|
242400
|
+
* - Nested expressions
|
|
242401
|
+
*
|
|
242402
|
+
* @param expression - The expression to parse
|
|
242403
|
+
* @param attributes - Set to add found attributes to
|
|
242404
|
+
*/
|
|
242405
|
+
extractAttributesFromExpression(expression, attributes) {
|
|
242406
|
+
const cleaned = expression.replace(/^\(|\)$/g, "").trim();
|
|
242407
|
+
const parts = cleaned.split(/\s+/);
|
|
242408
|
+
for (const part of parts) {
|
|
242409
|
+
if (part.match(/^(if|unless|each|with|eq|ne|lt|gt|lte|gte|and|or|not)$/) || part.match(/^['"].*['"]$/) || part.match(/^\d+$/) || part.match(/^(true|false|null|undefined)$/)) {
|
|
242410
|
+
continue;
|
|
242411
|
+
}
|
|
242412
|
+
const cleanPart = part.replace(/['"()]/g, "");
|
|
242413
|
+
if (cleanPart && cleanPart.match(/^[a-zA-Z_][a-zA-Z0-9_.]*$/)) {
|
|
242414
|
+
attributes.add(cleanPart);
|
|
242415
|
+
}
|
|
242416
|
+
}
|
|
242417
|
+
}
|
|
242418
|
+
/**
|
|
242419
|
+
* Check if an attribute is an object type
|
|
242420
|
+
*
|
|
242421
|
+
* @param value - The attribute value to check
|
|
242422
|
+
* @returns true if value is a plain object (not array, not null)
|
|
242423
|
+
*/
|
|
242424
|
+
isObjectType(value) {
|
|
242425
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
|
|
242426
|
+
}
|
|
242222
242427
|
};
|
|
242223
242428
|
function createNarrativeValidator() {
|
|
242224
242429
|
return new NarrativeValidator();
|
|
@@ -249024,7 +249229,7 @@ function capitalize(str3) {
|
|
|
249024
249229
|
// src/commands/narrative/validate.ts
|
|
249025
249230
|
function createValidateCommand2() {
|
|
249026
249231
|
const command = new Command("validate");
|
|
249027
|
-
command.description("Validate narrative template syntax, schema, and references").argument("<file>", "Path to .narrative.json file").option("--canvas <path>", "Override canvas file path for validation").option("--json", "Output violations as JSON").option("-q, --quiet", "Only show errors, suppress warnings").option("-d, --dir <path>", "Project directory (default: cwd)").action(async (file, options) => {
|
|
249232
|
+
command.description("Validate narrative template syntax, schema, and references").argument("<file>", "Path to .narrative.json file").option("--canvas <path>", "Override canvas file path for validation").option("--execution <path>", "Execution file (.otel.json) for validating attribute references").option("--json", "Output violations as JSON").option("-q, --quiet", "Only show errors, suppress warnings").option("-d, --dir <path>", "Project directory (default: cwd)").action(async (file, options) => {
|
|
249028
249233
|
try {
|
|
249029
249234
|
const baseDir = options.dir || process.cwd();
|
|
249030
249235
|
const narrativePath = resolvePath(file, baseDir);
|
|
@@ -249045,13 +249250,40 @@ function createValidateCommand2() {
|
|
|
249045
249250
|
canvas = void 0;
|
|
249046
249251
|
}
|
|
249047
249252
|
}
|
|
249253
|
+
let executionData;
|
|
249254
|
+
if (options.execution) {
|
|
249255
|
+
try {
|
|
249256
|
+
const executionPath = resolvePath(options.execution, baseDir);
|
|
249257
|
+
const execution = await loadExecution(executionPath);
|
|
249258
|
+
const events = executionToEvents(execution);
|
|
249259
|
+
const aggregates = computeAggregates(events);
|
|
249260
|
+
const eventAttributes = /* @__PURE__ */ new Map();
|
|
249261
|
+
for (const event of events) {
|
|
249262
|
+
if (!eventAttributes.has(event.name)) {
|
|
249263
|
+
eventAttributes.set(event.name, {});
|
|
249264
|
+
}
|
|
249265
|
+
const attrs = eventAttributes.get(event.name);
|
|
249266
|
+
if (event.attributes) {
|
|
249267
|
+
Object.assign(attrs, event.attributes);
|
|
249268
|
+
}
|
|
249269
|
+
}
|
|
249270
|
+
executionData = { aggregates, eventAttributes };
|
|
249271
|
+
} catch (error) {
|
|
249272
|
+
console.error(
|
|
249273
|
+
source_default.yellow("Warning:"),
|
|
249274
|
+
`Failed to load execution file: ${error.message}`
|
|
249275
|
+
);
|
|
249276
|
+
console.error(source_default.gray(" Attribute validation will be skipped"));
|
|
249277
|
+
}
|
|
249278
|
+
}
|
|
249048
249279
|
const validator = new NarrativeValidator();
|
|
249049
249280
|
const context = {
|
|
249050
249281
|
narrative,
|
|
249051
249282
|
narrativePath,
|
|
249052
249283
|
canvasPath,
|
|
249053
249284
|
canvas,
|
|
249054
|
-
basePath: baseDir
|
|
249285
|
+
basePath: baseDir,
|
|
249286
|
+
executionData
|
|
249055
249287
|
};
|
|
249056
249288
|
const result = await validator.validate(context);
|
|
249057
249289
|
const violations = options.quiet ? result.violations.filter((v) => v.severity === "error") : result.violations;
|
|
@@ -249075,7 +249307,8 @@ function createValidateCommand2() {
|
|
|
249075
249307
|
errors: errors.length,
|
|
249076
249308
|
warnings: warnings.length,
|
|
249077
249309
|
scenarioCount: narrative.scenarios.length,
|
|
249078
|
-
hasDefault: narrative.scenarios.some((s) => s.condition.default)
|
|
249310
|
+
hasDefault: narrative.scenarios.some((s) => s.condition.default),
|
|
249311
|
+
attributeValidation: executionData ? "enabled" : "skipped"
|
|
249079
249312
|
}
|
|
249080
249313
|
};
|
|
249081
249314
|
console.log(JSON.stringify(output, null, 2));
|
|
@@ -249140,6 +249373,17 @@ ${icon} ${severity}: ${violation.message}`);
|
|
|
249140
249373
|
if (canvasPath) {
|
|
249141
249374
|
console.log(source_default.gray(` \u2022 Canvas: ${narrative.canvas || canvasPath}`));
|
|
249142
249375
|
}
|
|
249376
|
+
if (executionData) {
|
|
249377
|
+
console.log(
|
|
249378
|
+
source_default.gray(" \u2022 Attribute validation:"),
|
|
249379
|
+
source_default.green("enabled")
|
|
249380
|
+
);
|
|
249381
|
+
} else {
|
|
249382
|
+
console.log(
|
|
249383
|
+
source_default.gray(" \u2022 Attribute validation:"),
|
|
249384
|
+
source_default.gray("skipped (use --execution to enable)")
|
|
249385
|
+
);
|
|
249386
|
+
}
|
|
249143
249387
|
console.log();
|
|
249144
249388
|
}
|
|
249145
249389
|
if (errors.length > 0) {
|