@blogmcp/nav-core 0.1.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.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @blogmcp/nav-core — traverse
3
+ *
4
+ * Pure read helpers over a resolved tree. Two consumers with opposite needs use
5
+ * the same functions: the SDK runs them in-memory over a cached tree (zero
6
+ * round-trips), and the server exposes them as MCP tools (token-efficient).
7
+ *
8
+ * All functions are non-mutating. Functions that depend on display order sort
9
+ * siblings by `order` internally (stable: explicit order ascending, then
10
+ * undefined-order nodes in their original sequence).
11
+ */
12
+ import type { FlatNode, LocalizedTree, Locale, NavNode, NavTree } from '../types/index.js';
13
+ /** Stable sort of siblings by `order` (undefined sorts last, ties keep input order). */
14
+ export declare function sortSiblings(nodes: readonly NavNode[]): NavNode[];
15
+ /** Return a new tree with every level's children sorted by `order`. */
16
+ export declare function sortTree(tree: NavTree): NavNode[];
17
+ /** Depth-first pre-order walk. Return `false` from the visitor to stop early. */
18
+ export declare function walk(tree: NavTree, visitor: (node: NavNode, parent: NavNode | null, depth: number) => void | boolean): void;
19
+ /** Find a node by id, or `undefined`. */
20
+ export declare function findNode(tree: NavTree, id: string): NavNode | undefined;
21
+ /** Find a node's parent, or `null` if it is a root (or `undefined` if not found). */
22
+ export declare function getParent(tree: NavTree, id: string): NavNode | null | undefined;
23
+ /**
24
+ * Ancestor path from root to the node, inclusive of the node itself.
25
+ * Empty array if not found. This is the basis for breadcrumbs.
26
+ */
27
+ export declare function getPath(tree: NavTree, id: string): NavNode[];
28
+ /** Ancestors of a node, root → parent (excludes the node itself). */
29
+ export declare function breadcrumbs(tree: NavTree, id: string): NavNode[];
30
+ /** Siblings of a node (same parent), excluding the node itself, in display order. */
31
+ export declare function getSiblings(tree: NavTree, id: string): NavNode[];
32
+ /** The node together with its descendants, or `undefined`. */
33
+ export declare function getSubtree(tree: NavTree, id: string): NavNode | undefined;
34
+ /** Default "navigable" predicate: visible article references only. */
35
+ export declare function isNavigable(node: NavNode): boolean;
36
+ export interface NextPrev {
37
+ readonly prev: NavNode | null;
38
+ readonly next: NavNode | null;
39
+ }
40
+ /**
41
+ * Previous/next navigable node in depth-first display order. By default only
42
+ * visible `article-ref` nodes participate; pass a predicate to change that.
43
+ */
44
+ export declare function getNextPrev(tree: NavTree, id: string, predicate?: (node: NavNode) => boolean): NextPrev;
45
+ /** Flatten to FLAT form (parent pointer + index + depth), in display order. */
46
+ export declare function flatten(tree: NavTree): FlatNode[];
47
+ /** Default access-tier ranking (higher = more access). */
48
+ export declare const DEFAULT_TIER_RANK: Readonly<Record<string, number>>;
49
+ /**
50
+ * Whether a node is visible to a viewer tier. A node with no `visibility.tiers`
51
+ * is public. Otherwise the viewer must meet (rank ≥) at least one declared tier.
52
+ * Pure helper; pair with {@link prune} to filter a tree. (Content-level access —
53
+ * which articles exist at all — is enforced upstream in the data query; this
54
+ * gates override-declared per-node visibility.)
55
+ */
56
+ export declare function visibleToTier(node: NavNode, viewerTier: string, ranks?: Readonly<Record<string, number>>): boolean;
57
+ /** Remove every node (and its subtree) for which `keep` returns false. */
58
+ export declare function prune(tree: NavTree, keep: (node: NavNode) => boolean): NavNode[];
59
+ export interface LocalizeOptions {
60
+ readonly locale: Locale;
61
+ readonly fallbackLocale?: Locale;
62
+ /**
63
+ * Supplies a default label when `labels` has none for the locale — typically
64
+ * the referenced article's title. nav-core does not know article content, so
65
+ * the consumer provides this.
66
+ */
67
+ readonly labelResolver?: (node: NavNode, locale: Locale) => string | undefined;
68
+ }
69
+ /** Resolve `labels` to a single locale, producing a {@link LocalizedTree}. */
70
+ export declare function localize(tree: NavTree, options: LocalizeOptions): LocalizedTree;
@@ -0,0 +1,213 @@
1
+ /**
2
+ * @blogmcp/nav-core — traverse
3
+ *
4
+ * Pure read helpers over a resolved tree. Two consumers with opposite needs use
5
+ * the same functions: the SDK runs them in-memory over a cached tree (zero
6
+ * round-trips), and the server exposes them as MCP tools (token-efficient).
7
+ *
8
+ * All functions are non-mutating. Functions that depend on display order sort
9
+ * siblings by `order` internally (stable: explicit order ascending, then
10
+ * undefined-order nodes in their original sequence).
11
+ */
12
+ /* -------------------------------------------------------------------------- */
13
+ /* ordering */
14
+ /* -------------------------------------------------------------------------- */
15
+ /** Stable sort of siblings by `order` (undefined sorts last, ties keep input order). */
16
+ export function sortSiblings(nodes) {
17
+ return [...nodes].sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity));
18
+ }
19
+ /** Return a new tree with every level's children sorted by `order`. */
20
+ export function sortTree(tree) {
21
+ return sortSiblings(tree).map((node) => node.children && node.children.length > 0
22
+ ? { ...node, children: sortTree(node.children) }
23
+ : node);
24
+ }
25
+ /* -------------------------------------------------------------------------- */
26
+ /* lookup */
27
+ /* -------------------------------------------------------------------------- */
28
+ /** Depth-first pre-order walk. Return `false` from the visitor to stop early. */
29
+ export function walk(tree, visitor) {
30
+ const recur = (nodes, parent, depth) => {
31
+ for (const node of nodes) {
32
+ if (visitor(node, parent, depth) === false)
33
+ return false;
34
+ if (node.children && !recur(node.children, node, depth + 1))
35
+ return false;
36
+ }
37
+ return true;
38
+ };
39
+ recur(tree, null, 0);
40
+ }
41
+ /** Find a node by id, or `undefined`. */
42
+ export function findNode(tree, id) {
43
+ let found;
44
+ walk(tree, (node) => {
45
+ if (node.id === id) {
46
+ found = node;
47
+ return false;
48
+ }
49
+ });
50
+ return found;
51
+ }
52
+ /** Find a node's parent, or `null` if it is a root (or `undefined` if not found). */
53
+ export function getParent(tree, id) {
54
+ let result;
55
+ walk(tree, (node, parent) => {
56
+ if (node.id === id) {
57
+ result = parent;
58
+ return false;
59
+ }
60
+ });
61
+ return result;
62
+ }
63
+ /**
64
+ * Ancestor path from root to the node, inclusive of the node itself.
65
+ * Empty array if not found. This is the basis for breadcrumbs.
66
+ */
67
+ export function getPath(tree, id) {
68
+ const path = [];
69
+ let found = false;
70
+ const recur = (nodes) => {
71
+ for (const node of nodes) {
72
+ path.push(node);
73
+ if (node.id === id) {
74
+ found = true;
75
+ return true;
76
+ }
77
+ if (node.children && recur(node.children))
78
+ return true;
79
+ path.pop();
80
+ }
81
+ return false;
82
+ };
83
+ recur(tree);
84
+ return found ? path : [];
85
+ }
86
+ /** Ancestors of a node, root → parent (excludes the node itself). */
87
+ export function breadcrumbs(tree, id) {
88
+ const path = getPath(tree, id);
89
+ return path.slice(0, -1);
90
+ }
91
+ /** Siblings of a node (same parent), excluding the node itself, in display order. */
92
+ export function getSiblings(tree, id) {
93
+ const parent = getParent(tree, id);
94
+ if (parent === undefined)
95
+ return [];
96
+ const siblings = parent === null ? tree : (parent.children ?? []);
97
+ return sortSiblings(siblings).filter((n) => n.id !== id);
98
+ }
99
+ /** The node together with its descendants, or `undefined`. */
100
+ export function getSubtree(tree, id) {
101
+ return findNode(tree, id);
102
+ }
103
+ /* -------------------------------------------------------------------------- */
104
+ /* sequential navigation (prev / next) */
105
+ /* -------------------------------------------------------------------------- */
106
+ /** Default "navigable" predicate: visible article references only. */
107
+ export function isNavigable(node) {
108
+ return node.type === 'article-ref' && node.hidden !== true;
109
+ }
110
+ /**
111
+ * Previous/next navigable node in depth-first display order. By default only
112
+ * visible `article-ref` nodes participate; pass a predicate to change that.
113
+ */
114
+ export function getNextPrev(tree, id, predicate = isNavigable) {
115
+ const sequence = [];
116
+ walk(sortTree(tree), (node) => {
117
+ if (predicate(node))
118
+ sequence.push(node);
119
+ });
120
+ const idx = sequence.findIndex((n) => n.id === id);
121
+ if (idx === -1)
122
+ return { prev: null, next: null };
123
+ return {
124
+ prev: idx > 0 ? sequence[idx - 1] : null,
125
+ next: idx < sequence.length - 1 ? sequence[idx + 1] : null,
126
+ };
127
+ }
128
+ /* -------------------------------------------------------------------------- */
129
+ /* projections of shape */
130
+ /* -------------------------------------------------------------------------- */
131
+ /** Flatten to FLAT form (parent pointer + index + depth), in display order. */
132
+ export function flatten(tree) {
133
+ const out = [];
134
+ const recur = (nodes, parentId, depth) => {
135
+ sortSiblings(nodes).forEach((node, index) => {
136
+ out.push({ id: node.id, parentId, type: node.type, index, depth });
137
+ if (node.children)
138
+ recur(node.children, node.id, depth + 1);
139
+ });
140
+ };
141
+ recur(tree, null, 0);
142
+ return out;
143
+ }
144
+ /* -------------------------------------------------------------------------- */
145
+ /* access-tier visibility */
146
+ /* -------------------------------------------------------------------------- */
147
+ /** Default access-tier ranking (higher = more access). */
148
+ export const DEFAULT_TIER_RANK = {
149
+ public: 0,
150
+ premium: 1,
151
+ partners: 2,
152
+ internal: 3,
153
+ };
154
+ /**
155
+ * Whether a node is visible to a viewer tier. A node with no `visibility.tiers`
156
+ * is public. Otherwise the viewer must meet (rank ≥) at least one declared tier.
157
+ * Pure helper; pair with {@link prune} to filter a tree. (Content-level access —
158
+ * which articles exist at all — is enforced upstream in the data query; this
159
+ * gates override-declared per-node visibility.)
160
+ */
161
+ export function visibleToTier(node, viewerTier, ranks = DEFAULT_TIER_RANK) {
162
+ const tiers = node.visibility?.tiers;
163
+ if (!tiers || tiers.length === 0)
164
+ return true;
165
+ const viewerRank = ranks[viewerTier] ?? 0;
166
+ return tiers.some((t) => viewerRank >= (ranks[t] ?? 0));
167
+ }
168
+ /** Remove every node (and its subtree) for which `keep` returns false. */
169
+ export function prune(tree, keep) {
170
+ return tree
171
+ .filter(keep)
172
+ .map((node) => node.children && node.children.length > 0
173
+ ? { ...node, children: prune(node.children, keep) }
174
+ : node);
175
+ }
176
+ /** Resolve `labels` to a single locale, producing a {@link LocalizedTree}. */
177
+ export function localize(tree, options) {
178
+ const { locale, fallbackLocale, labelResolver } = options;
179
+ const resolveLabel = (node) => {
180
+ return (node.labels?.[locale] ??
181
+ (fallbackLocale ? node.labels?.[fallbackLocale] : undefined) ??
182
+ labelResolver?.(node, locale) ??
183
+ (fallbackLocale ? labelResolver?.(node, fallbackLocale) : undefined));
184
+ };
185
+ const localizeNode = (node) => {
186
+ const base = {
187
+ id: node.id,
188
+ type: node.type,
189
+ label: resolveLabel(node),
190
+ order: node.order,
191
+ badge: node.badge,
192
+ icon: node.icon,
193
+ hidden: node.hidden,
194
+ visibility: node.visibility,
195
+ data: node.data,
196
+ children: node.children ? sortSiblings(node.children).map(localizeNode) : [],
197
+ };
198
+ switch (node.type) {
199
+ case 'article-ref':
200
+ return { ...base, ref: node.ref, owned: node.owned, slug: node.slug };
201
+ case 'external-link':
202
+ return { ...base, href: node.href };
203
+ case 'generated-section':
204
+ return { ...base, slug: node.slug };
205
+ case 'group':
206
+ return { ...base, link: node.link, collapsed: node.collapsed, collapsible: node.collapsible };
207
+ case 'separator':
208
+ return base;
209
+ }
210
+ };
211
+ return sortSiblings(tree).map(localizeNode);
212
+ }
213
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/traverse/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,165 @@
1
+ /**
2
+ * @blogmcp/nav-core — types
3
+ *
4
+ * The framework-agnostic contract for the KB navigation (menu) tree.
5
+ * See competitor-monitor/docs/internal/navigation-model.md §4 for the design.
6
+ *
7
+ * Design invariants encoded here:
8
+ * - Stable UUID per node (deep links / anchored comments / moves survive reorder).
9
+ * - Discriminated `type` union: article-ref | group | external-link |
10
+ * generated-section | separator.
11
+ * - Single multilingual tree: per-node `labels` map keyed by locale; the
12
+ * referenced article's title is the default (resolved by the consumer via a
13
+ * `labelResolver`, since nav-core does not know article content).
14
+ * - `badge` and `icon` are typed first-class (Starlight precedent); everything
15
+ * else genuinely arbitrary lives in the opaque, size-capped `data` bag.
16
+ * - Trees are treated as immutable: every node/collection is `readonly`, and the
17
+ * mutate module returns new trees rather than editing in place.
18
+ */
19
+ /** Discriminator for {@link NavNode}. */
20
+ export type NavNodeType = 'article-ref' | 'group' | 'external-link' | 'generated-section' | 'separator';
21
+ /** All node type discriminators, for runtime validation. */
22
+ export declare const NAV_NODE_TYPES: readonly NavNodeType[];
23
+ /** BCP-47 / language code, e.g. `cs`, `en`, `pt-BR`. */
24
+ export type Locale = string;
25
+ /** Per-locale label overrides. Missing locales fall back (see localize). */
26
+ export type LocaleLabels = Readonly<Record<Locale, string>>;
27
+ /**
28
+ * Typed first-class presentation hint. Mirrors Astro Starlight's `badge`.
29
+ * `variant` is intentionally open (`string`) so themes can define their own.
30
+ */
31
+ export interface BadgeSpec {
32
+ readonly text: string;
33
+ readonly variant?: string;
34
+ readonly class?: string;
35
+ }
36
+ /**
37
+ * Access-tier gating. nav-core stays policy-free: it only carries the declared
38
+ * tiers; consumers decide what a viewer may see (e.g. via {@link prune}).
39
+ * Omitted = inherit from ancestor / public.
40
+ */
41
+ export interface Visibility {
42
+ readonly tiers?: readonly string[];
43
+ }
44
+ /**
45
+ * Opaque, namespaced customer extension bag. The platform stores and round-trips
46
+ * it but never interprets it. Size/depth-capped by {@link validate}. Renderers
47
+ * must treat values as untrusted.
48
+ */
49
+ export type NavData = Readonly<Record<string, unknown>>;
50
+ /** Fields shared by every node type. */
51
+ export interface NavNodeBase {
52
+ /** Stable UUID. Unique across the whole tree. */
53
+ readonly id: string;
54
+ /** Sibling sort key. Fractional, so a node can be inserted between two siblings without renumbering. */
55
+ readonly order?: number;
56
+ /** Per-locale label overrides. */
57
+ readonly labels?: LocaleLabels;
58
+ /** Typed first-class badge hint. */
59
+ readonly badge?: BadgeSpec;
60
+ /** Typed first-class icon hint (icon name/key; renderer maps it). */
61
+ readonly icon?: string;
62
+ /** Reachable by direct URL but suppressed from this menu. Distinct from access-tier {@link visibility}. */
63
+ readonly hidden?: boolean;
64
+ /** Access-tier gating. */
65
+ readonly visibility?: Visibility;
66
+ /** Opaque customer extension bag. */
67
+ readonly data?: NavData;
68
+ /** Child nodes. Only `group` and `article-ref` may have children. */
69
+ readonly children?: readonly NavNode[];
70
+ }
71
+ /** References a `kb_articles.id`. */
72
+ export interface ArticleRefNode extends NavNodeBase {
73
+ readonly type: 'article-ref';
74
+ /** `kb_articles.id`. */
75
+ readonly ref: string;
76
+ /** Public URL handle (filename without extension) — lets consumers build links without a second lookup. */
77
+ readonly slug?: string;
78
+ /**
79
+ * Exactly one nav location should *own* an article (drives prev/next pagination
80
+ * and the canonical breadcrumb). Other locations referencing the same article
81
+ * set `owned: false` (Docusaurus `doc` vs `ref`).
82
+ */
83
+ readonly owned?: boolean;
84
+ }
85
+ /** Optional clickable target for a {@link GroupNode} heading. */
86
+ export type GroupLink = {
87
+ readonly kind: 'article';
88
+ readonly ref: string;
89
+ } | {
90
+ readonly kind: 'generated';
91
+ readonly slug: string;
92
+ };
93
+ /** A section. May be a clickable heading and may collapse. */
94
+ export interface GroupNode extends NavNodeBase {
95
+ readonly type: 'group';
96
+ /** Optional clickable section heading (existing article or a generated-section). */
97
+ readonly link?: GroupLink;
98
+ /** Default open/closed state. */
99
+ readonly collapsed?: boolean;
100
+ /** Whether the group can be collapsed at all. */
101
+ readonly collapsible?: boolean;
102
+ }
103
+ /** Arbitrary external URL. Override-only — auto-build never emits these. */
104
+ export interface ExternalLinkNode extends NavNodeBase {
105
+ readonly type: 'external-link';
106
+ readonly href: string;
107
+ }
108
+ /** Query backing a {@link GeneratedSectionNode}. Open shape; `tag` is the v1 case. */
109
+ export interface GeneratedQuery {
110
+ readonly tag?: string;
111
+ readonly [key: string]: unknown;
112
+ }
113
+ /** A section resolved at read time (e.g. "all articles tagged X"). */
114
+ export interface GeneratedSectionNode extends NavNodeBase {
115
+ readonly type: 'generated-section';
116
+ /** URL slug of the generated landing page. */
117
+ readonly slug: string;
118
+ /** Query that selects the members. */
119
+ readonly query: GeneratedQuery;
120
+ readonly title?: string;
121
+ readonly description?: string;
122
+ }
123
+ /** Pure visual divider. Leaf node. */
124
+ export interface SeparatorNode extends NavNodeBase {
125
+ readonly type: 'separator';
126
+ }
127
+ /** Any navigation node. */
128
+ export type NavNode = ArticleRefNode | GroupNode | ExternalLinkNode | GeneratedSectionNode | SeparatorNode;
129
+ /** A navigation tree is an ordered list of root nodes. */
130
+ export type NavTree = readonly NavNode[];
131
+ /** Node types that are allowed to have children. */
132
+ export declare const CONTAINER_NODE_TYPES: readonly NavNodeType[];
133
+ /** One node in FLAT form: parent pointer + depth instead of nesting. */
134
+ export interface FlatNode {
135
+ readonly id: string;
136
+ readonly parentId: string | null;
137
+ readonly type: NavNodeType;
138
+ /** 0-based position among siblings (post-sort). */
139
+ readonly index: number;
140
+ /** Depth from root (root nodes = 0). */
141
+ readonly depth: number;
142
+ }
143
+ /** A node with its label resolved to one locale (no `labels` map). */
144
+ export interface LocalizedNode {
145
+ readonly id: string;
146
+ readonly type: NavNodeType;
147
+ /** Resolved label for the requested locale, if any could be determined. */
148
+ readonly label?: string;
149
+ readonly order?: number;
150
+ readonly badge?: BadgeSpec;
151
+ readonly icon?: string;
152
+ readonly hidden?: boolean;
153
+ readonly visibility?: Visibility;
154
+ readonly data?: NavData;
155
+ /** Carried through for renderers (ref/href/slug/link/collapsed…). */
156
+ readonly ref?: string;
157
+ readonly owned?: boolean;
158
+ readonly href?: string;
159
+ readonly slug?: string;
160
+ readonly link?: GroupLink;
161
+ readonly collapsed?: boolean;
162
+ readonly collapsible?: boolean;
163
+ readonly children: readonly LocalizedNode[];
164
+ }
165
+ export type LocalizedTree = readonly LocalizedNode[];
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @blogmcp/nav-core — types
3
+ *
4
+ * The framework-agnostic contract for the KB navigation (menu) tree.
5
+ * See competitor-monitor/docs/internal/navigation-model.md §4 for the design.
6
+ *
7
+ * Design invariants encoded here:
8
+ * - Stable UUID per node (deep links / anchored comments / moves survive reorder).
9
+ * - Discriminated `type` union: article-ref | group | external-link |
10
+ * generated-section | separator.
11
+ * - Single multilingual tree: per-node `labels` map keyed by locale; the
12
+ * referenced article's title is the default (resolved by the consumer via a
13
+ * `labelResolver`, since nav-core does not know article content).
14
+ * - `badge` and `icon` are typed first-class (Starlight precedent); everything
15
+ * else genuinely arbitrary lives in the opaque, size-capped `data` bag.
16
+ * - Trees are treated as immutable: every node/collection is `readonly`, and the
17
+ * mutate module returns new trees rather than editing in place.
18
+ */
19
+ /** All node type discriminators, for runtime validation. */
20
+ export const NAV_NODE_TYPES = [
21
+ 'article-ref',
22
+ 'group',
23
+ 'external-link',
24
+ 'generated-section',
25
+ 'separator',
26
+ ];
27
+ /** Node types that are allowed to have children. */
28
+ export const CONTAINER_NODE_TYPES = ['group', 'article-ref'];
29
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAUH,4DAA4D;AAC5D,MAAM,CAAC,MAAM,cAAc,GAA2B;IACpD,aAAa;IACb,OAAO;IACP,eAAe;IACf,mBAAmB;IACnB,WAAW;CACZ,CAAC;AA8HF,oDAAoD;AACpD,MAAM,CAAC,MAAM,oBAAoB,GAA2B,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @blogmcp/nav-core — validate
3
+ *
4
+ * Structural validation of a navigation tree. Server-authoritative: the API
5
+ * layer re-runs this on every mutation (clients can't be trusted not to create
6
+ * a cycle / oversized data bag). nav-core never *interprets* the `data` bag — it
7
+ * only enforces size/depth caps so the JSONB-source-of-truth stays a small,
8
+ * one-row-read, cacheable artifact (see navigation-model.md §2, §4).
9
+ */
10
+ import { type NavTree } from '../types/index.js';
11
+ /** Default caps. Override per call via {@link ValidateOptions}. */
12
+ export declare const DEFAULT_MAX_DEPTH = 6;
13
+ export declare const DEFAULT_MAX_DATA_BYTES = 4096;
14
+ export declare const DEFAULT_MAX_DATA_DEPTH = 8;
15
+ export interface ValidateOptions {
16
+ /** Max nesting depth (root nodes are depth 0). */
17
+ readonly maxDepth?: number;
18
+ /** Max serialized byte size of a single node's `data` bag. */
19
+ readonly maxDataBytes?: number;
20
+ /** Max nesting depth of a single node's `data` bag. */
21
+ readonly maxDataDepth?: number;
22
+ }
23
+ export type ValidationCode = 'DUPLICATE_ID' | 'MISSING_ID' | 'UNKNOWN_TYPE' | 'MAX_DEPTH_EXCEEDED' | 'CHILDREN_NOT_ALLOWED' | 'MISSING_REQUIRED_FIELD' | 'INVALID_ORDER' | 'MULTIPLE_OWNERS' | 'DATA_TOO_LARGE' | 'DATA_TOO_DEEP' | 'INVALID_OVERRIDES';
24
+ export interface ValidationError {
25
+ readonly code: ValidationCode;
26
+ readonly message: string;
27
+ readonly nodeId?: string;
28
+ }
29
+ export interface ValidationResult {
30
+ readonly valid: boolean;
31
+ readonly errors: readonly ValidationError[];
32
+ }
33
+ /**
34
+ * Validate a navigation tree. Returns all errors found (does not throw); callers
35
+ * that prefer to throw can use {@link assertValid}.
36
+ */
37
+ export declare function validate(tree: NavTree, options?: ValidateOptions): ValidationResult;
38
+ /** Throwing variant. */
39
+ export declare function assertValid(tree: NavTree, options?: ValidateOptions): void;
40
+ /** Error thrown by {@link assertValid}. */
41
+ export declare class NavValidationError extends Error {
42
+ readonly errors: readonly ValidationError[];
43
+ constructor(message: string, errors: readonly ValidationError[]);
44
+ }