@unified-product-graph/sdk 0.6.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,1480 @@
1
+ import * as _unified_product_graph_core from '@unified-product-graph/core';
2
+ import { UPGEdge, UPGDocument, UPGBaseNode, UPGPortfolioDocument, UPGCrossEdge, UPGEdgeType, UPGProductStage, AntiPatternInputs, PropertyDefinition } from '@unified-product-graph/core';
3
+
4
+ /**
5
+ * Classify and (optionally) repair dangling edges in a loaded UPG document.
6
+ *
7
+ * Three classes:
8
+ *
9
+ * - **expected**: cross-product edge with `source_product_id` /
10
+ * `target_product_id` annotations. Resolves once the sibling product is
11
+ * loaded; keep these in the doc.
12
+ * - **suspect**: cross-product edge type (per `UPG_CROSS_EDGE_TYPES`) but
13
+ * no annotation. May be a stale ref; needs operator decision.
14
+ * - **corrupt**: non-cross-product edge with a missing endpoint. A genuine
15
+ * integrity break, typically the product of a botched manual edit or
16
+ * incomplete migration.
17
+ *
18
+ * A future change will move cross-product edges to `portfolio.cross_edges`
19
+ * and eliminate `expected` entirely. Until then, this classifier is the
20
+ * bridge: better hygiene without forcing the schema migration.
21
+ */
22
+
23
+ type DanglingEdgeClass = 'expected' | 'suspect' | 'corrupt';
24
+ interface DanglingEdgeRecord {
25
+ id: string;
26
+ type: string;
27
+ class: DanglingEdgeClass;
28
+ source: string;
29
+ target: string;
30
+ source_product_id?: string;
31
+ target_product_id?: string;
32
+ /** Which endpoint(s) didn't resolve. */
33
+ missing: Array<'source' | 'target'>;
34
+ }
35
+ interface DanglingEdgeReport {
36
+ total: number;
37
+ by_class: Record<DanglingEdgeClass, number>;
38
+ edges: DanglingEdgeRecord[];
39
+ }
40
+ /**
41
+ * Walk `edges` and pick out every edge with at least one endpoint not in
42
+ * `nodeIds`. Pure function — does not mutate inputs. Safe to call many times.
43
+ */
44
+ declare function classifyDanglingEdges(edges: readonly UPGEdge[], nodeIds: ReadonlySet<string>): DanglingEdgeReport;
45
+ /**
46
+ * Render the integrity report as the multi-line stderr message described
47
+ * in the acceptance criteria. Returns null when there are no dangling edges.
48
+ */
49
+ declare function renderDanglingReport(report: DanglingEdgeReport, filePath: string): string | null;
50
+
51
+ /**
52
+ * Schema-drift summary. Walks a loaded UPG document once on load and
53
+ * counts deviations from the canonical UPG shape, by class:
54
+ *
55
+ * 1. entity_drift: nodes whose type ∉ UPG_TYPES (often have a
56
+ * UPG_MIGRATIONS or UPG_SPLIT_MIGRATIONS rule).
57
+ * 2. edge_drift: edges whose type ∉ UPG_EDGE_CATALOG. A non-canonical
58
+ * edge type often has a UPG_EDGE_MIGRATIONS rule
59
+ * pointing at its successor; see validation.ts for the
60
+ * suggested-migration logic. Canonical edges are never
61
+ * counted, even if they still appear as `from` in a
62
+ * historical migration rule.
63
+ * 3. top_level_drift: nodes with top-level keys outside UPGBaseNode.
64
+ * 4. lifecycle_drift: nodes whose `status` value is not a valid phase id
65
+ * for their entity type (per `getLifecycleForType`).
66
+ * 5. self_referential: nodes where `source_id === id` AND
67
+ * `source_type === type`. The fields are for
68
+ * external-import round-trip; self-references are
69
+ * redundant and should be dropped.
70
+ * 6. property_drift: properties on a node that should have been migrated
71
+ * (rename, drop, or lift to top-level). Powered by
72
+ * UPG_PROPERTY_MIGRATIONS once full property-migration
73
+ * coverage ships; today the count is driven by
74
+ * intra-`properties` rename rules only.
75
+ *
76
+ * Output is COUNTS ONLY. The full per-node breakdown lives in the
77
+ * `validate_graph` MCP tool.
78
+ */
79
+
80
+ interface SchemaDriftSummary {
81
+ entity_drift: number;
82
+ edge_drift: number;
83
+ top_level_drift: number;
84
+ lifecycle_drift: number;
85
+ self_referential: number;
86
+ property_drift: number;
87
+ total_nodes: number;
88
+ total_edges: number;
89
+ }
90
+ declare function computeSchemaDriftSummary(doc: UPGDocument): SchemaDriftSummary;
91
+ declare function renderDriftSummary(summary: SchemaDriftSummary, filePath?: string): string | null;
92
+
93
+ interface QuarantinedEntity {
94
+ id: string;
95
+ type: string;
96
+ title: string;
97
+ reason: string;
98
+ }
99
+ interface IntegrityReport {
100
+ /** Whether the file was modified outside the MCP server */
101
+ tampered: boolean;
102
+ /** Entities that failed schema validation after external modification */
103
+ quarantined: QuarantinedEntity[];
104
+ /** Edges removed because they reference quarantined or missing nodes */
105
+ orphanedEdges: number;
106
+ }
107
+ interface ChangeEntry {
108
+ action: 'create' | 'update' | 'delete';
109
+ entity: 'node' | 'edge';
110
+ id: string;
111
+ type: string;
112
+ title?: string;
113
+ timestamp: string;
114
+ }
115
+ interface MergeResult {
116
+ merged: boolean;
117
+ nodesAdded: number;
118
+ edgesAdded: number;
119
+ nodesFromDisk: number;
120
+ edgesFromDisk: number;
121
+ conflicts: Array<{
122
+ nodeId: string;
123
+ field: string;
124
+ ours: unknown;
125
+ theirs: unknown;
126
+ }>;
127
+ }
128
+ declare class UPGFileStore {
129
+ private doc;
130
+ private filePath;
131
+ private dirty;
132
+ private saveTimer;
133
+ private selfWriteInProgress;
134
+ private watcher;
135
+ private nodeMap;
136
+ private edgeMap;
137
+ private edgesByNode;
138
+ private changeLog;
139
+ private contentHash;
140
+ private baselineFileHash;
141
+ private baselineNodeIds;
142
+ private baselineEdgeIds;
143
+ private lastMergeResult;
144
+ private lastIntegrityReport;
145
+ private knownTypes;
146
+ private lastDanglingReport;
147
+ private lastDriftSummary;
148
+ /** Hash raw file bytes — used for baseline comparison (NOT the same as contentHash) */
149
+ private hashRawContent;
150
+ /** Compute integrity checksum over nodes + edges content (deterministic) */
151
+ private computeIntegrityChecksum;
152
+ /** Stamp the document with current integrity checksum */
153
+ private stampIntegrity;
154
+ /** Verify integrity and quarantine invalid entities if file was modified externally */
155
+ private verifyIntegrity;
156
+ getIntegrityReport(): IntegrityReport | null;
157
+ /** Snapshot current node/edge IDs as baseline for three-way merge */
158
+ private snapshotBaseline;
159
+ getLastMergeResult(): MergeResult | null;
160
+ load(filePath: string): Promise<void>;
161
+ /**
162
+ * Snapshot of the dangling-edge classification computed at load time.
163
+ * Returns null until `load()` has run.
164
+ */
165
+ getDanglingReport(): DanglingEdgeReport | null;
166
+ /**
167
+ * Snapshot of the schema-drift summary computed at load time.
168
+ * Counts only — full per-node breakdown is `validate_graph`.
169
+ * Returns null until `load()` has run.
170
+ */
171
+ getDriftSummary(): SchemaDriftSummary | null;
172
+ /**
173
+ * Drop edges matching the given dangling classes from the document. Used by
174
+ * the `repair_dangling_edges` tool with `dry_run: false`. Caller is
175
+ * responsible for choosing classes — this method does not protect
176
+ * `expected` cross-product edges by default; pass an empty array to no-op.
177
+ */
178
+ dropDanglingEdges(classes: ReadonlyArray<DanglingEdgeClass>): {
179
+ dropped: number;
180
+ remaining: DanglingEdgeReport;
181
+ };
182
+ save(): Promise<void>;
183
+ private mergeWithDisk;
184
+ flush(): Promise<void>;
185
+ /**
186
+ * Mark the store as dirty so the next `flush()` or `save()` writes to disk.
187
+ * Use this when an external caller mutates the document object directly (e.g.
188
+ * the cross-edge migration which modifies `doc.edges` in-place via
189
+ * `UPGPortfolioStore.migrateCrossEdgesFromDoc`).
190
+ */
191
+ markDirty(): void;
192
+ private scheduleSave;
193
+ private startWatching;
194
+ stopWatching(): void;
195
+ private rebuildIndexes;
196
+ private indexEdgeForNode;
197
+ private unindexEdgeForNode;
198
+ private computeHash;
199
+ getContentHash(): string;
200
+ getFilePath(): string;
201
+ getDocument(): UPGDocument;
202
+ getProduct(): _unified_product_graph_core.UPGProduct;
203
+ getNode(id: string): UPGBaseNode | undefined;
204
+ getEdge(id: string): UPGEdge | undefined;
205
+ getAllNodes(): UPGBaseNode[];
206
+ getAllEdges(): UPGEdge[];
207
+ getEdgesForNode(nodeId: string): UPGEdge[];
208
+ private logChange;
209
+ getChanges(since?: string): ChangeEntry[];
210
+ addNode(node: UPGBaseNode): void;
211
+ updateNode(id: string, patch: Partial<UPGBaseNode>): UPGBaseNode;
212
+ removeNode(id: string): {
213
+ node: UPGBaseNode;
214
+ removedEdgeIds: string[];
215
+ };
216
+ addEdge(edge: UPGEdge, skipValidation?: boolean): void;
217
+ removeEdge(id: string): UPGEdge;
218
+ migrateType(fromType: string, toType: string, defaults?: Record<string, unknown>): {
219
+ migratedNodes: number;
220
+ edgeRenames: Array<{
221
+ id: string;
222
+ from: string;
223
+ to: string;
224
+ flipped: boolean;
225
+ }>;
226
+ edgeDrops: Array<{
227
+ id: string;
228
+ from: string;
229
+ }>;
230
+ };
231
+ /**
232
+ * Exact-match rename of every edge whose `type === from` to `to`. Optionally
233
+ * flips `source`/`target` for each affected edge. The catalog is intentionally
234
+ * NOT consulted here — this is the low-level primitive backing
235
+ * `rename_edge_type`. Catalog awareness lives in the wrappers
236
+ * tracked separately.
237
+ *
238
+ * Returns the IDs of every edge that was actually mutated. The internal
239
+ * `edgesByNode` index is keyed by node id, so a flip does not require
240
+ * re-indexing — both endpoints are already tracked for the same edge id.
241
+ */
242
+ renameEdgeType(from: string, to: string, flip?: boolean): {
243
+ renamed: number;
244
+ ids: string[];
245
+ };
246
+ /**
247
+ * Apply every applicable rule from `UPG_EDGE_MIGRATIONS` (the v0.2.4
248
+ * canonical edge registry) to the loaded graph.
249
+ * Renames retarget the edge type; flipped renames swap source/target;
250
+ * dropped edges are removed entirely. Endpoint guards in the migration
251
+ * rules check post-migration node types — so callers should run any
252
+ * needed `migrateType` / `applySplit` pass on nodes BEFORE calling this.
253
+ *
254
+ * Wave 3 of the MCP edge-primitives cascade.
255
+ */
256
+ applyEdgeMigrations(fromVersion: string, toVersion: string): {
257
+ renamed: Array<{
258
+ id: string;
259
+ from: string;
260
+ to: string;
261
+ flipped: boolean;
262
+ }>;
263
+ dropped: Array<{
264
+ id: string;
265
+ from: string;
266
+ }>;
267
+ };
268
+ applyPropertyMigrations(fromVersion: string, toVersion: string): {
269
+ top_level_renames: Array<{
270
+ id: string;
271
+ from: string;
272
+ to: string;
273
+ value_changed: boolean;
274
+ }>;
275
+ lifted_properties: Array<{
276
+ id: string;
277
+ from_property: string;
278
+ to: string;
279
+ value_changed: boolean;
280
+ }>;
281
+ dropped_props: Array<{
282
+ id: string;
283
+ key: string;
284
+ }>;
285
+ dropped_self_referential: Array<{
286
+ id: string;
287
+ field: string;
288
+ }>;
289
+ };
290
+ }
291
+ interface PortfolioLoadResult {
292
+ /** Number of cross-product edges in the portfolio */
293
+ cross_edge_count: number;
294
+ /** Number of products listed in the portfolio */
295
+ product_count: number;
296
+ /** Path to the loaded portfolio file */
297
+ file_path: string;
298
+ }
299
+ interface CrossEdgeMigrationResult {
300
+ /** Cross-edges successfully migrated to portfolio format */
301
+ migrated: Array<{
302
+ id: string;
303
+ source: string;
304
+ target: string;
305
+ type: string;
306
+ source_product_id: string;
307
+ }>;
308
+ /** IDs of edges that could not be migrated (missing product context) */
309
+ skipped: Array<{
310
+ id: string;
311
+ reason: string;
312
+ }>;
313
+ /** Whether this was a dry run (no writes performed) */
314
+ dry_run: boolean;
315
+ }
316
+ declare class UPGPortfolioStore {
317
+ private doc;
318
+ private filePath;
319
+ private dirty;
320
+ private saveTimer;
321
+ /**
322
+ * Load an existing portfolio document from disk, or initialise an empty one
323
+ * at the given path if it does not exist.
324
+ *
325
+ * @param filePath Absolute path to the `.portfolio.upg` file.
326
+ * @param orgTitle Organisation title for newly created portfolios.
327
+ */
328
+ loadOrInit(filePath: string, orgTitle?: string): Promise<PortfolioLoadResult>;
329
+ /** Create a minimal valid UPGPortfolioDocument. */
330
+ private makeEmptyPortfolio;
331
+ /** Return the resolved portfolio file path, or null if not loaded. */
332
+ getFilePath(): string | null;
333
+ /** Return the loaded portfolio document, or null if not loaded. */
334
+ getDocument(): UPGPortfolioDocument | null;
335
+ /** True when a portfolio document is loaded and ready. */
336
+ isLoaded(): boolean;
337
+ /** Flush pending writes to disk. No-op if not dirty. */
338
+ flush(): Promise<void>;
339
+ /**
340
+ * Mark the portfolio store as dirty so the next `flush()` writes to disk.
341
+ * Use this when an external caller mutates the document object directly via
342
+ * `getDocument()` rather than going through `addCrossEdge` /
343
+ * `removeCrossEdge`. Mirrors `UPGFileStore.markDirty()`.
344
+ */
345
+ markDirty(): void;
346
+ private scheduleSave;
347
+ private writeToDisk;
348
+ /**
349
+ * Add a cross-product edge to the portfolio document. Both `source` and
350
+ * `target` must be qualified IDs (`{product_id}/{node_id}`).
351
+ */
352
+ addCrossEdge(edge: UPGCrossEdge): void;
353
+ /**
354
+ * Remove a cross-product edge by ID.
355
+ * @returns The removed edge, or null if not found.
356
+ */
357
+ removeCrossEdge(edgeId: string): UPGCrossEdge | null;
358
+ /** Return all cross-product edges. */
359
+ getAllCrossEdges(): UPGCrossEdge[];
360
+ /** Find a cross-product edge by ID. */
361
+ getCrossEdge(id: string): UPGCrossEdge | undefined;
362
+ /**
363
+ * Migrate inline cross-product edges from a product document into this
364
+ * portfolio document.
365
+ *
366
+ * **Does not flush** — caller is responsible for calling `.flush()` after
367
+ * inspecting the result and, for non-dry-run, also saving `sourceDoc`.
368
+ */
369
+ migrateCrossEdgesFromDoc(sourceDoc: UPGDocument, sourceProductId: string, targetProductId: string | null, dryRun: boolean): CrossEdgeMigrationResult;
370
+ }
371
+
372
+ interface EdgeAlias {
373
+ /** Original (often deprecated) type the caller passed */
374
+ from: string;
375
+ /** Canonical replacement resolved via @unified-product-graph/core's deprecation map */
376
+ to: string;
377
+ }
378
+ interface EdgeInferenceSuggestion {
379
+ source_type: string;
380
+ target_type: string;
381
+ edge_type: UPGEdgeType;
382
+ }
383
+ type EdgeInferenceOk = {
384
+ ok: true;
385
+ edgeType: UPGEdgeType;
386
+ tier: 'core' | 'extended';
387
+ /** Set when source and/or target were resolved via getReplacementType. */
388
+ aliased?: EdgeAlias[];
389
+ warning?: string;
390
+ };
391
+ type EdgeInferenceFail = {
392
+ ok: false;
393
+ reason: string;
394
+ /** Pairs that DO resolve in the catalog — best-effort near-misses. */
395
+ suggestions: EdgeInferenceSuggestion[];
396
+ /** Source/target after deprecation resolution, for diagnostics. */
397
+ resolved: {
398
+ source_type: string;
399
+ target_type: string;
400
+ };
401
+ };
402
+ type EdgeInferenceResult = EdgeInferenceOk | EdgeInferenceFail;
403
+ /**
404
+ * Infers the canonical edge type for a (sourceType, targetType) pair, or
405
+ * reports a structured failure when neither the raw nor alias-resolved pair
406
+ * appears in the catalog. Never fabricates a Tier-3 string.
407
+ */
408
+ declare function inferEdgeTypeWithTier(sourceType: string, targetType: string): EdgeInferenceResult;
409
+ /**
410
+ * Throwing variant. Suitable for code paths where a missing canonical edge is
411
+ * unrecoverable (the caller intends to write an edge and has no fallback).
412
+ * Throws an `InferEdgeTypeError` carrying the structured failure for callers
413
+ * to format into MCP error responses.
414
+ */
415
+ declare class InferEdgeTypeError extends Error {
416
+ readonly result: EdgeInferenceFail;
417
+ constructor(result: EdgeInferenceFail);
418
+ }
419
+ declare function inferEdgeType(sourceType: string, targetType: string): UPGEdgeType;
420
+
421
+ /**
422
+ * Shared edge-type-pair validation. Single source of truth for the question:
423
+ * "given an edge type X, are these source/target node types what the catalog
424
+ * declares?"
425
+ *
426
+ * Called from every write surface that takes an explicit `type` parameter
427
+ * (create_edge, batch_create_edges, move_node) and from validate_graph to
428
+ * flag the new `edge_type_pair_drift` class.
429
+ *
430
+ * Background: the 2026-05-20 adversarial spec audit (F1) confirmed that
431
+ * explicit-type writes bypassed catalog cross-checking entirely. An LLM could
432
+ * write `persona_pursues_job` between `vision → vulnerability` and
433
+ * `validate_graph` would report the graph as clean. This helper closes that
434
+ * gap.
435
+ */
436
+ interface EdgePairValidationOk {
437
+ valid: true;
438
+ }
439
+ interface EdgePairValidationFail {
440
+ valid: false;
441
+ reason: string;
442
+ expected: {
443
+ source: string;
444
+ target: string;
445
+ };
446
+ actual: {
447
+ source: string;
448
+ target: string;
449
+ };
450
+ }
451
+ type EdgePairValidationResult = EdgePairValidationOk | EdgePairValidationFail;
452
+ /**
453
+ * Verify that an explicit edge type's source/target node types match the
454
+ * catalog's declared `source_type` / `target_type`. Returns `{ valid: true }`
455
+ * on match. On mismatch, returns a structured failure with both the catalog
456
+ * expectation and the actual node types so the caller can render a single
457
+ * clear error message.
458
+ *
459
+ * Unknown edge types (not in `UPG_EDGE_CATALOG`) return `{ valid: true }` —
460
+ * the legacy / non-canonical edge surface is not this helper's concern.
461
+ * Callers that care about catalog membership should check that upstream.
462
+ */
463
+ declare function validateEdgeTypePair(edgeType: string, sourceNodeType: string, targetNodeType: string): EdgePairValidationResult;
464
+
465
+ /** Generate a prefixed node ID */
466
+ declare function nodeId(): string;
467
+ /** Generate a prefixed edge ID */
468
+ declare function edgeId(): string;
469
+ /** Generate a prefixed product ID (for `doc.product.id` in new .upg files) */
470
+ declare function productId(): string;
471
+
472
+ /**
473
+ * Local anti-pattern input collector.
474
+ *
475
+ * Walks the loaded UPGFileStore once and builds the `AntiPatternInputs`
476
+ * shape consumed by `evaluateAntiPatterns` from `@unified-product-graph/core`.
477
+ *
478
+ * Cloud has its own SQL-based collector (Phase 2 at `packages/upg-cloud-server/
479
+ * src/lib/anti-pattern-inputs.ts`). Different stores, same output shape.
480
+ */
481
+
482
+ /**
483
+ * Build the per-graph statistics consumed by `evaluateAntiPatterns`.
484
+ *
485
+ * Single linear pass over `getAllNodes()` plus one over `getAllEdges()`. No
486
+ * caching — the cost is bounded by graph size, and `validate_graph` is
487
+ * already O(nodes + edges) for schema drift.
488
+ *
489
+ * @param store The loaded UPGFileStore.
490
+ * @param productStage Active product stage (read from the document's
491
+ * `product.stage` by the caller). Used by stage-gated anti-patterns.
492
+ */
493
+ declare function collectAntiPatternInputs(store: UPGFileStore, productStage?: UPGProductStage): AntiPatternInputs;
494
+
495
+ /**
496
+ * Shared property-type validation. Single source of truth for the question:
497
+ * "given a declared property in UPG_PROPERTY_SCHEMA, does the value match its
498
+ * declared type?"
499
+ *
500
+ * Called from every write surface that takes a `properties` parameter
501
+ * (create_node, update_node, batch_update_nodes) and from validate_graph to
502
+ * flag the new `property_type_drift` class.
503
+ *
504
+ * Background: the 2026-05-20 adversarial spec audit (F4) confirmed that
505
+ * declared property type violations were silently accepted — writing
506
+ * `metric.target_value = "not_a_number_lol"` was stored verbatim despite the
507
+ * schema declaring `target_value: number`. This helper closes that gap.
508
+ *
509
+ * UNDECLARED properties are out of scope for this helper. The existing
510
+ * unknown-properties warning (checkUnknownProperties) handles those.
511
+ */
512
+
513
+ interface PropertyTypeViolation {
514
+ /** The property key with the wrong type. */
515
+ property: string;
516
+ /** The schema-declared expected type. */
517
+ expected_type: PropertyDefinition['type'];
518
+ /** The actual JavaScript typeof / shape descriptor. */
519
+ actual_type: string;
520
+ /** Free-text explanation. */
521
+ reason: string;
522
+ }
523
+ interface PropertyTypeCheckResult {
524
+ /** Violations against declared property types. Empty when no declared
525
+ * property mismatched — independent of whether undeclared properties
526
+ * exist (those are reported separately by checkUnknownProperties). */
527
+ violations: PropertyTypeViolation[];
528
+ }
529
+ /**
530
+ * Walk a node's properties bag and report violations against the type
531
+ * declared in `UPG_PROPERTY_SCHEMA`.
532
+ *
533
+ * Entity types with no registered schema are treated as fully permissive:
534
+ * no violations reported. Properties not present in the schema are skipped
535
+ * (unknown-properties is a separate concern).
536
+ *
537
+ * Null and undefined values are tolerated (not a violation). Arrays are
538
+ * special-cased for `string[]` schema entries.
539
+ */
540
+ declare function checkPropertyTypes(entityType: string, properties: Record<string, unknown> | undefined): PropertyTypeCheckResult;
541
+ /**
542
+ * Render violations as a single warning string for embedding in tool
543
+ * responses. Empty when there are no violations.
544
+ */
545
+ declare function renderPropertyTypeWarning(entityType: string, violations: PropertyTypeViolation[]): string | undefined;
546
+
547
+ /**
548
+ * Length / size guards for entity payloads. Soft warnings — not refusals.
549
+ *
550
+ * Background: the 2026-05-20 adversarial spec audit (F8) noted unbounded
551
+ * payload sizes on titles, descriptions, and property trees are a
552
+ * denial-of-service surface. We pick warnings (not refusals) to avoid
553
+ * breaking existing graphs that may already carry oversized payloads from
554
+ * earlier imports.
555
+ *
556
+ * Limits (deliberately generous):
557
+ * - title.length > 256 → warning
558
+ * - description.length > 16000 → warning
559
+ * - property tree depth > 8 OR total property tree JSON size > 64KB → warning
560
+ */
561
+ declare const TITLE_SOFT_LIMIT = 256;
562
+ declare const DESCRIPTION_SOFT_LIMIT = 16000;
563
+ declare const PROPERTY_TREE_SOFT_BYTES: number;
564
+ declare const PROPERTY_TREE_SOFT_DEPTH = 8;
565
+ interface LengthCheckResult {
566
+ warnings: string[];
567
+ }
568
+ /**
569
+ * Walk title / description / properties and return warning strings for each
570
+ * soft-limit breach. Empty `warnings` means everything is within limits.
571
+ */
572
+ declare function checkLengthCaps(args: {
573
+ title?: string;
574
+ description?: string;
575
+ properties?: Record<string, unknown>;
576
+ }): LengthCheckResult;
577
+
578
+ /**
579
+ * Safe arithmetic expression evaluator for framework `computed_properties`.
580
+ *
581
+ * Framework expressions are simple math over property names, e.g.
582
+ * `(reach * impact * confidence) / effort`
583
+ * `(user_value + time_criticality + risk_reduction) / job_size`
584
+ *
585
+ * This evaluator is intentionally narrow: NO `eval`, NO `new Function`, NO
586
+ * named functions. The grammar is:
587
+ *
588
+ * expr := term (('+' | '-') term)*
589
+ * term := factor (('*' | '/' | '%') factor)*
590
+ * factor := unary ('^' factor)?
591
+ * unary := '-' unary | primary
592
+ * primary := NUMBER | IDENTIFIER | '(' expr ')'
593
+ *
594
+ * Identifiers resolve against a `Record<string, number>` passed by the caller.
595
+ * Missing identifiers cause a typed failure result; division/modulo by zero
596
+ * also fail. Callers always check `ok` before reading `value`.
597
+ *
598
+ * The evaluator is pure: no globals, no I/O, no closures over the caller's
599
+ * scope. Same input always produces the same output.
600
+ */
601
+ type EvalResult = {
602
+ ok: true;
603
+ value: number;
604
+ } | {
605
+ ok: false;
606
+ error: string;
607
+ missing?: string[];
608
+ };
609
+ /**
610
+ * Evaluate an arithmetic expression with identifiers resolved against `scope`.
611
+ *
612
+ * Returns a typed success/failure result. When the expression references
613
+ * identifiers absent from `scope`, the returned `missing` array lists them.
614
+ *
615
+ * @example
616
+ * evaluateExpression('(a + b) * c', { a: 2, b: 3, c: 4 })
617
+ * // → { ok: true, value: 20 }
618
+ *
619
+ * @example
620
+ * evaluateExpression('a / b', { a: 1, b: 0 })
621
+ * // → { ok: false, error: 'Division by zero ...' }
622
+ *
623
+ * @example
624
+ * evaluateExpression('reach * impact', { reach: 5 })
625
+ * // → { ok: false, error: 'Missing variables: impact', missing: ['impact'] }
626
+ */
627
+ declare function evaluateExpression(expression: string, scope: Record<string, number>): EvalResult;
628
+
629
+ /**
630
+ * Resolver enrichment helpers — UPG-505 and UPG-515.
631
+ *
632
+ * When the canonical resolver returns `null` for a (source_type, target_type)
633
+ * pair, these helpers surface what the catalog DOES know about, so the failure
634
+ * boundary becomes a teaching moment instead of a dead end.
635
+ *
636
+ * Three enrichments:
637
+ *
638
+ * - `buildAnchorHint(source, target)` — UPG-505: when the target's domain has
639
+ * a canonical anchor entity that DIFFERS from the source, surface the
640
+ * anchor + the domain's creation sequence so the author can route via the
641
+ * correct entry point ("ideal_customer_profile is anchored in gtm_strategy
642
+ * — create one of those first").
643
+ *
644
+ * - `buildAlternateAnchors(source, target)` — UPG-515: catalog walk for
645
+ * edges where `target_type === requested_target` AND `source_type !==
646
+ * requested_source`. Surfaces the OTHER sources the catalog connects to
647
+ * this target. Sorted by classification: hierarchy > causal > semantic
648
+ * > cross-domain. Capped at 3.
649
+ *
650
+ * - `buildAdjacentEdges(source)` — UPG-515: catalog walk for edges where
651
+ * `source_type === requested_source`. Helps the author discover what they
652
+ * CAN reach from this source. Capped at 3.
653
+ *
654
+ * All three are pure functions of the catalog; no graph state required.
655
+ */
656
+ interface AnchorHint {
657
+ target_domain: string;
658
+ domain_anchor: string;
659
+ creation_sequence: readonly string[];
660
+ hint: string;
661
+ }
662
+ interface AlternateAnchor {
663
+ source_type: string;
664
+ edge_type: string;
665
+ hint: string;
666
+ }
667
+ interface AdjacentEdge {
668
+ source_type: string;
669
+ target_type: string;
670
+ edge_type: string;
671
+ }
672
+ /**
673
+ * UPG-505 — anchor hint.
674
+ *
675
+ * When the target type is anchored in a domain different from the source's
676
+ * domain, surface that anchor + the domain's creation sequence. Returns
677
+ * undefined when no useful hint can be constructed (target has no known
678
+ * domain, target's domain has no guide, or the target IS the anchor and the
679
+ * source is already in the same domain).
680
+ *
681
+ * The hint prose intentionally names the anchor explicitly so a first-time
682
+ * author reading the error sees what entity to create next.
683
+ */
684
+ declare function buildAnchorHint(sourceType: string, targetType: string): AnchorHint | undefined;
685
+ /**
686
+ * UPG-515 — alternate anchors.
687
+ *
688
+ * Walk `UPG_EDGE_CATALOG` for every edge whose `target_type` matches the
689
+ * requested target AND whose `source_type` differs from the requested source.
690
+ * Sort by classification rank (hierarchy → cross-domain) and cap at 3.
691
+ *
692
+ * This converts "no edge from `service` to `external_api`" into "but
693
+ * `bounded_context → external_api` is canonical."
694
+ *
695
+ * Deduplicates by source_type — when the catalog registers multiple edges
696
+ * from the same source to the same target (e.g. semantic + causal), the
697
+ * highest-ranked one wins.
698
+ */
699
+ declare function buildAlternateAnchors(sourceType: string, targetType: string): AlternateAnchor[];
700
+ /**
701
+ * UPG-515 — adjacent edges.
702
+ *
703
+ * Walk `UPG_EDGE_CATALOG` for every edge whose `source_type` matches the
704
+ * requested source. Surfaces what the author CAN reach from where they are
705
+ * standing. Sort by classification rank and cap at 3.
706
+ *
707
+ * Skips polymorphic edges (`source_type === '*'`) — those don't teach
708
+ * useful adjacency.
709
+ */
710
+ declare function buildAdjacentEdges(sourceType: string): AdjacentEdge[];
711
+ /**
712
+ * Convenience aggregator — emit every applicable enrichment block for a
713
+ * failed (source, target) pair. Used by `resolve_edge_for_pair` and
714
+ * `create_edge` / `batch_create_edges` error paths.
715
+ *
716
+ * Only includes fields that are non-empty / defined, so consumers don't
717
+ * have to filter undefined keys.
718
+ */
719
+ declare function buildResolverHints(sourceType: string, targetType: string): {
720
+ anchor_hint?: AnchorHint;
721
+ alternate_anchors?: AlternateAnchor[];
722
+ adjacent_edges?: AdjacentEdge[];
723
+ };
724
+
725
+ /**
726
+ * Framework categories and structure patterns.
727
+ *
728
+ * Format conventions:
729
+ * FrameworkCategory — broad discipline a framework belongs to. Grouped into
730
+ * six clusters: Core Product (prioritization, strategy, discovery,
731
+ * business_model, metrics, validation, planning, competitive); Design &
732
+ * Research (design, ux_research, user_understanding, research,
733
+ * accessibility, feedback_voc); Engineering & Ops (engineering, devops,
734
+ * security, qa_testing, ai_ml, agentic); Growth & GTM (growth, marketing,
735
+ * go_to_market, sales, pricing); Data/Legal/Ops (data_analytics,
736
+ * legal_compliance, customer_success, team_process, program_mgmt);
737
+ * Content/Portfolio (content, education, partnerships, localisation,
738
+ * portfolio).
739
+ * StructurePattern — visual topology of a framework's output:
740
+ * 'tree' | 'table' | 'matrix' | 'funnel' | 'collection' | 'quadrant' | 'flow'
741
+ */
742
+ /** The broad domain a framework belongs to */
743
+ type FrameworkCategory = 'prioritization' | 'strategy' | 'discovery' | 'business_model' | 'metrics' | 'validation' | 'planning' | 'competitive' | 'design' | 'ux_research' | 'user_understanding' | 'research' | 'accessibility' | 'feedback_voc' | 'engineering' | 'devops' | 'security' | 'qa_testing' | 'ai_ml' | 'agentic' | 'growth' | 'marketing' | 'go_to_market' | 'sales' | 'pricing' | 'data_analytics' | 'legal_compliance' | 'customer_success' | 'team_process' | 'program_mgmt' | 'content' | 'education' | 'partnerships' | 'localisation' | 'portfolio';
744
+ /** The visual / topological shape a framework's structure takes */
745
+ type StructurePattern = 'tree' | 'table' | 'matrix' | 'funnel' | 'collection' | 'quadrant' | 'flow';
746
+
747
+ /**
748
+ * UPG v0.2 — Framework Type Definitions
749
+ *
750
+ * Frameworks are config-driven lenses that structure UPG graph data into
751
+ * well-known product management patterns (RICE, Lean Canvas, Opportunity
752
+ * Solution Tree, etc.). Each framework is declarative JSON — no code —
753
+ * describing four layers: data, structure, presentation, and education.
754
+ *
755
+ * To sequence multiple frameworks into a guided journey, use a
756
+ * `UPGWorkflow` (with `kind: 'framework'` steps) from
757
+ * `@unified-product-graph/core`.
758
+ *
759
+ * Zero dependencies — works in any JavaScript environment.
760
+ *
761
+ * https://unifiedproductgraph.org/spec
762
+ * License: MIT
763
+ */
764
+
765
+ /** A named position within a framework's visual structure, populated by an entity type */
766
+ interface FrameworkSlot {
767
+ /** Display label for this slot (e.g. "Key Partners", "Problem", "Reach") */
768
+ label: string;
769
+ /** The UPG entity type that fills this slot */
770
+ entityTypeId: string;
771
+ /** Explanation of what this slot represents in the framework */
772
+ description?: string;
773
+ }
774
+ /** Where the framework came from — attribution and licensing */
775
+ interface FrameworkOrigin {
776
+ /** Whether the framework is from academia, a practitioner, the community, or original to UPG */
777
+ type: 'academic' | 'practitioner' | 'community' | 'custom';
778
+ /** Human-readable attribution (e.g. "Sean Ellis", "Marty Cagan", "Teresa Torres") */
779
+ attribution?: string;
780
+ /** Provenance narrative — how the framework came about (e.g. "Published in Business Model Generation") */
781
+ description?: string;
782
+ /** URL to the original source or publication */
783
+ url?: string;
784
+ /** Year the framework was first published or popularised */
785
+ year?: number;
786
+ /** License under which the framework definition is shared */
787
+ license?: string;
788
+ }
789
+ /** Declares which UPG entity type plays which role within a framework */
790
+ interface FrameworkEntityTypeSpec {
791
+ /** The UPG entity type (must be a valid UPGEntityType) */
792
+ type: string;
793
+ /** The role this entity plays in the framework (e.g. "root", "item", "branch", "leaf", "bucket") */
794
+ role: string;
795
+ /** Minimum number of entities of this type required */
796
+ min_count?: number;
797
+ /** Maximum number of entities of this type allowed */
798
+ max_count?: number;
799
+ /** Whether to auto-create placeholder entities when the framework is applied */
800
+ auto_scaffold?: boolean;
801
+ }
802
+ /** A property that the framework requires on an entity */
803
+ interface FrameworkPropertyRequirement {
804
+ /** The property key on the entity's properties object */
805
+ property: string;
806
+ /** The data type of the property value */
807
+ type: 'number' | 'string' | 'enum' | 'boolean' | 'assessment';
808
+ /** Whether the property must be filled for the framework to function */
809
+ required: boolean;
810
+ /** Default value to use when the property is not set */
811
+ default_value?: unknown;
812
+ /** Valid values when type is 'enum' */
813
+ enum_values?: string[];
814
+ /** Human-readable label for the property (shown in UI) */
815
+ label?: string;
816
+ /** Explanation of what this property represents */
817
+ description?: string;
818
+ }
819
+ /** A property whose value is computed from other properties via a math DSL */
820
+ interface FrameworkComputedProperty {
821
+ /** The property key that will hold the computed result */
822
+ property: string;
823
+ /** A simple math expression referencing other properties (e.g. "(reach * impact * confidence) / effort") */
824
+ expression: string;
825
+ /** The entity type this computed property applies to */
826
+ entity_type: string;
827
+ /** Human-readable label for the computed property */
828
+ label?: string;
829
+ /** How to format the computed value in the UI */
830
+ format?: 'number' | 'percentage' | 'currency';
831
+ }
832
+ /** A fixed entity that the framework scaffolds automatically (e.g. quadrant labels, funnel stages) */
833
+ interface FrameworkConstant {
834
+ /** The UPG entity type for this constant */
835
+ type: string;
836
+ /** Display title for the constant */
837
+ title: string;
838
+ /** Predefined properties for the constant entity */
839
+ properties: Record<string, unknown>;
840
+ }
841
+ /**
842
+ * Everything the framework needs from the graph's data layer — which entity
843
+ * types play which roles, which properties each type must carry under the
844
+ * lens, and any computed or scaffolded constants.
845
+ *
846
+ * @example
847
+ * // RICE — scores a feature on reach, impact, confidence, effort.
848
+ * const riceData: FrameworkDataSpec = {
849
+ * entity_types: [
850
+ * { type: 'feature', role: 'item', min_count: 1 },
851
+ * ],
852
+ * required_properties: {
853
+ * feature: [
854
+ * { property: 'reach', type: 'number', required: true, label: 'Reach' },
855
+ * { property: 'impact', type: 'number', required: true, label: 'Impact' },
856
+ * { property: 'confidence', type: 'number', required: true, label: 'Confidence (%)' },
857
+ * { property: 'effort', type: 'number', required: true, label: 'Effort (person-weeks)' },
858
+ * ],
859
+ * },
860
+ * computed_properties: [
861
+ * {
862
+ * property: 'rice_score',
863
+ * entity_type: 'feature',
864
+ * expression: '(reach * impact * confidence) / effort',
865
+ * label: 'RICE',
866
+ * format: 'number',
867
+ * },
868
+ * ],
869
+ * }
870
+ */
871
+ interface FrameworkDataSpec {
872
+ /** The entity types that participate in this framework and their roles */
873
+ entity_types: FrameworkEntityTypeSpec[];
874
+ /**
875
+ * Properties required on each entity type, keyed by entity type string.
876
+ *
877
+ * **Framework-introduced properties.** A framework may declare
878
+ * property keys that do NOT appear in `UPG_PROPERTY_SCHEMA` for the target
879
+ * entity type. This is by design: frameworks are lenses that layer
880
+ * domain-specific fields on top of canonical entities (RICE adds
881
+ * `reach`/`impact`/`confidence`/`effort` to a feature; Kano adds
882
+ * `functional_response`/`dysfunctional_response`). These fields are NOT
883
+ * universal to the entity — they live inside the framework context.
884
+ *
885
+ * Consumers must read framework-introduced properties from this spec, not
886
+ * from `UPG_PROPERTY_SCHEMA`. Renderers merge both when displaying an
887
+ * entity under a framework. See `src/ARCHITECTURE.md` —
888
+ * "Framework Properties — Lens-Scoped Fields".
889
+ */
890
+ required_properties: Record<string, FrameworkPropertyRequirement[]>;
891
+ /** Properties that are derived from other properties via expressions */
892
+ computed_properties?: FrameworkComputedProperty[];
893
+ /** Fixed entities that the framework creates automatically */
894
+ constants?: FrameworkConstant[];
895
+ }
896
+ /** One level in a tree-structured framework */
897
+ interface FrameworkLevel {
898
+ /** Zero-based depth in the tree (0 = root) */
899
+ depth: number;
900
+ /** Human-readable name for this level (e.g. "Outcome", "Opportunity") */
901
+ label: string;
902
+ /** Which UPG entity types can appear at this level */
903
+ entity_types: string[];
904
+ /** Explanation of what this level represents */
905
+ description: string;
906
+ /** The edge type connecting entities at this level to their parent */
907
+ edge_from_parent: string;
908
+ }
909
+ /** A cell in a matrix-structured framework */
910
+ interface MatrixSlot {
911
+ /** Unique identifier for this slot */
912
+ id: string;
913
+ /** Display label for the slot */
914
+ label: string;
915
+ /** The UPG entity type placed in this slot */
916
+ entity_type: string;
917
+ /** Position within the matrix grid */
918
+ position: {
919
+ /** Zero-based row index */
920
+ row: number;
921
+ /** Zero-based column index */
922
+ col: number;
923
+ /** Number of rows this slot spans */
924
+ rowSpan?: number;
925
+ /** Number of columns this slot spans */
926
+ colSpan?: number;
927
+ };
928
+ }
929
+ /** A stage in a funnel-structured framework */
930
+ interface FunnelStage {
931
+ /** Unique identifier for this stage */
932
+ id: string;
933
+ /** Display label for the stage */
934
+ label: string;
935
+ /** Position in the funnel (0 = top / widest) */
936
+ order: number;
937
+ /** The UPG entity type associated with this stage */
938
+ entity_type?: string;
939
+ /** The metric tracked at this stage (e.g. "visitors", "signups") */
940
+ metric_name?: string;
941
+ }
942
+ /** A logical grouping of entities within a collection-structured framework */
943
+ interface NamedGroup {
944
+ /** Unique identifier for this group */
945
+ id: string;
946
+ /** Display label for the group */
947
+ label: string;
948
+ /** Explanation of what this group contains */
949
+ description: string;
950
+ /** Which UPG entity types belong to this group */
951
+ entity_types: string[];
952
+ }
953
+ /** How entities are topologically organised within the framework */
954
+ interface FrameworkStructureSpec {
955
+ /** The visual / topological pattern (tree, table, matrix, etc.) */
956
+ pattern: StructurePattern;
957
+ /** Tree levels — only used when pattern is 'tree' */
958
+ levels?: FrameworkLevel[];
959
+ /** Edge types used to connect entities in this framework */
960
+ edge_types?: string[];
961
+ /** Matrix slots — only used when pattern is 'matrix' */
962
+ slots?: MatrixSlot[];
963
+ /** Funnel stages — only used when pattern is 'funnel' */
964
+ stages?: FunnelStage[];
965
+ /** Named groups — only used when pattern is 'collection' */
966
+ groups?: NamedGroup[];
967
+ }
968
+ /** A column definition for table-layout frameworks */
969
+ interface TableColumn {
970
+ /** The property key to display in this column */
971
+ property: string;
972
+ /** Column header label */
973
+ label: string;
974
+ /** Column width in pixels (optional) */
975
+ width?: number;
976
+ /** Whether the column can be sorted */
977
+ sortable?: boolean;
978
+ /** How to render the cell value */
979
+ format?: 'number' | 'bar' | 'badge' | 'score_pill';
980
+ }
981
+ /**
982
+ * Discriminated union of all supported layout types.
983
+ * Each layout carries its own configuration fields.
984
+ *
985
+ * @example { type: 'tree', direction: 'TB', engine: 'elk' }
986
+ * @example { type: 'table', columns: [{ property: 'reach', label: 'Reach', format: 'score_pill' }] }
987
+ * @example { type: 'matrix', rows: 2, cols: 2, template: 'eisenhower' }
988
+ * @example { type: 'funnel', orientation: 'vertical' }
989
+ * @example { type: 'kanban', columns: ['backlog', 'in_progress', 'done'] }
990
+ * @example { type: 'quadrant', x_axis: 'impact', y_axis: 'effort', x_label: 'Impact', y_label: 'Effort' }
991
+ * @example { type: 'grid', groupBy: 'domain' }
992
+ * @example { type: 'flow', direction: 'LR' }
993
+ */
994
+ type FrameworkLayout = {
995
+ type: 'tree'; /** Layout direction */
996
+ direction: 'TB' | 'LR'; /** Layout engine */
997
+ engine?: 'dagre' | 'elk';
998
+ } | {
999
+ type: 'table'; /** Column definitions */
1000
+ columns: TableColumn[];
1001
+ } | {
1002
+ type: 'matrix'; /** Number of rows */
1003
+ rows: number; /** Number of columns */
1004
+ cols: number; /** Optional named template */
1005
+ template?: string;
1006
+ } | {
1007
+ type: 'funnel'; /** Funnel orientation */
1008
+ orientation: 'vertical' | 'horizontal';
1009
+ } | {
1010
+ type: 'kanban'; /** Column identifiers */
1011
+ columns: string[];
1012
+ } | {
1013
+ type: 'quadrant'; /** X-axis property */
1014
+ x_axis: string; /** Y-axis property */
1015
+ y_axis: string; /** X-axis label */
1016
+ x_label?: string; /** Y-axis label */
1017
+ y_label?: string;
1018
+ } | {
1019
+ type: 'grid'; /** Property to group entities by */
1020
+ groupBy: string;
1021
+ } | {
1022
+ type: 'flow'; /** Flow direction */
1023
+ direction: 'LR' | 'TB';
1024
+ };
1025
+ /** How the framework should be rendered in a UI */
1026
+ interface FrameworkPresentationSpec {
1027
+ /** The layout strategy for rendering */
1028
+ layout: FrameworkLayout;
1029
+ /** Default sort order for entities */
1030
+ sort_by?: {
1031
+ property: string; /** Sort direction */
1032
+ direction: 'asc' | 'desc';
1033
+ };
1034
+ /** Which dimension to use for colour coding */
1035
+ colour_by?: 'type' | 'status' | 'score' | 'group' | 'custom';
1036
+ /** Which properties to show on entity cards */
1037
+ card_fields?: string[];
1038
+ /** Whether tree branches can be collapsed */
1039
+ collapsible?: boolean;
1040
+ /** Custom colour map — keys are values of the colour_by dimension, values are CSS colours */
1041
+ colour_map?: Record<string, string>;
1042
+ }
1043
+ /** One step in a guided walkthrough of how to use a framework */
1044
+ interface FrameworkStep {
1045
+ /** Step order (1-based) */
1046
+ order: number;
1047
+ /** Human-readable instruction for this step */
1048
+ instruction: string;
1049
+ /** The property this step asks the user to fill */
1050
+ property?: string;
1051
+ /** The entity type this step focuses on */
1052
+ entity_type?: string;
1053
+ }
1054
+ /** Educational context that helps users understand and apply the framework */
1055
+ interface FrameworkEducation {
1056
+ /** A one-sentence explanation of what the framework does */
1057
+ purpose: string;
1058
+ /** The core question the framework helps answer */
1059
+ core_question: string;
1060
+ /** Situations where this framework is a good fit */
1061
+ when_to_use: string[];
1062
+ /** Situations where this framework is a poor fit */
1063
+ when_not_to_use: string[];
1064
+ /** URL to further reading about the framework */
1065
+ learn_more_url?: string;
1066
+ /** Step-by-step guided walkthrough */
1067
+ steps?: FrameworkStep[];
1068
+ }
1069
+ /**
1070
+ * A UPG Framework is a declarative, config-driven lens that structures
1071
+ * UPG graph data into a well-known product management pattern.
1072
+ *
1073
+ * Frameworks are pure data — no code. The rendering engine reads the
1074
+ * framework definition and produces the appropriate UI.
1075
+ *
1076
+ * @example
1077
+ * // Abbreviated RICE definition — shows the layer split (data / structure
1078
+ * // / presentation / education). Real definitions live in
1079
+ * // `src/frameworks/definitions/`.
1080
+ * const riceFramework: UPGFramework = {
1081
+ * id: 'rice-scoring',
1082
+ * name: 'RICE Scoring',
1083
+ * version: '0.1.0',
1084
+ * description: 'Score features on Reach, Impact, Confidence, Effort to prioritise.',
1085
+ * category: 'prioritisation',
1086
+ * origin: {
1087
+ * type: 'practitioner',
1088
+ * attribution: 'Sean McBride (Intercom)',
1089
+ * year: 2016,
1090
+ * license: 'MIT',
1091
+ * },
1092
+ * tags: ['prioritisation', 'scoring'],
1093
+ * data: {
1094
+ * entity_types: [{ type: 'feature', role: 'item', min_count: 1 }],
1095
+ * required_properties: {
1096
+ * feature: [
1097
+ * { property: 'reach', type: 'number', required: true },
1098
+ * { property: 'impact', type: 'number', required: true },
1099
+ * { property: 'confidence', type: 'number', required: true },
1100
+ * { property: 'effort', type: 'number', required: true },
1101
+ * ],
1102
+ * },
1103
+ * },
1104
+ * structure: { pattern: 'table' },
1105
+ * presentation: {
1106
+ * layout: {
1107
+ * type: 'table',
1108
+ * columns: [
1109
+ * { property: 'title', label: 'Feature' },
1110
+ * { property: 'rice_score', label: 'RICE', format: 'score_pill' },
1111
+ * ],
1112
+ * },
1113
+ * sort_by: { property: 'rice_score', direction: 'desc' },
1114
+ * },
1115
+ * education: {
1116
+ * purpose: 'Rank features by a simple weighted score.',
1117
+ * core_question: 'Which feature gives the most value for the least effort?',
1118
+ * when_to_use: ['A large backlog needs triage', 'Stakeholders argue by intuition'],
1119
+ * when_not_to_use: ['Strategic bets where effort is the wrong denominator'],
1120
+ * },
1121
+ * }
1122
+ */
1123
+ interface UPGFramework {
1124
+ /** Unique identifier (kebab-case, e.g. "rice-scoring", "lean-canvas") */
1125
+ id: string;
1126
+ /** Human-readable name (e.g. "RICE Scoring", "Lean Canvas") */
1127
+ name: string;
1128
+ /** Semver version of this framework definition */
1129
+ version: string;
1130
+ /** One-sentence description of the framework */
1131
+ description: string;
1132
+ /** The broad domain this framework belongs to */
1133
+ category: FrameworkCategory;
1134
+ /** Attribution and licensing */
1135
+ origin: FrameworkOrigin;
1136
+ /** Freeform tags for filtering and discovery */
1137
+ tags: string[];
1138
+ /** Named positions within the framework's visual structure */
1139
+ slots?: FrameworkSlot[];
1140
+ /** What data the framework needs from the graph */
1141
+ data: FrameworkDataSpec;
1142
+ /** How entities are topologically organised */
1143
+ structure: FrameworkStructureSpec;
1144
+ /** How the framework should be rendered */
1145
+ presentation: FrameworkPresentationSpec;
1146
+ /** Educational context and guided walkthrough */
1147
+ education: FrameworkEducation;
1148
+ /** IDs of other frameworks this one can be composed with */
1149
+ composable_with?: string[];
1150
+ /** ID of a parent framework this one extends */
1151
+ extends?: string;
1152
+ /**
1153
+ * The approaches this framework serves — many-to-many. Each value is a
1154
+ * `UPGApproachId` (`'plan' | 'inspect' | 'prioritise' | 'trace' | 'reflect'`).
1155
+ * Optional — partial coverage is by design; adding tags later is additive.
1156
+ *
1157
+ * Type-erased to `string[]` to avoid a circular import with
1158
+ * `approaches/types.ts`. Conceptually `readonly UPGApproachId[]`.
1159
+ *
1160
+ * @see MENTAL-MODEL.md — approach × framework relationship
1161
+ */
1162
+ approach_ids?: readonly string[];
1163
+ }
1164
+
1165
+ /**
1166
+ * Approach execution helpers — pure functions powering the five approach
1167
+ * verb tools (`plan`, `inspect`, `prioritise`, `trace`, `reflect`).
1168
+ *
1169
+ * Each helper consumes the live graph (via `UPGFileStore`) plus the spec
1170
+ * catalog and emits a concrete structured projection. The verb handlers in
1171
+ * `tools/spec.ts` are thin wrappers that validate inputs, call into here,
1172
+ * and wrap the result in the family-resemblance envelope.
1173
+ *
1174
+ * Why a separate module:
1175
+ * - Keeps `tools/spec.ts` slim — handler authoring stays declarative.
1176
+ * - Lets the executors be unit-tested directly without the MCP envelope.
1177
+ * - Provides a clear contract: every function returns the structured
1178
+ * projection the verb's `signature_hint` documents.
1179
+ */
1180
+
1181
+ interface PrioritiseRankedRow {
1182
+ entity_id: string;
1183
+ /** Numeric score; null when the formula could not be evaluated. */
1184
+ score: number | null;
1185
+ /** Human-readable explanation: formula + substituted values or failure reason. */
1186
+ rationale: string;
1187
+ /** When score is null, the missing property keys that prevented evaluation. */
1188
+ missing_properties?: string[];
1189
+ }
1190
+ interface PrioritiseExecutionResult {
1191
+ kind: 'execution';
1192
+ ranked: PrioritiseRankedRow[];
1193
+ framework_used: {
1194
+ id: string;
1195
+ name: string;
1196
+ category: string;
1197
+ expression: string;
1198
+ };
1199
+ /** Properties the formula consumed (variable identifiers). */
1200
+ required_properties: string[];
1201
+ }
1202
+ interface PrioritiseFallbackResult {
1203
+ kind: 'fallback';
1204
+ framework_used: {
1205
+ id: string;
1206
+ name: string;
1207
+ category: string;
1208
+ };
1209
+ /** Why we couldn't execute (no computed expression, etc.). */
1210
+ hint: string;
1211
+ }
1212
+ /**
1213
+ * Execute a framework's first numeric `computed_properties` expression over a
1214
+ * candidate set. Returns either ranked rows (when the framework defines an
1215
+ * expression) or a fallback envelope (when it doesn't — caller should
1216
+ * surface the framework's slots / classification as LLM substrate).
1217
+ *
1218
+ * Resolution per candidate:
1219
+ * - Look up the node in the store; missing nodes get `score: null` with a
1220
+ * "node not found" rationale.
1221
+ * - Resolve each identifier in the expression as a numeric property:
1222
+ * `node.properties[key]` (coerced via Number()). Top-level numeric fields
1223
+ * like `node.title` are NOT consulted — formulas address property keys.
1224
+ * - Evaluate via `evaluateExpression`; on success append to ranked with the
1225
+ * computed score; on failure record the missing variables.
1226
+ *
1227
+ * Sort: numeric desc, with nulls last (preserves input order among null rows).
1228
+ */
1229
+ declare function executePrioritise(framework: UPGFramework, candidateIds: string[], store: UPGFileStore): PrioritiseExecutionResult | PrioritiseFallbackResult;
1230
+ interface MissingEntityRow {
1231
+ entity_type: string;
1232
+ domain: string | null;
1233
+ /** Index in the domain guide's `creation_sequence` (lower = earlier). */
1234
+ position_in_sequence: number;
1235
+ /** Anchor entity type for the domain (commonly the typical parent). */
1236
+ typical_parent_type: string | null;
1237
+ hint: string;
1238
+ }
1239
+ interface PlanResult {
1240
+ missing_entities: MissingEntityRow[];
1241
+ /** Ratio of covered entity types to expected entity types. */
1242
+ coverage_score: number;
1243
+ /** Total expected entity types in scope. */
1244
+ expected_count: number;
1245
+ /** Number of expected types with at least one instance in the graph. */
1246
+ covered_count: number;
1247
+ /** Region narrowed to, if any. */
1248
+ region: string | null;
1249
+ }
1250
+ /**
1251
+ * Compute a missing-entity backlog from the graph's current coverage against
1252
+ * the canonical creation sequences.
1253
+ *
1254
+ * When `region` is provided, the expected set is that region's entity
1255
+ * memberships. When omitted, the expected set is every type listed across all
1256
+ * domain guides' creation sequences (the union — that's the "whole-graph"
1257
+ * planning surface).
1258
+ *
1259
+ * Ordering: missing entities are sorted by position in their domain's
1260
+ * `creation_sequence` (earlier types surface first) so the agent always sees
1261
+ * the foundational gaps before the late-stage ones.
1262
+ */
1263
+ declare function executePlan(store: UPGFileStore, regionId?: string): PlanResult;
1264
+ interface InspectViolation {
1265
+ severity: 'high' | 'medium' | 'low' | 'unknown';
1266
+ kind: 'anti_pattern' | 'entity_drift' | 'edge_drift' | 'lifecycle_drift' | 'property_drift' | 'self_referential' | 'top_level_drift';
1267
+ entity_id?: string;
1268
+ description: string;
1269
+ fix_hint: string;
1270
+ /** Source id (anti-pattern id, migration via, etc.) for traceability. */
1271
+ source?: string;
1272
+ }
1273
+ interface InspectResult {
1274
+ violations: InspectViolation[];
1275
+ scope: {
1276
+ region: string | null;
1277
+ entities: string[] | null;
1278
+ };
1279
+ /** Counts of violation kinds for quick triage. */
1280
+ summary: Record<string, number>;
1281
+ }
1282
+ /**
1283
+ * Wrap `validate_graph` + `get_anti_pattern_violations_for` into a single
1284
+ * inspect projection: a flat, severity-ordered violations array plus a kind
1285
+ * count map. The scope filter narrows by region (drop violations whose target
1286
+ * type isn't in the region) or by entities (drop violations whose
1287
+ * `target_entities` don't include any node-of-interest's type, and drift rows
1288
+ * whose id isn't in the candidate set).
1289
+ */
1290
+ declare function executeInspect(validateResult: ValidateGraphBody, scope: {
1291
+ region?: string;
1292
+ entities?: string[];
1293
+ }): InspectResult;
1294
+ interface ValidateGraphBody {
1295
+ anti_pattern_violations?: Array<{
1296
+ anti_pattern_id: string;
1297
+ name: string;
1298
+ severity: 'high' | 'medium' | 'low';
1299
+ target_entities: string[];
1300
+ description?: string;
1301
+ why_it_matters?: string;
1302
+ remediation?: string;
1303
+ }>;
1304
+ entity_drift?: Array<{
1305
+ id: string;
1306
+ type: string;
1307
+ suggested_migration?: {
1308
+ kind: string;
1309
+ to?: string | string[];
1310
+ via?: string;
1311
+ };
1312
+ }>;
1313
+ edge_drift?: Array<{
1314
+ id: string;
1315
+ type: string;
1316
+ source: string;
1317
+ target: string;
1318
+ suggested_migration?: {
1319
+ kind: string;
1320
+ to?: string;
1321
+ via?: string;
1322
+ flip?: boolean;
1323
+ };
1324
+ }>;
1325
+ lifecycle_drift?: Array<{
1326
+ id: string;
1327
+ type: string;
1328
+ status: string;
1329
+ valid_phases: string[];
1330
+ }>;
1331
+ property_drift?: Array<{
1332
+ id: string;
1333
+ type: string;
1334
+ property: string;
1335
+ via: string;
1336
+ }>;
1337
+ self_referential?: Array<{
1338
+ id: string;
1339
+ fields: string[];
1340
+ }>;
1341
+ top_level_drift?: Array<{
1342
+ id: string;
1343
+ type: string;
1344
+ unknown_fields: string[];
1345
+ }>;
1346
+ }
1347
+ interface TraceTrailRow {
1348
+ depth: number;
1349
+ entity_id: string;
1350
+ entity_type: string;
1351
+ edge_type_in: string | null;
1352
+ }
1353
+ interface TraceResult {
1354
+ trail: TraceTrailRow[];
1355
+ reached: string[];
1356
+ /** Set when traversal halts early — e.g. no canonical edge for a hop. */
1357
+ error?: string;
1358
+ /** Depth at which the trace halted, if it stopped early. */
1359
+ halted_at_depth?: number;
1360
+ }
1361
+ /**
1362
+ * Walk a typed path starting from `anchor`. Each step in `path` is an entity
1363
+ * type; the walker chooses the canonical edge for the previous-type →
1364
+ * current-type pair (via `resolveContainmentEdge`) unless `edgesOverride[i]`
1365
+ * is set to a non-null string.
1366
+ *
1367
+ * Strategy is breadth-first per depth: at depth N+1 we walk every outgoing
1368
+ * edge of the resolved type whose target node matches `path[N+1]`. The trail
1369
+ * carries every reached node at every depth.
1370
+ *
1371
+ * Halts when no canonical edge can be resolved for a hop AND no override is
1372
+ * supplied — returning a partial trail + `error` + `halted_at_depth`. This
1373
+ * gives the caller enough signal to either supply an override or rewrite
1374
+ * the path.
1375
+ */
1376
+ declare function executeTrace(store: UPGFileStore, anchor: string, path: string[], edgesOverride?: (string | null)[]): TraceResult;
1377
+ type ReflectPromptKind = 'assumption' | 'alternative' | 'blind_spot' | 'load_bearing';
1378
+ interface ReflectPrompt {
1379
+ kind: ReflectPromptKind;
1380
+ question: string;
1381
+ target_entities?: string[];
1382
+ }
1383
+ interface ReflectResult {
1384
+ prompts: ReflectPrompt[];
1385
+ mode: string | null;
1386
+ scope: string | null;
1387
+ }
1388
+ /**
1389
+ * Emit structured reflection prompts based on graph topology + mode.
1390
+ *
1391
+ * - "assumptions" — find entities of type `assumption` or drafted hypotheses;
1392
+ * one prompt per entity asks the author to surface evidence / falsification.
1393
+ * - "alternatives" — find parents with multiple siblings of the same type and
1394
+ * prompt "did you consider…" alternatives.
1395
+ * - "blind-spots" — find atomic domains with zero entities and prompt
1396
+ * the author to explain why that surface is empty.
1397
+ * - "load-bearing" — find entities with the highest incoming-edge counts and
1398
+ * prompt "if this changes, what depends on it?"
1399
+ * - No mode (open) — pick the most informative single category based on the
1400
+ * graph's actual state; never empty unless the graph itself is empty.
1401
+ *
1402
+ * Prompts are capped at a sensible per-mode limit so the response stays
1403
+ * digestible — the agent calls again with a tighter mode if it wants more.
1404
+ */
1405
+ declare function executeReflect(store: UPGFileStore, mode?: string, scope?: string | null): ReflectResult;
1406
+
1407
+ /**
1408
+ * UPG entity classification — three axes used by skills to determine
1409
+ * relevance:
1410
+ *
1411
+ * 1. **Business Area**: what question does this entity help answer?
1412
+ * 2. **Tier**: what stage of company maturity needs this?
1413
+ * 3. **Tier Cluster**: which capability group within team/scaleup tiers?
1414
+ *
1415
+ * Canonical reference; import from here rather than duplicating mappings.
1416
+ * Entity types are defined in `@unified-product-graph/core`. Use
1417
+ * `validateClassificationCoverage()` to detect drift between the spec and
1418
+ * this map.
1419
+ */
1420
+ type BusinessArea = 'identity' | 'understanding' | 'discovery' | 'reaching' | 'converting' | 'building' | 'sustaining' | 'learning';
1421
+ type Tier = 'solo' | 'team' | 'scaleup' | 'enterprise';
1422
+ type TierCluster = 'team_coordination' | 'design_alignment' | 'user_signal' | 'growth_operations' | 'ecosystem_partnerships' | 'program_management';
1423
+ declare const BUSINESS_AREA_META: Record<BusinessArea, {
1424
+ label: string;
1425
+ question: string;
1426
+ emoji: string;
1427
+ }>;
1428
+ declare const TIER_DESCRIPTIONS: Record<Tier, {
1429
+ label: string;
1430
+ description: string;
1431
+ }>;
1432
+ /** Tier entity counts — computed from ENTITY_CLASSIFICATION so they never drift */
1433
+ declare function getTierEntityCount(tier: Tier): number;
1434
+ declare const TIER_CLUSTER_META: Record<TierCluster, {
1435
+ label: string;
1436
+ description: string;
1437
+ tier: Tier;
1438
+ }>;
1439
+ interface EntityClassification {
1440
+ business_area: BusinessArea;
1441
+ tier: Tier;
1442
+ cluster?: TierCluster;
1443
+ }
1444
+ declare const ENTITY_CLASSIFICATION: Record<string, EntityClassification>;
1445
+ /** Get the classification for an entity type (falls back to enterprise/building for unknown types) */
1446
+ declare function getClassification(entityType: string): EntityClassification;
1447
+ /** Get all entity types assigned to a specific tier (exact match, not cumulative) */
1448
+ declare function getEntitiesForTier(tier: Tier): string[];
1449
+ /** Get all entity types assigned to a specific business area */
1450
+ declare function getEntitiesForBusinessArea(area: BusinessArea): string[];
1451
+ /** Get all entity types for a tier AND business area intersection */
1452
+ declare function getEntitiesForTierAndArea(tier: Tier, area: BusinessArea): string[];
1453
+ /** Get all entity types in a specific tier cluster */
1454
+ declare function getEntitiesForCluster(cluster: TierCluster): string[];
1455
+ /**
1456
+ * Map a product stage to the appropriate tier.
1457
+ * Products at earlier stages need fewer entity types.
1458
+ */
1459
+ declare function getTierForStage(stage: string): Tier;
1460
+ /**
1461
+ * Get all entity types available at a given tier (cumulative).
1462
+ * solo includes only solo. team includes solo + team. etc.
1463
+ */
1464
+ declare function getEntitiesUpToTier(tier: Tier): string[];
1465
+ /**
1466
+ * Check whether an entity type is relevant for a given product stage.
1467
+ * A type is relevant if its tier is at or below the tier for the stage.
1468
+ */
1469
+ declare function isRelevantForStage(entityType: string, stage: string): boolean;
1470
+ /**
1471
+ * Validates that ENTITY_CLASSIFICATION covers every type in @unified-product-graph/core.
1472
+ * Run at dev time or in tests to catch drift early.
1473
+ * Returns { missing, extra } — both should be empty for a healthy sync.
1474
+ */
1475
+ declare function validateClassificationCoverage(): {
1476
+ missing: string[];
1477
+ extra: string[];
1478
+ };
1479
+
1480
+ export { buildAlternateAnchors as $, type AdjacentEdge as A, BUSINESS_AREA_META as B, type ChangeEntry as C, DESCRIPTION_SOFT_LIMIT as D, ENTITY_CLASSIFICATION as E, type PrioritiseRankedRow as F, type PropertyTypeCheckResult as G, type PropertyTypeViolation as H, type IntegrityReport as I, type ReflectPromptKind as J, type ReflectResult as K, type LengthCheckResult as L, type MergeResult as M, TIER_DESCRIPTIONS as N, TITLE_SOFT_LIMIT as O, PROPERTY_TREE_SOFT_BYTES as P, type QuarantinedEntity as Q, type ReflectPrompt as R, type SchemaDriftSummary as S, TIER_CLUSTER_META as T, UPGFileStore as U, type Tier as V, type TierCluster as W, type TraceResult as X, type TraceTrailRow as Y, type ValidateGraphBody as Z, buildAdjacentEdges as _, UPGPortfolioStore as a, buildAnchorHint as a0, buildResolverHints as a1, checkLengthCaps as a2, checkPropertyTypes as a3, classifyDanglingEdges as a4, collectAntiPatternInputs as a5, computeSchemaDriftSummary as a6, edgeId as a7, evaluateExpression as a8, executeInspect as a9, executePlan as aa, executePrioritise as ab, executeReflect as ac, executeTrace as ad, getClassification as ae, getEntitiesForBusinessArea as af, getEntitiesForCluster as ag, getEntitiesForTier as ah, getEntitiesForTierAndArea as ai, getEntitiesUpToTier as aj, getTierEntityCount as ak, getTierForStage as al, inferEdgeType as am, inferEdgeTypeWithTier as an, isRelevantForStage as ao, nodeId as ap, productId as aq, renderDanglingReport as ar, renderDriftSummary as as, renderPropertyTypeWarning as at, validateClassificationCoverage as au, validateEdgeTypePair as av, type AlternateAnchor as b, type AnchorHint as c, type BusinessArea as d, type CrossEdgeMigrationResult as e, type DanglingEdgeClass as f, type DanglingEdgeRecord as g, type DanglingEdgeReport as h, type EdgeAlias as i, type EdgeInferenceFail as j, type EdgeInferenceOk as k, type EdgeInferenceResult as l, type EdgeInferenceSuggestion as m, type EdgePairValidationFail as n, type EdgePairValidationOk as o, type EdgePairValidationResult as p, type EvalResult as q, InferEdgeTypeError as r, type InspectResult as s, type InspectViolation as t, type MissingEntityRow as u, PROPERTY_TREE_SOFT_DEPTH as v, type PlanResult as w, type PortfolioLoadResult as x, type PrioritiseExecutionResult as y, type PrioritiseFallbackResult as z };