@principal-ai/principal-view-core 0.26.28 → 0.26.30
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/dist/discovery/CanvasDiscovery.d.ts.map +1 -1
- package/dist/discovery/CanvasDiscovery.js +11 -1
- package/dist/discovery/CanvasDiscovery.js.map +1 -1
- package/dist/discovery/types.d.ts +2 -1
- package/dist/discovery/types.d.ts.map +1 -1
- package/dist/events/EventsCanvasValidator.d.ts +123 -0
- package/dist/events/EventsCanvasValidator.d.ts.map +1 -0
- package/dist/events/EventsCanvasValidator.js +210 -0
- package/dist/events/EventsCanvasValidator.js.map +1 -0
- package/dist/events/ScopeEventsValidator.d.ts +65 -0
- package/dist/events/ScopeEventsValidator.d.ts.map +1 -0
- package/dist/events/ScopeEventsValidator.js +90 -0
- package/dist/events/ScopeEventsValidator.js.map +1 -0
- package/dist/events/index.d.ts +5 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +8 -0
- package/dist/events/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +2 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +5 -1
- package/dist/node.js.map +1 -1
- package/package.json +1 -1
- package/src/discovery/CanvasDiscovery.ts +12 -1
- package/src/discovery/types.ts +2 -1
- package/src/events/EventsCanvasValidator.ts +341 -0
- package/src/events/ScopeEventsValidator.ts +157 -0
- package/src/events/index.ts +14 -0
- package/src/index.ts +9 -0
- package/src/node.ts +11 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events Canvas Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates that a .events.canvas file properly documents event namespaces
|
|
5
|
+
* and their events, with correct namespace extraction and node structure.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtendedCanvas, ExtendedCanvasNode } from '../types/canvas';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Event namespace node structure
|
|
12
|
+
*/
|
|
13
|
+
export interface EventNamespaceNode {
|
|
14
|
+
id: string;
|
|
15
|
+
type: 'event-namespace';
|
|
16
|
+
namespace: {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
events: Array<{
|
|
20
|
+
name: string;
|
|
21
|
+
severity?: 'INFO' | 'WARN' | 'ERROR';
|
|
22
|
+
description?: string;
|
|
23
|
+
attributes?: Record<string, {
|
|
24
|
+
type: string;
|
|
25
|
+
required?: boolean;
|
|
26
|
+
description?: string;
|
|
27
|
+
}>;
|
|
28
|
+
}>;
|
|
29
|
+
};
|
|
30
|
+
// Standard canvas node fields
|
|
31
|
+
x: number;
|
|
32
|
+
y: number;
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
color?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Events canvas validation context
|
|
40
|
+
*/
|
|
41
|
+
export interface EventsCanvasValidationContext {
|
|
42
|
+
/** The events canvas (if found) */
|
|
43
|
+
eventsCanvas?: ExtendedCanvas;
|
|
44
|
+
|
|
45
|
+
/** Path to the events canvas file */
|
|
46
|
+
eventsCanvasPath?: string;
|
|
47
|
+
|
|
48
|
+
/** Base path for resolving relative paths */
|
|
49
|
+
basePath: string;
|
|
50
|
+
|
|
51
|
+
/** Optional: Workflow files to validate against */
|
|
52
|
+
workflowFiles?: Array<{
|
|
53
|
+
path: string;
|
|
54
|
+
events: string[];
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Events canvas validation violation
|
|
60
|
+
*/
|
|
61
|
+
export interface EventsCanvasViolation {
|
|
62
|
+
/** Rule ID that detected this violation */
|
|
63
|
+
ruleId: string;
|
|
64
|
+
|
|
65
|
+
/** Severity level */
|
|
66
|
+
severity: 'error' | 'warn';
|
|
67
|
+
|
|
68
|
+
/** File path where violation occurred */
|
|
69
|
+
file: string;
|
|
70
|
+
|
|
71
|
+
/** JSON path within file (optional) */
|
|
72
|
+
path?: string;
|
|
73
|
+
|
|
74
|
+
/** Human-readable description of what's wrong */
|
|
75
|
+
message: string;
|
|
76
|
+
|
|
77
|
+
/** Why this matters */
|
|
78
|
+
impact: string;
|
|
79
|
+
|
|
80
|
+
/** How to fix it */
|
|
81
|
+
suggestion: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Events canvas validation result
|
|
86
|
+
*/
|
|
87
|
+
export interface EventsCanvasValidationResult {
|
|
88
|
+
/** Whether validation passed (no errors) */
|
|
89
|
+
valid: boolean;
|
|
90
|
+
|
|
91
|
+
/** List of violations found */
|
|
92
|
+
violations: EventsCanvasViolation[];
|
|
93
|
+
|
|
94
|
+
/** Coverage metrics */
|
|
95
|
+
metrics: {
|
|
96
|
+
/** Total unique namespaces found */
|
|
97
|
+
totalNamespaces: number;
|
|
98
|
+
/** Namespaces with nodes */
|
|
99
|
+
documentedNamespaces: string[];
|
|
100
|
+
/** Namespaces missing nodes */
|
|
101
|
+
missingNamespaces: string[];
|
|
102
|
+
/** Total events across all namespaces */
|
|
103
|
+
totalEvents: number;
|
|
104
|
+
/** Events properly registered in namespace nodes */
|
|
105
|
+
registeredEvents: string[];
|
|
106
|
+
/** Events not in any namespace node */
|
|
107
|
+
unregisteredEvents: string[];
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Validates events canvas files
|
|
113
|
+
*/
|
|
114
|
+
export class EventsCanvasValidator {
|
|
115
|
+
/**
|
|
116
|
+
* Extract namespace from event name (all segments except last)
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* extractNamespace('validation.started') // 'validation'
|
|
120
|
+
* extractNamespace('file.read.complete') // 'file.read'
|
|
121
|
+
* extractNamespace('error') // null (invalid, needs at least 2 segments)
|
|
122
|
+
*/
|
|
123
|
+
private extractNamespace(eventName: string): string | null {
|
|
124
|
+
const segments = eventName.split('.');
|
|
125
|
+
if (segments.length < 2) {
|
|
126
|
+
return null; // Invalid event name
|
|
127
|
+
}
|
|
128
|
+
return segments.slice(0, -1).join('.');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check if a node is an event-namespace node
|
|
133
|
+
*/
|
|
134
|
+
private isEventNamespaceNode(node: any): node is EventNamespaceNode {
|
|
135
|
+
return node?.type === 'event-namespace' &&
|
|
136
|
+
node?.namespace?.name !== undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Extract all events from namespace nodes
|
|
141
|
+
*/
|
|
142
|
+
private extractEventsFromCanvas(
|
|
143
|
+
canvas: ExtendedCanvas
|
|
144
|
+
): Map<string, Set<string>> {
|
|
145
|
+
const namespaceEvents = new Map<string, Set<string>>();
|
|
146
|
+
|
|
147
|
+
for (const node of canvas.nodes || []) {
|
|
148
|
+
if (this.isEventNamespaceNode(node)) {
|
|
149
|
+
const namespaceNode = node as EventNamespaceNode;
|
|
150
|
+
const namespace = namespaceNode.namespace.name;
|
|
151
|
+
const events = new Set<string>();
|
|
152
|
+
|
|
153
|
+
for (const event of namespaceNode.namespace.events || []) {
|
|
154
|
+
events.add(event.name);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
namespaceEvents.set(namespace, events);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return namespaceEvents;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Build a map of event name → expected namespace
|
|
166
|
+
*/
|
|
167
|
+
private buildEventNamespaceMap(
|
|
168
|
+
namespaceEvents: Map<string, Set<string>>
|
|
169
|
+
): Map<string, string> {
|
|
170
|
+
const eventNamespaceMap = new Map<string, string>();
|
|
171
|
+
|
|
172
|
+
for (const [namespace, events] of namespaceEvents) {
|
|
173
|
+
for (const eventName of events) {
|
|
174
|
+
const extractedNamespace = this.extractNamespace(eventName);
|
|
175
|
+
eventNamespaceMap.set(eventName, extractedNamespace || '');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return eventNamespaceMap;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Validate an events canvas
|
|
184
|
+
*/
|
|
185
|
+
async validate(
|
|
186
|
+
context: EventsCanvasValidationContext
|
|
187
|
+
): Promise<EventsCanvasValidationResult> {
|
|
188
|
+
const violations: EventsCanvasViolation[] = [];
|
|
189
|
+
const { eventsCanvas, eventsCanvasPath, basePath } = context;
|
|
190
|
+
|
|
191
|
+
// Initialize metrics
|
|
192
|
+
const metrics = {
|
|
193
|
+
totalNamespaces: 0,
|
|
194
|
+
documentedNamespaces: [] as string[],
|
|
195
|
+
missingNamespaces: [] as string[],
|
|
196
|
+
totalEvents: 0,
|
|
197
|
+
registeredEvents: [] as string[],
|
|
198
|
+
unregisteredEvents: [] as string[],
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Check if canvas exists
|
|
202
|
+
if (!eventsCanvas) {
|
|
203
|
+
violations.push({
|
|
204
|
+
ruleId: 'events-canvas-required',
|
|
205
|
+
severity: 'error',
|
|
206
|
+
file: eventsCanvasPath || '.principal-views/cli.events.canvas',
|
|
207
|
+
message: 'Events canvas is required for documenting event namespaces',
|
|
208
|
+
impact: 'Cannot validate event structure or namespace organization',
|
|
209
|
+
suggestion: 'Create an events canvas with event-namespace nodes for each namespace',
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
valid: false,
|
|
214
|
+
violations,
|
|
215
|
+
metrics,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Extract namespace nodes and events
|
|
220
|
+
const namespaceEvents = this.extractEventsFromCanvas(eventsCanvas);
|
|
221
|
+
const namespaceNodes = new Set(namespaceEvents.keys());
|
|
222
|
+
metrics.documentedNamespaces = Array.from(namespaceNodes);
|
|
223
|
+
metrics.totalNamespaces = namespaceNodes.size;
|
|
224
|
+
|
|
225
|
+
// Collect all event names and their extracted namespaces
|
|
226
|
+
const allEvents = new Set<string>();
|
|
227
|
+
const eventToExtractedNamespace = new Map<string, string>();
|
|
228
|
+
const extractedNamespaces = new Set<string>();
|
|
229
|
+
|
|
230
|
+
for (const [namespace, events] of namespaceEvents) {
|
|
231
|
+
for (const eventName of events) {
|
|
232
|
+
allEvents.add(eventName);
|
|
233
|
+
metrics.totalEvents++;
|
|
234
|
+
|
|
235
|
+
// Extract namespace from event name
|
|
236
|
+
const extractedNamespace = this.extractNamespace(eventName);
|
|
237
|
+
|
|
238
|
+
if (!extractedNamespace) {
|
|
239
|
+
violations.push({
|
|
240
|
+
ruleId: 'events-invalid-event-name',
|
|
241
|
+
severity: 'error',
|
|
242
|
+
file: eventsCanvasPath || '.principal-views/cli.events.canvas',
|
|
243
|
+
path: `nodes[].namespace.events[name="${eventName}"]`,
|
|
244
|
+
message: `Event name "${eventName}" is invalid (must have at least 2 segments)`,
|
|
245
|
+
impact: 'Event name does not follow {namespace}.{action} convention',
|
|
246
|
+
suggestion: `Rename to follow pattern like "${namespace}.${eventName}"`,
|
|
247
|
+
});
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
eventToExtractedNamespace.set(eventName, extractedNamespace);
|
|
252
|
+
extractedNamespaces.add(extractedNamespace);
|
|
253
|
+
|
|
254
|
+
// Check namespace consistency: extracted namespace should match the node it's in
|
|
255
|
+
if (extractedNamespace !== namespace) {
|
|
256
|
+
violations.push({
|
|
257
|
+
ruleId: 'events-namespace-mismatch',
|
|
258
|
+
severity: 'error',
|
|
259
|
+
file: eventsCanvasPath || '.principal-views/cli.events.canvas',
|
|
260
|
+
path: `nodes[namespace.name="${namespace}"].namespace.events[name="${eventName}"]`,
|
|
261
|
+
message: `Event "${eventName}" is in wrong namespace node (expected: "${extractedNamespace}", actual: "${namespace}")`,
|
|
262
|
+
impact: 'Event is incorrectly organized, making namespace structure confusing',
|
|
263
|
+
suggestion: `Move event "${eventName}" to namespace node "${extractedNamespace}"`,
|
|
264
|
+
});
|
|
265
|
+
} else {
|
|
266
|
+
metrics.registeredEvents.push(eventName);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Check for missing namespace nodes
|
|
272
|
+
for (const namespace of extractedNamespaces) {
|
|
273
|
+
if (!namespaceNodes.has(namespace)) {
|
|
274
|
+
metrics.missingNamespaces.push(namespace);
|
|
275
|
+
violations.push({
|
|
276
|
+
ruleId: 'events-namespace-node-missing',
|
|
277
|
+
severity: 'error',
|
|
278
|
+
file: eventsCanvasPath || '.principal-views/cli.events.canvas',
|
|
279
|
+
message: `Namespace "${namespace}" is missing a node in the canvas`,
|
|
280
|
+
impact: 'Cannot visualize or document this namespace group',
|
|
281
|
+
suggestion: `Add a node with type: "event-namespace" and namespace.name: "${namespace}"`,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Validate namespace nodes have descriptions
|
|
287
|
+
for (const node of eventsCanvas.nodes || []) {
|
|
288
|
+
if (this.isEventNamespaceNode(node)) {
|
|
289
|
+
const namespaceNode = node as EventNamespaceNode;
|
|
290
|
+
if (!namespaceNode.namespace.description) {
|
|
291
|
+
violations.push({
|
|
292
|
+
ruleId: 'events-namespace-missing-description',
|
|
293
|
+
severity: 'warn',
|
|
294
|
+
file: eventsCanvasPath || '.principal-views/cli.events.canvas',
|
|
295
|
+
path: `nodes[id="${namespaceNode.id}"].namespace`,
|
|
296
|
+
message: `Namespace "${namespaceNode.namespace.name}" is missing a description`,
|
|
297
|
+
impact: 'Namespace purpose is undocumented',
|
|
298
|
+
suggestion: 'Add a description field explaining what events this namespace contains',
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Validate each event has description and severity
|
|
303
|
+
for (const event of namespaceNode.namespace.events || []) {
|
|
304
|
+
if (!event.description) {
|
|
305
|
+
violations.push({
|
|
306
|
+
ruleId: 'events-event-missing-description',
|
|
307
|
+
severity: 'warn',
|
|
308
|
+
file: eventsCanvasPath || '.principal-views/cli.events.canvas',
|
|
309
|
+
path: `nodes[id="${namespaceNode.id}"].namespace.events[name="${event.name}"]`,
|
|
310
|
+
message: `Event "${event.name}" is missing a description`,
|
|
311
|
+
impact: 'Event purpose is undocumented',
|
|
312
|
+
suggestion: 'Add a description field explaining when/why this event is emitted',
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!event.severity) {
|
|
317
|
+
violations.push({
|
|
318
|
+
ruleId: 'events-event-missing-severity',
|
|
319
|
+
severity: 'warn',
|
|
320
|
+
file: eventsCanvasPath || '.principal-views/cli.events.canvas',
|
|
321
|
+
path: `nodes[id="${namespaceNode.id}"].namespace.events[name="${event.name}"]`,
|
|
322
|
+
message: `Event "${event.name}" is missing a severity level`,
|
|
323
|
+
impact: 'Cannot determine event criticality',
|
|
324
|
+
suggestion: 'Add severity field with value: "INFO", "WARN", or "ERROR"',
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
metrics.totalNamespaces = extractedNamespaces.size;
|
|
332
|
+
|
|
333
|
+
const errors = violations.filter(v => v.severity === 'error');
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
valid: errors.length === 0,
|
|
337
|
+
violations,
|
|
338
|
+
metrics,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope Events Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates that each instrumentation scope documented in architecture.scopes.canvas
|
|
5
|
+
* has a corresponding {scope-name}.events.canvas file documenting its event vocabulary.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtendedCanvas, OtelScopeNode } from '../types/canvas';
|
|
9
|
+
import { existsSync } from 'fs';
|
|
10
|
+
import { resolve, join } from 'path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Scope events validation context
|
|
14
|
+
*/
|
|
15
|
+
export interface ScopeEventsValidationContext {
|
|
16
|
+
/** The scopes canvas (if found) */
|
|
17
|
+
scopesCanvas?: ExtendedCanvas;
|
|
18
|
+
|
|
19
|
+
/** Path to the scopes canvas file */
|
|
20
|
+
scopesCanvasPath?: string;
|
|
21
|
+
|
|
22
|
+
/** Base path for resolving relative paths */
|
|
23
|
+
basePath: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Scope events validation violation
|
|
28
|
+
*/
|
|
29
|
+
export interface ScopeEventsViolation {
|
|
30
|
+
/** Rule ID that detected this violation */
|
|
31
|
+
ruleId: string;
|
|
32
|
+
|
|
33
|
+
/** Severity level */
|
|
34
|
+
severity: 'error' | 'warn';
|
|
35
|
+
|
|
36
|
+
/** Scope name */
|
|
37
|
+
scope: string;
|
|
38
|
+
|
|
39
|
+
/** Expected file path */
|
|
40
|
+
expectedPath: string;
|
|
41
|
+
|
|
42
|
+
/** Human-readable description of what's wrong */
|
|
43
|
+
message: string;
|
|
44
|
+
|
|
45
|
+
/** Why this matters */
|
|
46
|
+
impact: string;
|
|
47
|
+
|
|
48
|
+
/** How to fix it */
|
|
49
|
+
suggestion: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Scope events validation result
|
|
54
|
+
*/
|
|
55
|
+
export interface ScopeEventsValidationResult {
|
|
56
|
+
/** Whether validation passed (no errors) */
|
|
57
|
+
valid: boolean;
|
|
58
|
+
|
|
59
|
+
/** List of violations found */
|
|
60
|
+
violations: ScopeEventsViolation[];
|
|
61
|
+
|
|
62
|
+
/** Summary of events canvas coverage */
|
|
63
|
+
coverage: {
|
|
64
|
+
/** Total owned scopes from library.yaml */
|
|
65
|
+
totalScopes: number;
|
|
66
|
+
/** Scopes with events canvas files */
|
|
67
|
+
scopesWithEvents: string[];
|
|
68
|
+
/** Scopes missing events canvas */
|
|
69
|
+
scopesMissingEvents: string[];
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Convert scope name to events canvas filename
|
|
75
|
+
* e.g., "backlog.md.cli" -> "backlog-md-cli.events.canvas"
|
|
76
|
+
*/
|
|
77
|
+
function scopeToEventsCanvasFilename(scope: string): string {
|
|
78
|
+
return `${scope.replace(/\./g, '-')}.events.canvas`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validates that scopes have corresponding events canvas files
|
|
83
|
+
*/
|
|
84
|
+
export class ScopeEventsValidator {
|
|
85
|
+
/**
|
|
86
|
+
* Validate that each scope documented in scopes canvas has an events canvas
|
|
87
|
+
*/
|
|
88
|
+
async validate(context: ScopeEventsValidationContext): Promise<ScopeEventsValidationResult> {
|
|
89
|
+
const violations: ScopeEventsViolation[] = [];
|
|
90
|
+
const { scopesCanvas, scopesCanvasPath, basePath } = context;
|
|
91
|
+
|
|
92
|
+
const scopesWithEvents: string[] = [];
|
|
93
|
+
const scopesMissingEvents: string[] = [];
|
|
94
|
+
|
|
95
|
+
// If no scopes canvas, nothing to validate
|
|
96
|
+
if (!scopesCanvas) {
|
|
97
|
+
return {
|
|
98
|
+
valid: true,
|
|
99
|
+
violations: [],
|
|
100
|
+
coverage: {
|
|
101
|
+
totalScopes: 0,
|
|
102
|
+
scopesWithEvents: [],
|
|
103
|
+
scopesMissingEvents: [],
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Extract scope nodes from the canvas
|
|
109
|
+
const scopeNodes = (scopesCanvas.nodes || []).filter(
|
|
110
|
+
(node): node is OtelScopeNode => node.type === 'otel-scope'
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Check each scope for corresponding events canvas
|
|
114
|
+
for (const scopeNode of scopeNodes) {
|
|
115
|
+
const scope = scopeNode.otel?.scope;
|
|
116
|
+
if (!scope) continue;
|
|
117
|
+
|
|
118
|
+
const eventsCanvasFilename = scopeToEventsCanvasFilename(scope);
|
|
119
|
+
const eventsCanvasPath = join(basePath, '.principal-views', eventsCanvasFilename);
|
|
120
|
+
const relativePath = `.principal-views/${eventsCanvasFilename}`;
|
|
121
|
+
|
|
122
|
+
if (existsSync(eventsCanvasPath)) {
|
|
123
|
+
scopesWithEvents.push(scope);
|
|
124
|
+
} else {
|
|
125
|
+
scopesMissingEvents.push(scope);
|
|
126
|
+
violations.push({
|
|
127
|
+
ruleId: 'scope-events-canvas-required',
|
|
128
|
+
severity: 'warn',
|
|
129
|
+
scope,
|
|
130
|
+
expectedPath: relativePath,
|
|
131
|
+
message: `Scope "${scope}" is missing an events canvas`,
|
|
132
|
+
impact: `Event vocabulary for scope "${scope}" is not documented. This makes it impossible to:
|
|
133
|
+
- Understand what events the scope emits
|
|
134
|
+
- Validate event flows match actual code
|
|
135
|
+
- Track event schema changes over time
|
|
136
|
+
- Generate documentation for implementers`,
|
|
137
|
+
suggestion: `Create ${relativePath} that documents:
|
|
138
|
+
- Event namespaces (groups of related events like "validation", "file", etc.)
|
|
139
|
+
- Events within each namespace with their attributes
|
|
140
|
+
- Adjacency relationships (which event namespaces connect in workflows)
|
|
141
|
+
|
|
142
|
+
See architecture.events.md for conventions.`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
valid: violations.filter(v => v.severity === 'error').length === 0,
|
|
149
|
+
violations,
|
|
150
|
+
coverage: {
|
|
151
|
+
totalScopes: scopeNodes.length,
|
|
152
|
+
scopesWithEvents,
|
|
153
|
+
scopesMissingEvents,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { EventsCanvasValidator } from './EventsCanvasValidator';
|
|
2
|
+
export type {
|
|
3
|
+
EventNamespaceNode,
|
|
4
|
+
EventsCanvasValidationContext,
|
|
5
|
+
EventsCanvasViolation,
|
|
6
|
+
EventsCanvasValidationResult,
|
|
7
|
+
} from './EventsCanvasValidator';
|
|
8
|
+
|
|
9
|
+
export { ScopeEventsValidator } from './ScopeEventsValidator';
|
|
10
|
+
export type {
|
|
11
|
+
ScopeEventsValidationContext,
|
|
12
|
+
ScopeEventsViolation,
|
|
13
|
+
ScopeEventsValidationResult,
|
|
14
|
+
} from './ScopeEventsValidator';
|
package/src/index.ts
CHANGED
|
@@ -357,6 +357,15 @@ export type {
|
|
|
357
357
|
NormalizedScope,
|
|
358
358
|
} from './scopes';
|
|
359
359
|
|
|
360
|
+
// Export events module (event namespace canvas validation)
|
|
361
|
+
export { EventsCanvasValidator } from './events';
|
|
362
|
+
export type {
|
|
363
|
+
EventNamespaceNode,
|
|
364
|
+
EventsCanvasValidationContext,
|
|
365
|
+
EventsCanvasViolation,
|
|
366
|
+
EventsCanvasValidationResult,
|
|
367
|
+
} from './events';
|
|
368
|
+
|
|
360
369
|
// Export spans module (span conventions + span color utilities)
|
|
361
370
|
export {
|
|
362
371
|
DEFAULT_SPAN_COLOR,
|
package/src/node.ts
CHANGED
|
@@ -219,3 +219,14 @@ export type {
|
|
|
219
219
|
ScopesCanvasViolation,
|
|
220
220
|
NormalizedScope,
|
|
221
221
|
} from './scopes';
|
|
222
|
+
|
|
223
|
+
// Export events module (canvas validation)
|
|
224
|
+
export { EventsCanvasValidator, ScopeEventsValidator } from './events';
|
|
225
|
+
export type {
|
|
226
|
+
EventsCanvasValidationContext,
|
|
227
|
+
EventsCanvasValidationResult,
|
|
228
|
+
EventsCanvasViolation,
|
|
229
|
+
ScopeEventsValidationContext,
|
|
230
|
+
ScopeEventsValidationResult,
|
|
231
|
+
ScopeEventsViolation,
|
|
232
|
+
} from './events';
|