@principal-ai/principal-view-core 0.28.2 → 0.28.4

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.
Files changed (65) hide show
  1. package/dist/auxiliary/AuxiliaryManifestValidator.d.ts +38 -0
  2. package/dist/auxiliary/AuxiliaryManifestValidator.d.ts.map +1 -0
  3. package/dist/auxiliary/AuxiliaryManifestValidator.js +97 -0
  4. package/dist/auxiliary/AuxiliaryManifestValidator.js.map +1 -0
  5. package/dist/auxiliary/index.d.ts +9 -0
  6. package/dist/auxiliary/index.d.ts.map +1 -0
  7. package/dist/auxiliary/index.js +14 -0
  8. package/dist/auxiliary/index.js.map +1 -0
  9. package/dist/auxiliary/validateAreaScopeDisjoint.d.ts +24 -0
  10. package/dist/auxiliary/validateAreaScopeDisjoint.d.ts.map +1 -0
  11. package/dist/auxiliary/validateAreaScopeDisjoint.js +61 -0
  12. package/dist/auxiliary/validateAreaScopeDisjoint.js.map +1 -0
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +4 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/node.d.ts +4 -2
  18. package/dist/node.d.ts.map +1 -1
  19. package/dist/node.js +8 -2
  20. package/dist/node.js.map +1 -1
  21. package/dist/scopes/ScopePathIndex.d.ts +56 -0
  22. package/dist/scopes/ScopePathIndex.d.ts.map +1 -0
  23. package/dist/scopes/ScopePathIndex.js +67 -0
  24. package/dist/scopes/ScopePathIndex.js.map +1 -0
  25. package/dist/scopes/ScopesCanvasValidator.d.ts.map +1 -1
  26. package/dist/scopes/ScopesCanvasValidator.js +64 -0
  27. package/dist/scopes/ScopesCanvasValidator.js.map +1 -1
  28. package/dist/scopes/index.d.ts +2 -0
  29. package/dist/scopes/index.d.ts.map +1 -1
  30. package/dist/scopes/index.js +5 -1
  31. package/dist/scopes/index.js.map +1 -1
  32. package/dist/scopes/validateScopeNamespaceNesting.d.ts +38 -0
  33. package/dist/scopes/validateScopeNamespaceNesting.d.ts.map +1 -0
  34. package/dist/scopes/validateScopeNamespaceNesting.js +69 -0
  35. package/dist/scopes/validateScopeNamespaceNesting.js.map +1 -0
  36. package/dist/types/auxiliary.d.ts +54 -0
  37. package/dist/types/auxiliary.d.ts.map +1 -0
  38. package/dist/types/auxiliary.js +27 -0
  39. package/dist/types/auxiliary.js.map +1 -0
  40. package/dist/types/canvas.d.ts +15 -0
  41. package/dist/types/canvas.d.ts.map +1 -1
  42. package/dist/types/canvas.js.map +1 -1
  43. package/dist/types/index.d.ts +1 -0
  44. package/dist/types/index.d.ts.map +1 -1
  45. package/dist/types/index.js +1 -0
  46. package/dist/types/index.js.map +1 -1
  47. package/package.json +2 -1
  48. package/schemas/auxiliary.manifest.schema.json +48 -0
  49. package/src/auxiliary/AuxiliaryManifestValidator.test.ts +92 -0
  50. package/src/auxiliary/AuxiliaryManifestValidator.ts +129 -0
  51. package/src/auxiliary/index.ts +18 -0
  52. package/src/auxiliary/validateAreaScopeDisjoint.test.ts +85 -0
  53. package/src/auxiliary/validateAreaScopeDisjoint.ts +72 -0
  54. package/src/index.ts +6 -0
  55. package/src/node.ts +18 -0
  56. package/src/scopes/ScopePathIndex.test.ts +94 -0
  57. package/src/scopes/ScopePathIndex.ts +89 -0
  58. package/src/scopes/ScopesCanvasValidator.test.ts +126 -0
  59. package/src/scopes/ScopesCanvasValidator.ts +68 -0
  60. package/src/scopes/index.ts +12 -0
  61. package/src/scopes/validateScopeNamespaceNesting.test.ts +127 -0
  62. package/src/scopes/validateScopeNamespaceNesting.ts +88 -0
  63. package/src/types/auxiliary.ts +63 -0
  64. package/src/types/canvas.ts +15 -0
  65. package/src/types/index.ts +1 -0
@@ -0,0 +1,126 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+ import { ScopesCanvasValidator } from './ScopesCanvasValidator';
6
+ import type { ExtendedCanvas } from '../types/canvas';
7
+
8
+ function makeScopeNode(opts: {
9
+ id: string;
10
+ scope: string;
11
+ description?: string;
12
+ paths?: string[];
13
+ }): any {
14
+ return {
15
+ id: opts.id,
16
+ type: 'otel-scope',
17
+ description: opts.description ?? `${opts.scope} scope`,
18
+ ...(opts.paths !== undefined ? { paths: opts.paths } : {}),
19
+ otel: { scope: opts.scope },
20
+ x: 0,
21
+ y: 0,
22
+ width: 100,
23
+ height: 50,
24
+ };
25
+ }
26
+
27
+ function makeCanvas(nodes: any[]): ExtendedCanvas {
28
+ return { nodes, edges: [] } as ExtendedCanvas;
29
+ }
30
+
31
+ function withTempRepo(layout: string[], fn: (root: string) => Promise<void>): Promise<void> {
32
+ const root = mkdtempSync(join(tmpdir(), 'scope-paths-'));
33
+ try {
34
+ for (const rel of layout) {
35
+ const full = join(root, rel);
36
+ mkdirSync(full, { recursive: true });
37
+ writeFileSync(join(full, '.keep'), '');
38
+ }
39
+ return fn(root).finally(() => rmSync(root, { recursive: true, force: true }));
40
+ } catch (e) {
41
+ rmSync(root, { recursive: true, force: true });
42
+ throw e;
43
+ }
44
+ }
45
+
46
+ describe('ScopesCanvasValidator scope paths', () => {
47
+ test('no paths declared → no path violations', async () => {
48
+ const canvas = makeCanvas([makeScopeNode({ id: 's1', scope: 'a.b' })]);
49
+ const v = new ScopesCanvasValidator();
50
+ const r = await v.validate({
51
+ scopesCanvas: canvas,
52
+ ownedScopes: ['a.b'],
53
+ basePath: '/tmp',
54
+ });
55
+ expect(r.violations.find(x => x.ruleId.startsWith('scopes-paths'))).toBeUndefined();
56
+ });
57
+
58
+ test('scopes-paths-multiple warns when more than one path', async () => {
59
+ await withTempRepo(['packages/a/src', 'packages/a/generated'], async (root) => {
60
+ const canvas = makeCanvas([
61
+ makeScopeNode({
62
+ id: 's1',
63
+ scope: 'a',
64
+ paths: ['packages/a/src', 'packages/a/generated'],
65
+ }),
66
+ ]);
67
+ const v = new ScopesCanvasValidator();
68
+ const r = await v.validate({ scopesCanvas: canvas, ownedScopes: ['a'], basePath: root });
69
+ const w = r.violations.find(x => x.ruleId === 'scopes-paths-multiple');
70
+ expect(w).toBeDefined();
71
+ expect(w?.severity).toBe('warn');
72
+ });
73
+ });
74
+
75
+ test('scopes-paths-missing warns when path does not exist', async () => {
76
+ await withTempRepo(['packages/a/src'], async (root) => {
77
+ const canvas = makeCanvas([
78
+ makeScopeNode({ id: 's1', scope: 'a', paths: ['packages/a/missing'] }),
79
+ ]);
80
+ const v = new ScopesCanvasValidator();
81
+ const r = await v.validate({ scopesCanvas: canvas, ownedScopes: ['a'], basePath: root });
82
+ const w = r.violations.find(x => x.ruleId === 'scopes-paths-missing');
83
+ expect(w).toBeDefined();
84
+ expect(w?.severity).toBe('warn');
85
+ });
86
+ });
87
+
88
+ test('scopes-paths-overlap errors on identical paths across scopes', async () => {
89
+ await withTempRepo(['packages/shared/src'], async (root) => {
90
+ const canvas = makeCanvas([
91
+ makeScopeNode({ id: 's1', scope: 'a', paths: ['packages/shared/src'] }),
92
+ makeScopeNode({ id: 's2', scope: 'b', paths: ['packages/shared/src'] }),
93
+ ]);
94
+ const v = new ScopesCanvasValidator();
95
+ const r = await v.validate({
96
+ scopesCanvas: canvas,
97
+ ownedScopes: ['a', 'b'],
98
+ basePath: root,
99
+ });
100
+ const e = r.violations.find(x => x.ruleId === 'scopes-paths-overlap');
101
+ expect(e).toBeDefined();
102
+ expect(e?.severity).toBe('error');
103
+ expect(r.valid).toBe(false);
104
+ });
105
+ });
106
+
107
+ test('parent-child path nesting is allowed (no overlap error)', async () => {
108
+ await withTempRepo(['packages/core/src/validation'], async (root) => {
109
+ const canvas = makeCanvas([
110
+ makeScopeNode({ id: 's1', scope: 'principal-ai.core', paths: ['packages/core/src'] }),
111
+ makeScopeNode({
112
+ id: 's2',
113
+ scope: 'principal-ai.core.validation',
114
+ paths: ['packages/core/src/validation'],
115
+ }),
116
+ ]);
117
+ const v = new ScopesCanvasValidator();
118
+ const r = await v.validate({
119
+ scopesCanvas: canvas,
120
+ ownedScopes: ['principal-ai.core', 'principal-ai.core.validation'],
121
+ basePath: root,
122
+ });
123
+ expect(r.violations.find(x => x.ruleId === 'scopes-paths-overlap')).toBeUndefined();
124
+ });
125
+ });
126
+ });
@@ -5,8 +5,11 @@
5
5
  * all instrumentation scopes declared in library.yaml.
6
6
  */
7
7
 
8
+ import { existsSync } from 'fs';
9
+ import { resolve } from 'path';
8
10
  import type { ExtendedCanvas, ExtendedCanvasNode, OtelScopeNode, isOtelScopeNode } from '../types/canvas';
9
11
  import { isOtelScopeNode as checkOtelScopeNode } from '../types/canvas';
12
+ import { pathsOverlap } from '../events/path-helpers';
10
13
 
11
14
  /**
12
15
  * Scopes canvas validation context
@@ -194,6 +197,71 @@ export class ScopesCanvasValidator {
194
197
  }
195
198
  }
196
199
 
200
+ // Validate scope paths (optional field — enforcement is opt-in per scope).
201
+ // Collects declared paths across scopes to check for cross-scope overlap.
202
+ const declaredPaths: Array<{ scope: string; nodeId: string; path: string }> = [];
203
+ for (const { scope, nodeId, node } of canvasScopes) {
204
+ const otelScopeNode = node as unknown as OtelScopeNode;
205
+ const paths = otelScopeNode.paths;
206
+ if (!paths || paths.length === 0) continue;
207
+
208
+ // Warn when a scope declares more than one path — prompt author to
209
+ // confirm the multi-location is intentional (monorepo bundling,
210
+ // generated code, platform splits, migration) rather than accidental.
211
+ if (paths.length > 1) {
212
+ violations.push({
213
+ ruleId: 'scopes-paths-multiple',
214
+ severity: 'warn',
215
+ file: scopesCanvasPath || '.principal-views/architecture.scopes.canvas',
216
+ path: `nodes[${nodeId}].paths`,
217
+ message: `Scope "${scope}" declares ${paths.length} paths`,
218
+ impact: 'Multiple paths can indicate legitimate cases (monorepo bundling, generated code, platform splits, migration) but often signal that the scope should be split',
219
+ suggestion: 'Confirm the multi-location is intentional. Otherwise consider splitting the scope or grouping the code under a single parent folder.',
220
+ });
221
+ }
222
+
223
+ for (const p of paths) {
224
+ declaredPaths.push({ scope, nodeId, path: p });
225
+
226
+ // Warn when a declared path does not exist relative to the repo root.
227
+ const resolved = resolve(basePath, p);
228
+ if (!existsSync(resolved)) {
229
+ violations.push({
230
+ ruleId: 'scopes-paths-missing',
231
+ severity: 'warn',
232
+ file: scopesCanvasPath || '.principal-views/architecture.scopes.canvas',
233
+ path: `nodes[${nodeId}].paths`,
234
+ message: `Path "${p}" declared by scope "${scope}" does not exist`,
235
+ impact: 'Events emitted under this scope cannot be validated against a real code location',
236
+ suggestion: 'Verify the path exists relative to the repository root, or remove it from the scope declaration.',
237
+ });
238
+ }
239
+ }
240
+ }
241
+
242
+ // Cross-scope overlap check. Parent-child path nesting is a valid partition
243
+ // (longest-prefix wins at runtime); any other overlap makes scope ownership
244
+ // of a file ambiguous.
245
+ for (let i = 0; i < declaredPaths.length; i++) {
246
+ for (let j = i + 1; j < declaredPaths.length; j++) {
247
+ const a = declaredPaths[i];
248
+ const b = declaredPaths[j];
249
+ if (a.scope === b.scope) continue;
250
+
251
+ const relationship = pathsOverlap(a.path, b.path);
252
+ if (relationship === 'conflict') {
253
+ violations.push({
254
+ ruleId: 'scopes-paths-overlap',
255
+ severity: 'error',
256
+ file: scopesCanvasPath || '.principal-views/architecture.scopes.canvas',
257
+ message: `Paths overlap between scopes "${a.scope}" ("${a.path}") and "${b.scope}" ("${b.path}")`,
258
+ impact: 'A file covered by two scopes makes scope ownership ambiguous',
259
+ suggestion: 'Separate the paths so they are disjoint, or restructure as parent/child scopes by dotted name (e.g., "principal-ai.core" covering "packages/core/src" and "principal-ai.core.validation" covering "packages/core/src/validation").',
260
+ });
261
+ }
262
+ }
263
+ }
264
+
197
265
  const errors = violations.filter(v => v.severity === 'error');
198
266
 
199
267
  return {
@@ -13,6 +13,18 @@ export {
13
13
  type ScopesCanvasViolation,
14
14
  } from './ScopesCanvasValidator';
15
15
 
16
+ export {
17
+ ScopePathIndex,
18
+ type ScopePathEntry,
19
+ type ScopePathMatch,
20
+ } from './ScopePathIndex';
21
+
22
+ export {
23
+ validateScopeNamespaceNesting,
24
+ type ScopeEventsCanvasPair,
25
+ type ValidateScopeNamespaceNestingInput,
26
+ } from './validateScopeNamespaceNesting';
27
+
16
28
  export {
17
29
  DEFAULT_SCOPE_COLOR,
18
30
  DRAFT_NODE_COLOR,
@@ -0,0 +1,127 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { validateScopeNamespaceNesting } from './validateScopeNamespaceNesting';
3
+ import type { ExtendedCanvas } from '../types/canvas';
4
+
5
+ function scopesCanvas(nodes: Array<{ scope: string; paths?: string[] }>): ExtendedCanvas {
6
+ return {
7
+ nodes: nodes.map((n, i) => ({
8
+ id: `s${i}`,
9
+ type: 'otel-scope',
10
+ description: `${n.scope} scope`,
11
+ ...(n.paths !== undefined ? { paths: n.paths } : {}),
12
+ otel: { scope: n.scope },
13
+ x: 0, y: 0, width: 100, height: 50,
14
+ })),
15
+ edges: [],
16
+ } as ExtendedCanvas;
17
+ }
18
+
19
+ function eventsCanvas(namespaces: Array<{ name: string; paths?: string[] }>): ExtendedCanvas {
20
+ return {
21
+ nodes: namespaces.map((ns, i) => ({
22
+ id: `n${i}`,
23
+ type: 'event-namespace',
24
+ namespace: {
25
+ name: ns.name,
26
+ description: `${ns.name} namespace`,
27
+ ...(ns.paths !== undefined ? { paths: ns.paths } : {}),
28
+ events: [],
29
+ },
30
+ x: 0, y: 0, width: 100, height: 50,
31
+ })),
32
+ edges: [],
33
+ } as ExtendedCanvas;
34
+ }
35
+
36
+ describe('validateScopeNamespaceNesting', () => {
37
+ test('passes when every namespace path is covered by its scope path', () => {
38
+ const violations = validateScopeNamespaceNesting({
39
+ scopesCanvas: scopesCanvas([{ scope: 'a', paths: ['packages/core/src'] }]),
40
+ eventsCanvases: [
41
+ {
42
+ scope: 'a',
43
+ eventsCanvas: eventsCanvas([
44
+ { name: 'events', paths: ['packages/core/src/events'] },
45
+ { name: 'workflow', paths: ['packages/core/src/workflow'] },
46
+ ]),
47
+ },
48
+ ],
49
+ });
50
+ expect(violations).toHaveLength(0);
51
+ });
52
+
53
+ test('flags namespace paths that escape their scope', () => {
54
+ const violations = validateScopeNamespaceNesting({
55
+ scopesCanvas: scopesCanvas([{ scope: 'a', paths: ['packages/core/src'] }]),
56
+ eventsCanvases: [
57
+ {
58
+ scope: 'a',
59
+ eventsCanvas: eventsCanvas([
60
+ { name: 'rogue', paths: ['packages/react/src/hooks'] },
61
+ ]),
62
+ },
63
+ ],
64
+ });
65
+ expect(violations).toHaveLength(1);
66
+ expect(violations[0].ruleId).toBe('scopes-namespace-paths-escape');
67
+ expect(violations[0].severity).toBe('error');
68
+ expect(violations[0].message).toContain('rogue');
69
+ expect(violations[0].message).toContain('packages/react/src/hooks');
70
+ });
71
+
72
+ test('namespace path equal to scope path is covered (not an escape)', () => {
73
+ const violations = validateScopeNamespaceNesting({
74
+ scopesCanvas: scopesCanvas([{ scope: 'a', paths: ['packages/core/src'] }]),
75
+ eventsCanvases: [
76
+ {
77
+ scope: 'a',
78
+ eventsCanvas: eventsCanvas([{ name: 'whole', paths: ['packages/core/src'] }]),
79
+ },
80
+ ],
81
+ });
82
+ expect(violations).toHaveLength(0);
83
+ });
84
+
85
+ test('scope without paths imposes no constraint (opt-out)', () => {
86
+ const violations = validateScopeNamespaceNesting({
87
+ scopesCanvas: scopesCanvas([{ scope: 'a' }]),
88
+ eventsCanvases: [
89
+ {
90
+ scope: 'a',
91
+ eventsCanvas: eventsCanvas([{ name: 'anywhere', paths: ['packages/anywhere/src'] }]),
92
+ },
93
+ ],
94
+ });
95
+ expect(violations).toHaveLength(0);
96
+ });
97
+
98
+ test('namespaces without paths are skipped (opt-in per namespace)', () => {
99
+ const violations = validateScopeNamespaceNesting({
100
+ scopesCanvas: scopesCanvas([{ scope: 'a', paths: ['packages/core/src'] }]),
101
+ eventsCanvases: [
102
+ {
103
+ scope: 'a',
104
+ eventsCanvas: eventsCanvas([{ name: 'unenforced' }]),
105
+ },
106
+ ],
107
+ });
108
+ expect(violations).toHaveLength(0);
109
+ });
110
+
111
+ test('multiple scope paths: namespace covered by any one is allowed', () => {
112
+ const violations = validateScopeNamespaceNesting({
113
+ scopesCanvas: scopesCanvas([
114
+ { scope: 'a', paths: ['packages/core/src', 'packages/core/generated'] },
115
+ ]),
116
+ eventsCanvases: [
117
+ {
118
+ scope: 'a',
119
+ eventsCanvas: eventsCanvas([
120
+ { name: 'gen', paths: ['packages/core/generated/events'] },
121
+ ]),
122
+ },
123
+ ],
124
+ });
125
+ expect(violations).toHaveLength(0);
126
+ });
127
+ });
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Scope-Namespace Nesting Validator
3
+ *
4
+ * Cross-canvas check: every `event-namespace.paths` entry declared in a
5
+ * scope's events canvas must be covered by that scope's own `paths`.
6
+ * Surfaces `scopes-namespace-paths-escape` when a namespace claims a region
7
+ * outside its owning scope.
8
+ *
9
+ * Runs alongside the existing scopes-canvas validation but requires both
10
+ * canvases as input, so it lives outside `ScopesCanvasValidator` (which is
11
+ * single-canvas).
12
+ */
13
+
14
+ import type { ExtendedCanvas } from '../types/canvas';
15
+ import { isOtelScopeNode } from '../types/canvas';
16
+ import { pathCovers } from '../events/path-helpers';
17
+ import type { ScopesCanvasViolation } from './ScopesCanvasValidator';
18
+
19
+ /**
20
+ * One events canvas paired with the scope it documents.
21
+ * The mapping comes from the existing scope → events-canvas filename
22
+ * convention (see `ScopeEventsValidator`); callers resolve it before
23
+ * passing it in.
24
+ */
25
+ export interface ScopeEventsCanvasPair {
26
+ scope: string;
27
+ eventsCanvas: ExtendedCanvas;
28
+ eventsCanvasPath?: string;
29
+ }
30
+
31
+ export interface ValidateScopeNamespaceNestingInput {
32
+ scopesCanvas: ExtendedCanvas;
33
+ scopesCanvasPath?: string;
34
+ eventsCanvases: readonly ScopeEventsCanvasPair[];
35
+ }
36
+
37
+ /**
38
+ * For every namespace.paths entry, verify it is covered by its scope's paths.
39
+ * Scopes without `paths` impose no constraint (opt-in per scope) and are
40
+ * skipped. Namespaces without `paths` are also skipped — namespace-level
41
+ * enforcement remains independently opt-in.
42
+ */
43
+ export function validateScopeNamespaceNesting(
44
+ input: ValidateScopeNamespaceNestingInput,
45
+ ): ScopesCanvasViolation[] {
46
+ const { scopesCanvas, scopesCanvasPath, eventsCanvases } = input;
47
+ const violations: ScopesCanvasViolation[] = [];
48
+
49
+ // Build scope → paths map from the scopes canvas.
50
+ const scopePaths = new Map<string, string[]>();
51
+ for (const node of scopesCanvas.nodes || []) {
52
+ if (!isOtelScopeNode(node)) continue;
53
+ const scope = node.otel?.scope;
54
+ const paths = node.paths;
55
+ if (!scope || !paths || paths.length === 0) continue;
56
+ scopePaths.set(scope, paths);
57
+ }
58
+
59
+ // For each events canvas, walk its event-namespace nodes and check that
60
+ // every namespace.paths entry sits inside the owning scope's paths.
61
+ for (const pair of eventsCanvases) {
62
+ const ownerPaths = scopePaths.get(pair.scope);
63
+ if (!ownerPaths) continue; // scope opted out — nothing to check
64
+
65
+ for (const node of pair.eventsCanvas.nodes || []) {
66
+ if ((node as any)?.type !== 'event-namespace') continue;
67
+ const ns = (node as any).namespace;
68
+ if (!ns?.name || !Array.isArray(ns.paths) || ns.paths.length === 0) continue;
69
+
70
+ for (const declared of ns.paths as string[]) {
71
+ const covered = ownerPaths.some((sp) => pathCovers(sp, declared));
72
+ if (covered) continue;
73
+
74
+ violations.push({
75
+ ruleId: 'scopes-namespace-paths-escape',
76
+ severity: 'error',
77
+ file: scopesCanvasPath || '.principal-views/architecture.scopes.canvas',
78
+ path: `nodes[scope="${pair.scope}"].paths`,
79
+ message: `Namespace "${ns.name}" path "${declared}" escapes the bounds of scope "${pair.scope}" (paths: ${ownerPaths.map((p) => `"${p}"`).join(', ')})`,
80
+ impact: 'A namespace claiming code outside its owning scope breaks the scope→namespace partition; events from that file would be attributed to the wrong scope',
81
+ suggestion: `Either move the namespace path under one of "${pair.scope}"'s paths, or extend the scope to cover the namespace's region. If the code genuinely belongs to a different scope, declare the namespace in that scope's events canvas instead.`,
82
+ });
83
+ }
84
+ }
85
+ }
86
+
87
+ return violations;
88
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Auxiliary Manifest Types
3
+ *
4
+ * The auxiliary manifest catalogs project regions that are *not* OTEL scopes —
5
+ * folders like `docs/` or `.github/` that support the project but don't emit
6
+ * telemetry. Scope paths and area paths together form a partition of the
7
+ * accounted-for filesystem.
8
+ *
9
+ * The manifest is open-ended: `areas` is the first section, but the top-level
10
+ * shape leaves room for future sibling sections (e.g. external integrations)
11
+ * without breaking the format.
12
+ */
13
+
14
+ /**
15
+ * One project area — a folder (or file) that supports the project outside the
16
+ * OTEL telemetry surface.
17
+ */
18
+ export interface ProjectArea {
19
+ /**
20
+ * Display name. Must be unique across all areas in the manifest — used as
21
+ * the area's identifier in diagnostics.
22
+ */
23
+ name: string;
24
+
25
+ /**
26
+ * One or more repo-relative paths owned by this area. Each entry may be a
27
+ * folder (covers all descendants) or a specific file. Paths must not
28
+ * overlap other areas, and must not overlap any scope path declared in a
29
+ * `.scopes.canvas`.
30
+ */
31
+ paths: string[];
32
+
33
+ /**
34
+ * Free-text purpose. The "light documentation" — a sentence about why this
35
+ * folder exists.
36
+ */
37
+ description: string;
38
+ }
39
+
40
+ /**
41
+ * The full manifest document. Persisted at
42
+ * `.principal-views/auxiliary.manifest.json`.
43
+ */
44
+ export interface AuxiliaryManifest {
45
+ /**
46
+ * Optional pointer to the JSON Schema that describes this file. Lets editors
47
+ * give autocomplete and validation when the field is set.
48
+ */
49
+ $schema?: string;
50
+
51
+ /** Project areas declared in this manifest. */
52
+ areas: ProjectArea[];
53
+ }
54
+
55
+ /**
56
+ * Type guard: true when the value plausibly conforms to `AuxiliaryManifest`.
57
+ * Shallow check — full validation lives in `AuxiliaryManifestValidator`.
58
+ */
59
+ export function isAuxiliaryManifest(value: unknown): value is AuxiliaryManifest {
60
+ if (!value || typeof value !== 'object') return false;
61
+ const v = value as { areas?: unknown };
62
+ return Array.isArray(v.areas);
63
+ }
@@ -1014,6 +1014,21 @@ export interface OtelScopeNode extends OtelNodeBase {
1014
1014
  type: 'otel-scope';
1015
1015
  /** Short description of this instrumentation scope */
1016
1016
  description?: string;
1017
+ /**
1018
+ * Optional source paths that define this scope's code region.
1019
+ * Each entry may be a folder (covers all descendants) or a specific file.
1020
+ * When present, events under this scope may only originate from files
1021
+ * covered by one of these paths, and every `event-namespace` declared in
1022
+ * this scope's events canvas must have its own `paths` nested inside this
1023
+ * region. Scopes without `paths` remain unenforced — enforcement is opt-in
1024
+ * per scope.
1025
+ *
1026
+ * Semantics are "ownership / partition", distinct from `otel.files` on
1027
+ * `otel-event` nodes (which lists specific emission sites). Across scopes,
1028
+ * paths must be disjoint except for strict parent-child nesting by
1029
+ * dotted-scope name (longest-prefix wins).
1030
+ */
1031
+ paths?: string[];
1017
1032
  /** OTEL metadata - scope name is required */
1018
1033
  otel: OtelMetadata & {
1019
1034
  /** Scope name (required - maps to getTracer('scope-name')) */
@@ -456,3 +456,4 @@ export * from './resource-match';
456
456
  export * from './canvas-scope';
457
457
  export * from './audit';
458
458
  export * from './dashboard';
459
+ export * from './auxiliary';