@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;AAepC,wBAAgB,qBAAqB,IAAI,OAAO,CA0K/C"}
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/globby/node_modules/ignore/index.js
8974
+ // node_modules/ignore/index.js
8975
8975
  var require_ignore = __commonJS({
8976
- "node_modules/globby/node_modules/ignore/index.js"(exports2, module2) {
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 (warning level)
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(_context) {
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) {