@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.
- package/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/chunk-ARDXTSGG.js +20904 -0
- package/dist/chunk-ARDXTSGG.js.map +1 -0
- package/dist/index.d.ts +678 -0
- package/dist/index.js +2732 -0
- package/dist/index.js.map +1 -0
- package/dist/logic-DriXyFKi.d.ts +1480 -0
- package/dist/logic.d.ts +2 -0
- package/dist/logic.js +95 -0
- package/dist/logic.js.map +1 -0
- package/package.json +60 -0
|
@@ -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 };
|