@unified-product-graph/core 0.8.6 → 0.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -24,6 +24,7 @@ __export(index_exports, {
24
24
  BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK: () => BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK,
25
25
  BUSINESS_GTM_GROWTH_PLAYBOOK: () => BUSINESS_GTM_GROWTH_PLAYBOOK,
26
26
  BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK: () => BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK,
27
+ CONTENT_DEPTH_WARNING_RULES: () => CONTENT_DEPTH_WARNING_RULES,
27
28
  DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK: () => DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK,
28
29
  ENGINEERING_PLATFORM_PLAYBOOK: () => ENGINEERING_PLATFORM_PLAYBOOK,
29
30
  EXPERIENCE_DESIGN_BRAND_PLAYBOOK: () => EXPERIENCE_DESIGN_BRAND_PLAYBOOK,
@@ -8666,209 +8667,6 @@ function rotateSlug(node, next) {
8666
8667
  return node;
8667
8668
  }
8668
8669
 
8669
- // src/grammar/validate.ts
8670
- var crossProductEdgeTypeSet = new Set(UPG_CROSS_EDGE_TYPES);
8671
- function validateUPGDocument(doc) {
8672
- const errors = [];
8673
- const warnings = [];
8674
- if (!doc || typeof doc !== "object") {
8675
- return { valid: false, errors: [{ path: "$", message: "Document must be an object" }], warnings };
8676
- }
8677
- const d = doc;
8678
- if (!d.upg_version || typeof d.upg_version !== "string") {
8679
- errors.push({ path: "$.upg_version", message: "upg_version is required and must be a string" });
8680
- }
8681
- if (!d.exported_at || typeof d.exported_at !== "string") {
8682
- errors.push({ path: "$.exported_at", message: "exported_at is required and must be an ISO 8601 string" });
8683
- }
8684
- if (!d.source || typeof d.source !== "object") {
8685
- errors.push({ path: "$.source", message: "source is required and must be an object" });
8686
- } else {
8687
- const source = d.source;
8688
- if (!source.tool || typeof source.tool !== "string") {
8689
- errors.push({ path: "$.source.tool", message: "source.tool is required and must be a string" });
8690
- }
8691
- }
8692
- if (!d.product || typeof d.product !== "object") {
8693
- errors.push({ path: "$.product", message: "product is required and must be an object" });
8694
- } else {
8695
- const product = d.product;
8696
- if (!product.id || typeof product.id !== "string") {
8697
- errors.push({ path: "$.product.id", message: "product.id is required and must be a string" });
8698
- }
8699
- if (!product.title || typeof product.title !== "string") {
8700
- errors.push({ path: "$.product.title", message: "product.title is required and must be a string" });
8701
- }
8702
- }
8703
- const allKnownTypes = new Set(getTypes());
8704
- const allKnownEdgeTypes = new Set(Object.keys(UPG_EDGE_CATALOG));
8705
- if (!Array.isArray(d.nodes)) {
8706
- errors.push({ path: "$.nodes", message: "nodes is required and must be an array" });
8707
- } else {
8708
- const nodeIds = /* @__PURE__ */ new Set();
8709
- const nodesById = /* @__PURE__ */ new Map();
8710
- d.nodes.forEach((node, i) => {
8711
- const path = `$.nodes[${i}]`;
8712
- if (!node || typeof node !== "object") {
8713
- errors.push({ path, message: "Each node must be an object" });
8714
- return;
8715
- }
8716
- const n = node;
8717
- if (!n.id || typeof n.id !== "string") {
8718
- errors.push({ path: `${path}.id`, message: "Node id is required and must be a string" });
8719
- } else {
8720
- if (nodeIds.has(n.id)) {
8721
- errors.push({ path: `${path}.id`, message: `Duplicate node id: ${n.id}` });
8722
- }
8723
- nodeIds.add(n.id);
8724
- nodesById.set(n.id, n);
8725
- }
8726
- if (!n.type || typeof n.type !== "string") {
8727
- errors.push({ path: `${path}.type`, message: "Node type is required and must be a string" });
8728
- } else if (!allKnownTypes.has(n.type)) {
8729
- warnings.push({ path: `${path}.type`, message: `Unknown UPG type: "${n.type}". Node will be preserved with its original type.` });
8730
- }
8731
- if (!n.title || typeof n.title !== "string") {
8732
- errors.push({ path: `${path}.title`, message: "Node title is required and must be a string" });
8733
- }
8734
- });
8735
- if (!Array.isArray(d.edges)) {
8736
- errors.push({ path: "$.edges", message: "edges is required and must be an array" });
8737
- } else {
8738
- d.edges.forEach((edge, i) => {
8739
- const path = `$.edges[${i}]`;
8740
- if (!edge || typeof edge !== "object") {
8741
- errors.push({ path, message: "Each edge must be an object" });
8742
- return;
8743
- }
8744
- const e = edge;
8745
- if (!e.id || typeof e.id !== "string") {
8746
- errors.push({ path: `${path}.id`, message: "Edge id is required and must be a string" });
8747
- }
8748
- if (typeof e.type === "string" && crossProductEdgeTypeSet.has(e.type)) {
8749
- errors.push({
8750
- path: `${path}.type`,
8751
- message: `Cross-product edge type "${e.type}" must live in portfolio.cross_edges[], not product edges[].`
8752
- });
8753
- return;
8754
- }
8755
- if (!e.source || typeof e.source !== "string") {
8756
- errors.push({ path: `${path}.source`, message: "Edge source is required and must be a string" });
8757
- } else if (!nodeIds.has(e.source)) {
8758
- errors.push({ path: `${path}.source`, message: `Edge source references unknown node id: ${e.source}` });
8759
- }
8760
- if (!e.target || typeof e.target !== "string") {
8761
- errors.push({ path: `${path}.target`, message: "Edge target is required and must be a string" });
8762
- } else if (!nodeIds.has(e.target)) {
8763
- errors.push({ path: `${path}.target`, message: `Edge target references unknown node id: ${e.target}` });
8764
- }
8765
- if (!e.type || typeof e.type !== "string") {
8766
- errors.push({ path: `${path}.type`, message: "Edge type is required and must be a string" });
8767
- } else if (!allKnownEdgeTypes.has(e.type)) {
8768
- warnings.push({
8769
- path: `${path}.type`,
8770
- message: `Unknown edge type: "${e.type}". Edge preserved but not in the edge registry.`
8771
- });
8772
- }
8773
- });
8774
- const valueToAxis = /* @__PURE__ */ new Map();
8775
- const edgesArr = d.edges;
8776
- for (const edge of edgesArr) {
8777
- if (!edge || typeof edge !== "object") continue;
8778
- const e = edge;
8779
- if (e.type === "classification_axis_includes_classification_value" && typeof e.source === "string" && typeof e.target === "string") {
8780
- valueToAxis.set(e.target, e.source);
8781
- }
8782
- }
8783
- for (const [id, n] of nodesById.entries()) {
8784
- if (n.type !== "classification_value") continue;
8785
- if (valueToAxis.has(id)) continue;
8786
- const pid = n.parent_id;
8787
- if (typeof pid === "string") {
8788
- const parent = nodesById.get(pid);
8789
- if (parent && parent.type === "classification_axis") {
8790
- valueToAxis.set(id, pid);
8791
- }
8792
- }
8793
- }
8794
- const axesPerAnalysis = /* @__PURE__ */ new Map();
8795
- for (const edge of edgesArr) {
8796
- if (!edge || typeof edge !== "object") continue;
8797
- const e = edge;
8798
- if (e.type === "competitive_analysis_dimensioned_by_classification_axis" && typeof e.source === "string") {
8799
- axesPerAnalysis.set(e.source, (axesPerAnalysis.get(e.source) ?? 0) + 1);
8800
- }
8801
- }
8802
- d.nodes.forEach((node, i) => {
8803
- if (!node || typeof node !== "object") return;
8804
- const n = node;
8805
- if (n.type !== "competitive_analysis") return;
8806
- const props = n.properties;
8807
- if (!props || typeof props !== "object") return;
8808
- const empty = props.empty_cells;
8809
- if (!Array.isArray(empty)) return;
8810
- const axisCount = axesPerAnalysis.get(n.id ?? "") ?? 0;
8811
- if (axisCount !== 2) {
8812
- warnings.push({
8813
- path: `$.nodes[${i}].properties.empty_cells`,
8814
- message: `empty_cells is meaningful only on a 2-axis competitive_analysis; this node is dimensioned by ${axisCount} classification_axis ${axisCount === 1 ? "child" : "children"}.`
8815
- });
8816
- }
8817
- empty.forEach((cell, j) => {
8818
- const cpath = `$.nodes[${i}].properties.empty_cells[${j}]`;
8819
- if (!cell || typeof cell !== "object") {
8820
- errors.push({ path: cpath, message: "empty_cells entry must be an object" });
8821
- return;
8822
- }
8823
- const c = cell;
8824
- const aRef = c.axis_a_value_ref;
8825
- const bRef = c.axis_b_value_ref;
8826
- if (typeof aRef !== "string") {
8827
- errors.push({ path: `${cpath}.axis_a_value_ref`, message: "axis_a_value_ref is required and must be a string" });
8828
- }
8829
- if (typeof bRef !== "string") {
8830
- errors.push({ path: `${cpath}.axis_b_value_ref`, message: "axis_b_value_ref is required and must be a string" });
8831
- }
8832
- if (typeof aRef !== "string" || typeof bRef !== "string") return;
8833
- const aNode = nodesById.get(aRef);
8834
- const bNode = nodesById.get(bRef);
8835
- if (!aNode) {
8836
- errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Unknown node id: ${aRef}` });
8837
- } else if (aNode.type !== "classification_value") {
8838
- errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Expected classification_value, found "${String(aNode.type)}" at ${aRef}` });
8839
- }
8840
- if (!bNode) {
8841
- errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Unknown node id: ${bRef}` });
8842
- } else if (bNode.type !== "classification_value") {
8843
- errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Expected classification_value, found "${String(bNode.type)}" at ${bRef}` });
8844
- }
8845
- if (aNode && bNode && aNode.type === "classification_value" && bNode.type === "classification_value") {
8846
- const aAxis = valueToAxis.get(aRef);
8847
- const bAxis = valueToAxis.get(bRef);
8848
- if (aAxis && bAxis && aAxis === bAxis) {
8849
- errors.push({
8850
- path: cpath,
8851
- message: `empty_cells references two classification_values from the same axis (${aAxis}); axis_a and axis_b must come from distinct classification_axis parents.`
8852
- });
8853
- }
8854
- }
8855
- const kind = c.rationale_kind;
8856
- if (kind !== void 0 && kind !== "structural" && kind !== "ideological" && kind !== "opportunity") {
8857
- errors.push({
8858
- path: `${cpath}.rationale_kind`,
8859
- message: `rationale_kind must be one of 'structural' | 'ideological' | 'opportunity'; got "${String(kind)}".`
8860
- });
8861
- }
8862
- });
8863
- });
8864
- }
8865
- }
8866
- return { valid: errors.length === 0, errors, warnings };
8867
- }
8868
- function isUPGDocument(doc) {
8869
- return validateUPGDocument(doc).valid;
8870
- }
8871
-
8872
8670
  // src/properties/property-schema.ts
8873
8671
  var UPG_PROPERTY_SCHEMA = {
8874
8672
  // A11yAnnotationProperties: Accessibility annotation.
@@ -12245,31 +12043,37 @@ var UPG_FRAMEWORKS = [
12245
12043
  "slots": [
12246
12044
  {
12247
12045
  "label": "Customer Jobs",
12046
+ "role": "customer_job",
12248
12047
  "entityTypeId": "job",
12249
12048
  "description": "Place job entities in the Customer Jobs position of the matrix"
12250
12049
  },
12251
12050
  {
12252
12051
  "label": "Pains",
12052
+ "role": "pain",
12253
12053
  "entityTypeId": "need",
12254
12054
  "description": "Place need entities in the Pains position of the matrix"
12255
12055
  },
12256
12056
  {
12257
12057
  "label": "Gains",
12058
+ "role": "gain",
12258
12059
  "entityTypeId": "desired_outcome",
12259
12060
  "description": "Place desired outcome entities in the Gains position of the matrix"
12260
12061
  },
12261
12062
  {
12262
12063
  "label": "Products & Services",
12064
+ "role": "product_or_service",
12263
12065
  "entityTypeId": "feature",
12264
12066
  "description": "Place feature entities in the Products & Services position of the matrix"
12265
12067
  },
12266
12068
  {
12267
12069
  "label": "Pain Relievers",
12070
+ "role": "pain_reliever",
12268
12071
  "entityTypeId": "feature",
12269
12072
  "description": "Place feature entities in the Pain Relievers position of the matrix"
12270
12073
  },
12271
12074
  {
12272
12075
  "label": "Gain Creators",
12076
+ "role": "gain_creator",
12273
12077
  "entityTypeId": "feature",
12274
12078
  "description": "Place feature entities in the Gain Creators position of the matrix"
12275
12079
  }
@@ -12898,26 +12702,31 @@ var UPG_FRAMEWORKS = [
12898
12702
  "slots": [
12899
12703
  {
12900
12704
  "label": "Competitive Rivalry",
12705
+ "role": "competitive_rivalry",
12901
12706
  "entityTypeId": "competitor",
12902
12707
  "description": "Intensity of competition among existing players"
12903
12708
  },
12904
12709
  {
12905
12710
  "label": "Threat of New Entrants",
12711
+ "role": "new_entrants",
12906
12712
  "entityTypeId": "competitor",
12907
12713
  "description": "How easy is it for new competitors to enter?"
12908
12714
  },
12909
12715
  {
12910
12716
  "label": "Threat of Substitutes",
12717
+ "role": "substitutes",
12911
12718
  "entityTypeId": "competitor",
12912
12719
  "description": "Can customers switch to alternatives?"
12913
12720
  },
12914
12721
  {
12915
12722
  "label": "Buyer Power",
12723
+ "role": "buyer_power",
12916
12724
  "entityTypeId": "persona",
12917
12725
  "description": "How much leverage do buyers have?"
12918
12726
  },
12919
12727
  {
12920
12728
  "label": "Supplier Power",
12729
+ "role": "supplier_power",
12921
12730
  "entityTypeId": "persona",
12922
12731
  "description": "How much leverage do suppliers have?"
12923
12732
  }
@@ -13085,31 +12894,37 @@ var UPG_FRAMEWORKS = [
13085
12894
  "slots": [
13086
12895
  {
13087
12896
  "label": "Inbound Logistics",
12897
+ "role": "inbound_logistics",
13088
12898
  "entityTypeId": "key_activity",
13089
12899
  "description": "Inbound Logistics phase: key activity entities move through this stage"
13090
12900
  },
13091
12901
  {
13092
12902
  "label": "Operations",
12903
+ "role": "operations",
13093
12904
  "entityTypeId": "key_activity",
13094
12905
  "description": "Operations phase: key activity entities move through this stage"
13095
12906
  },
13096
12907
  {
13097
12908
  "label": "Outbound Logistics",
12909
+ "role": "outbound_logistics",
13098
12910
  "entityTypeId": "key_activity",
13099
12911
  "description": "Outbound Logistics phase: key activity entities move through this stage"
13100
12912
  },
13101
12913
  {
13102
12914
  "label": "Marketing & Sales",
12915
+ "role": "marketing_and_sales",
13103
12916
  "entityTypeId": "key_activity",
13104
12917
  "description": "Marketing & Sales phase: key activity entities move through this stage"
13105
12918
  },
13106
12919
  {
13107
12920
  "label": "Service",
12921
+ "role": "service",
13108
12922
  "entityTypeId": "key_activity",
13109
12923
  "description": "Service phase: key activity entities move through this stage"
13110
12924
  },
13111
12925
  {
13112
12926
  "label": "Support Activities",
12927
+ "role": "support_activity",
13113
12928
  "entityTypeId": "capability",
13114
12929
  "description": "Support Activities phase: capability entities move through this stage"
13115
12930
  }
@@ -13180,26 +12995,31 @@ var UPG_FRAMEWORKS = [
13180
12995
  "slots": [
13181
12996
  {
13182
12997
  "label": "Portfolio",
12998
+ "role": "portfolio",
13183
12999
  "entityTypeId": "portfolio",
13184
13000
  "description": "Innovation portfolio balanced across three horizons: core performance, emerging growth, and future creation"
13185
13001
  },
13186
13002
  {
13187
13003
  "label": "Product",
13004
+ "role": "product",
13188
13005
  "entityTypeId": "product",
13189
13006
  "description": "Product or business unit assigned to a horizon: H1 (defend and extend), H2 (build), or H3 (seed and explore)"
13190
13007
  },
13191
13008
  {
13192
13009
  "label": "Initiative",
13010
+ "role": "initiative",
13193
13011
  "entityTypeId": "portfolio",
13194
13012
  "description": "Growth initiative positioned in a horizon, with appropriate funding model, governance, and success metrics"
13195
13013
  },
13196
13014
  {
13197
13015
  "label": "Product Area",
13016
+ "role": "product_area",
13198
13017
  "entityTypeId": "product_area",
13199
13018
  "description": "Product area representing a horizon: core products (H1), adjacent expansions (H2), or experimental ventures (H3)"
13200
13019
  },
13201
13020
  {
13202
13021
  "label": "Capability",
13022
+ "role": "capability",
13203
13023
  "entityTypeId": "capability",
13204
13024
  "description": "Capability needed to execute across horizons: H1 needs efficiency, H2 needs scaling, H3 needs discovery"
13205
13025
  }
@@ -13263,7 +13083,7 @@ var UPG_FRAMEWORKS = [
13263
13083
  ],
13264
13084
  "name": "RICE Scoring",
13265
13085
  "version": "1.0.0",
13266
- "description": "Score features and opportunities by Reach, Impact, Confidence, and Effort to produce a ranked priority list.",
13086
+ "description": "Score features, opportunities, and needs by Reach, Impact, Confidence, and Effort to produce a ranked priority list.",
13267
13087
  "category": "prioritization",
13268
13088
  "origin": {
13269
13089
  "type": "practitioner",
@@ -13281,7 +13101,7 @@ var UPG_FRAMEWORKS = [
13281
13101
  {
13282
13102
  "label": "Items to score",
13283
13103
  "entityTypeId": "feature",
13284
- "description": "Features, opportunities, or solutions being evaluated"
13104
+ "description": "Features, opportunities, or needs being evaluated"
13285
13105
  },
13286
13106
  {
13287
13107
  "label": "Opportunities to score",
@@ -13447,7 +13267,60 @@ var UPG_FRAMEWORKS = [
13447
13267
  "label": "RICE Score",
13448
13268
  "format": "number"
13449
13269
  }
13450
- ]
13270
+ ],
13271
+ "scoring_lens": {
13272
+ "applies_to": [
13273
+ "feature",
13274
+ "opportunity",
13275
+ "need"
13276
+ ],
13277
+ "inputs": [
13278
+ {
13279
+ "property": "reach",
13280
+ "type": "assessment",
13281
+ "scale_id": "reach_5",
13282
+ "required": true,
13283
+ "scope": "framework",
13284
+ "label": "Reach",
13285
+ "description": "How many users will this impact per quarter?"
13286
+ },
13287
+ {
13288
+ "property": "impact",
13289
+ "type": "assessment",
13290
+ "scale_id": "impact_5",
13291
+ "required": true,
13292
+ "scope": "framework",
13293
+ "label": "Impact",
13294
+ "description": "How much will this impact each user, on the impact scale?"
13295
+ },
13296
+ {
13297
+ "property": "confidence",
13298
+ "type": "assessment",
13299
+ "scale_id": "confidence_5",
13300
+ "required": true,
13301
+ "scope": "framework",
13302
+ "label": "Confidence",
13303
+ "description": "How confident are you in the reach, impact, and effort estimates?"
13304
+ },
13305
+ {
13306
+ "property": "effort",
13307
+ "type": "assessment",
13308
+ "scale_id": "effort_5",
13309
+ "required": true,
13310
+ "scope": "framework",
13311
+ "label": "Effort",
13312
+ "description": "How much work is required to build and ship this, on the effort scale?"
13313
+ }
13314
+ ],
13315
+ "computed": [
13316
+ {
13317
+ "property": "rice_score",
13318
+ "expression": "(reach * impact * confidence) / effort",
13319
+ "label": "RICE Score",
13320
+ "format": "number"
13321
+ }
13322
+ ]
13323
+ }
13451
13324
  },
13452
13325
  "structure": {
13453
13326
  "pattern": "table"
@@ -13626,21 +13499,25 @@ var UPG_FRAMEWORKS = [
13626
13499
  "slots": [
13627
13500
  {
13628
13501
  "label": "Must-haves",
13502
+ "role": "must_have",
13629
13503
  "entityTypeId": "feature",
13630
13504
  "description": "Expected features: absence causes dissatisfaction"
13631
13505
  },
13632
13506
  {
13633
13507
  "label": "Performance",
13508
+ "role": "performance",
13634
13509
  "entityTypeId": "feature",
13635
13510
  "description": "More is better: linear satisfaction increase"
13636
13511
  },
13637
13512
  {
13638
13513
  "label": "Delighters",
13514
+ "role": "delighter",
13639
13515
  "entityTypeId": "feature",
13640
13516
  "description": "Unexpected features: presence creates delight"
13641
13517
  },
13642
13518
  {
13643
13519
  "label": "Indifferent",
13520
+ "role": "indifferent",
13644
13521
  "entityTypeId": "feature",
13645
13522
  "description": "Features users don't care about either way"
13646
13523
  }
@@ -13649,7 +13526,7 @@ var UPG_FRAMEWORKS = [
13649
13526
  "entity_types": [
13650
13527
  {
13651
13528
  "type": "feature",
13652
- "role": "item"
13529
+ "role": "scored_item"
13653
13530
  }
13654
13531
  ],
13655
13532
  "required_properties": {
@@ -13733,12 +13610,95 @@ var UPG_FRAMEWORKS = [
13733
13610
  "label": "Dissatisfaction Coefficient",
13734
13611
  "format": "number"
13735
13612
  }
13736
- ]
13737
- },
13738
- "structure": {
13739
- "pattern": "matrix"
13740
- },
13741
- "presentation": {
13613
+ ],
13614
+ "scoring_lens": {
13615
+ "applies_to": [
13616
+ "feature"
13617
+ ],
13618
+ "inputs": [
13619
+ {
13620
+ "property": "functional_response",
13621
+ "type": "enum",
13622
+ "required": true,
13623
+ "scope": "framework",
13624
+ "label": "Functional Response",
13625
+ "description": "How users feel when the feature IS present",
13626
+ "enum_values": [
13627
+ "i_like_it",
13628
+ "i_expect_it",
13629
+ "i_am_neutral",
13630
+ "i_can_tolerate_it",
13631
+ "i_dislike_it"
13632
+ ]
13633
+ },
13634
+ {
13635
+ "property": "dysfunctional_response",
13636
+ "type": "enum",
13637
+ "required": true,
13638
+ "scope": "framework",
13639
+ "label": "Dysfunctional Response",
13640
+ "description": "How users feel when the feature IS NOT present",
13641
+ "enum_values": [
13642
+ "i_like_it",
13643
+ "i_expect_it",
13644
+ "i_am_neutral",
13645
+ "i_can_tolerate_it",
13646
+ "i_dislike_it"
13647
+ ]
13648
+ },
13649
+ {
13650
+ "property": "delighter_count",
13651
+ "type": "number",
13652
+ "required": false,
13653
+ "scope": "framework",
13654
+ "label": "Delighter classifications",
13655
+ "description": "Count of survey responses classifying this feature as a delighter (attractive)"
13656
+ },
13657
+ {
13658
+ "property": "performance_count",
13659
+ "type": "number",
13660
+ "required": false,
13661
+ "scope": "framework",
13662
+ "label": "Performance classifications",
13663
+ "description": "Count of survey responses classifying this feature as performance (one-dimensional)"
13664
+ },
13665
+ {
13666
+ "property": "must_be_count",
13667
+ "type": "number",
13668
+ "required": false,
13669
+ "scope": "framework",
13670
+ "label": "Must-be classifications",
13671
+ "description": "Count of survey responses classifying this feature as must-be (basic)"
13672
+ },
13673
+ {
13674
+ "property": "indifferent_count",
13675
+ "type": "number",
13676
+ "required": false,
13677
+ "scope": "framework",
13678
+ "label": "Indifferent classifications",
13679
+ "description": "Count of survey responses classifying this feature as indifferent"
13680
+ }
13681
+ ],
13682
+ "computed": [
13683
+ {
13684
+ "property": "satisfaction_coefficient",
13685
+ "expression": "(delighter_count + performance_count) / (delighter_count + performance_count + must_be_count + indifferent_count)",
13686
+ "label": "Satisfaction Coefficient",
13687
+ "format": "number"
13688
+ },
13689
+ {
13690
+ "property": "dissatisfaction_coefficient",
13691
+ "expression": "(must_be_count + performance_count) / (delighter_count + performance_count + must_be_count + indifferent_count) * -1",
13692
+ "label": "Dissatisfaction Coefficient",
13693
+ "format": "number"
13694
+ }
13695
+ ]
13696
+ }
13697
+ },
13698
+ "structure": {
13699
+ "pattern": "matrix"
13700
+ },
13701
+ "presentation": {
13742
13702
  "layout": {
13743
13703
  "type": "matrix",
13744
13704
  "rows": 5,
@@ -13788,16 +13748,19 @@ var UPG_FRAMEWORKS = [
13788
13748
  "slots": [
13789
13749
  {
13790
13750
  "label": "Now",
13751
+ "role": "now",
13791
13752
  "entityTypeId": "feature",
13792
13753
  "description": "Committed work in progress"
13793
13754
  },
13794
13755
  {
13795
13756
  "label": "Next",
13757
+ "role": "next",
13796
13758
  "entityTypeId": "feature",
13797
13759
  "description": "High-confidence upcoming work"
13798
13760
  },
13799
13761
  {
13800
13762
  "label": "Later",
13763
+ "role": "later",
13801
13764
  "entityTypeId": "initiative",
13802
13765
  "description": "Exploratory, needs more discovery"
13803
13766
  }
@@ -14313,26 +14276,31 @@ var UPG_FRAMEWORKS = [
14313
14276
  "slots": [
14314
14277
  {
14315
14278
  "label": "Atoms",
14279
+ "role": "atom",
14316
14280
  "entityTypeId": "design_component",
14317
14281
  "description": "Atoms: design component entities for this dimension of the framework"
14318
14282
  },
14319
14283
  {
14320
14284
  "label": "Molecules",
14285
+ "role": "molecule",
14321
14286
  "entityTypeId": "design_component",
14322
14287
  "description": "Molecules: design token entities for this dimension of the framework"
14323
14288
  },
14324
14289
  {
14325
14290
  "label": "Organisms",
14291
+ "role": "organism",
14326
14292
  "entityTypeId": "design_pattern",
14327
14293
  "description": "Organisms: design pattern entities for this dimension of the framework"
14328
14294
  },
14329
14295
  {
14330
14296
  "label": "Templates",
14297
+ "role": "template",
14331
14298
  "entityTypeId": "wireframe",
14332
14299
  "description": "Templates: design system entities for this dimension of the framework"
14333
14300
  },
14334
14301
  {
14335
14302
  "label": "Pages",
14303
+ "role": "page",
14336
14304
  "entityTypeId": "screen",
14337
14305
  "description": "Pages: screen entities for this dimension of the framework"
14338
14306
  }
@@ -14997,26 +14965,31 @@ var UPG_FRAMEWORKS = [
14997
14965
  "slots": [
14998
14966
  {
14999
14967
  "label": "Outer Ring",
14968
+ "role": "outer_ring",
15000
14969
  "entityTypeId": "acquisition_channel",
15001
14970
  "description": "All 19 traction channels brainstormed"
15002
14971
  },
15003
14972
  {
15004
14973
  "label": "Middle Ring",
14974
+ "role": "middle_ring",
15005
14975
  "entityTypeId": "acquisition_channel",
15006
14976
  "description": "Top 6 channels worth testing"
15007
14977
  },
15008
14978
  {
15009
14979
  "label": "Inner Ring",
14980
+ "role": "inner_ring",
15010
14981
  "entityTypeId": "acquisition_channel",
15011
14982
  "description": "Top 3 channels to focus on"
15012
14983
  },
15013
14984
  {
15014
14985
  "label": "Traction Test",
14986
+ "role": "traction_test",
15015
14987
  "entityTypeId": "growth_campaign",
15016
14988
  "description": "Cheap test for each channel"
15017
14989
  },
15018
14990
  {
15019
14991
  "label": "Bullseye Channel",
14992
+ "role": "bullseye_channel",
15020
14993
  "entityTypeId": "acquisition_channel",
15021
14994
  "description": "The single best-performing channel"
15022
14995
  }
@@ -15086,26 +15059,31 @@ var UPG_FRAMEWORKS = [
15086
15059
  "slots": [
15087
15060
  {
15088
15061
  "label": "Free Entry",
15062
+ "role": "free_entry",
15089
15063
  "entityTypeId": "gtm_strategy",
15090
15064
  "description": "How users access the product for free"
15091
15065
  },
15092
15066
  {
15093
15067
  "label": "Aha Moment",
15068
+ "role": "aha_moment",
15094
15069
  "entityTypeId": "gtm_strategy",
15095
15070
  "description": "First experience of core value"
15096
15071
  },
15097
15072
  {
15098
15073
  "label": "Expansion Motion",
15074
+ "role": "expansion_motion",
15099
15075
  "entityTypeId": "sales_motion",
15100
15076
  "description": "How usage spreads within accounts"
15101
15077
  },
15102
15078
  {
15103
15079
  "label": "Monetisation Trigger",
15080
+ "role": "monetisation_trigger",
15104
15081
  "entityTypeId": "gtm_strategy",
15105
15082
  "description": "When and how to convert to paid"
15106
15083
  },
15107
15084
  {
15108
15085
  "label": "Metric",
15086
+ "role": "metric",
15109
15087
  "entityTypeId": "metric",
15110
15088
  "description": "Key metric driving the PLG motion"
15111
15089
  }
@@ -15180,21 +15158,25 @@ var UPG_FRAMEWORKS = [
15180
15158
  "slots": [
15181
15159
  {
15182
15160
  "label": "Objective",
15161
+ "role": "objective",
15183
15162
  "entityTypeId": "objective",
15184
15163
  "description": "Qualitative, inspiring goal"
15185
15164
  },
15186
15165
  {
15187
15166
  "label": "Key Result 1",
15167
+ "role": "key_result",
15188
15168
  "entityTypeId": "key_result",
15189
15169
  "description": "Measurable outcome proving progress"
15190
15170
  },
15191
15171
  {
15192
15172
  "label": "Key Result 2",
15173
+ "role": "key_result",
15193
15174
  "entityTypeId": "key_result",
15194
15175
  "description": "Measurable outcome proving progress"
15195
15176
  },
15196
15177
  {
15197
15178
  "label": "Initiatives",
15179
+ "role": "initiative",
15198
15180
  "entityTypeId": "initiative",
15199
15181
  "description": "Work streams that drive key results"
15200
15182
  }
@@ -15268,26 +15250,31 @@ var UPG_FRAMEWORKS = [
15268
15250
  "slots": [
15269
15251
  {
15270
15252
  "label": "Activity",
15253
+ "role": "activity",
15271
15254
  "entityTypeId": "key_activity",
15272
15255
  "description": "Place persona entities in the Activity position of the matrix"
15273
15256
  },
15274
15257
  {
15275
15258
  "label": "Responsible",
15259
+ "role": "responsible",
15276
15260
  "entityTypeId": "role",
15277
15261
  "description": "Does the work"
15278
15262
  },
15279
15263
  {
15280
15264
  "label": "Accountable",
15265
+ "role": "accountable",
15281
15266
  "entityTypeId": "role",
15282
15267
  "description": "Makes the final call"
15283
15268
  },
15284
15269
  {
15285
15270
  "label": "Consulted",
15271
+ "role": "consulted",
15286
15272
  "entityTypeId": "role",
15287
15273
  "description": "Gives input"
15288
15274
  },
15289
15275
  {
15290
15276
  "label": "Informed",
15277
+ "role": "informed",
15291
15278
  "entityTypeId": "role",
15292
15279
  "description": "Kept in the loop"
15293
15280
  }
@@ -15361,21 +15348,25 @@ var UPG_FRAMEWORKS = [
15361
15348
  "slots": [
15362
15349
  {
15363
15350
  "label": "What Went Well",
15351
+ "role": "went_well",
15364
15352
  "entityTypeId": "outcome",
15365
15353
  "description": "Practices to continue"
15366
15354
  },
15367
15355
  {
15368
15356
  "label": "What Didn't Go Well",
15357
+ "role": "went_poorly",
15369
15358
  "entityTypeId": "need",
15370
15359
  "description": "Issues to address"
15371
15360
  },
15372
15361
  {
15373
15362
  "label": "Action Items",
15363
+ "role": "action_item",
15374
15364
  "entityTypeId": "learning",
15375
15365
  "description": "Changes for next iteration"
15376
15366
  },
15377
15367
  {
15378
15368
  "label": "Learnings",
15369
+ "role": "learning",
15379
15370
  "entityTypeId": "learning",
15380
15371
  "description": "Insights to carry forward"
15381
15372
  }
@@ -15572,7 +15563,7 @@ var UPG_FRAMEWORKS = [
15572
15563
  "entity_types": [
15573
15564
  {
15574
15565
  "type": "team",
15575
- "role": "scored_item"
15566
+ "role": "item"
15576
15567
  },
15577
15568
  {
15578
15569
  "type": "retrospective",
@@ -15736,7 +15727,36 @@ var UPG_FRAMEWORKS = [
15736
15727
  "label": "Severity",
15737
15728
  "format": "number"
15738
15729
  }
15739
- ]
15730
+ ],
15731
+ "scoring_lens": {
15732
+ "applies_to": [
15733
+ "risk"
15734
+ ],
15735
+ "inputs": [
15736
+ {
15737
+ "property": "probability",
15738
+ "type": "number",
15739
+ "required": true,
15740
+ "label": "Probability",
15741
+ "description": "Likelihood the risk materialises (risk.probability assessment)"
15742
+ },
15743
+ {
15744
+ "property": "impact",
15745
+ "type": "number",
15746
+ "required": true,
15747
+ "label": "Impact",
15748
+ "description": "Consequence severity if the risk materialises (risk.impact assessment)"
15749
+ }
15750
+ ],
15751
+ "computed": [
15752
+ {
15753
+ "property": "severity",
15754
+ "expression": "probability * impact",
15755
+ "label": "Severity",
15756
+ "format": "number"
15757
+ }
15758
+ ]
15759
+ }
15740
15760
  },
15741
15761
  "structure": {
15742
15762
  "pattern": "table"
@@ -15812,31 +15832,37 @@ var UPG_FRAMEWORKS = [
15812
15832
  "slots": [
15813
15833
  {
15814
15834
  "label": "Items to score",
15835
+ "role": "candidate",
15815
15836
  "entityTypeId": "feature",
15816
15837
  "description": "Features or experiments being evaluated"
15817
15838
  },
15818
15839
  {
15819
15840
  "label": "Impact",
15841
+ "role": "impact",
15820
15842
  "entityTypeId": "outcome",
15821
15843
  "description": "How much will this move the needle?"
15822
15844
  },
15823
15845
  {
15824
15846
  "label": "Confidence",
15847
+ "role": "confidence",
15825
15848
  "entityTypeId": "assumption",
15826
15849
  "description": "How sure are we about the impact?"
15827
15850
  },
15828
15851
  {
15829
15852
  "label": "Ease",
15853
+ "role": "ease",
15830
15854
  "entityTypeId": "feature",
15831
15855
  "description": "How easy is this to implement?"
15832
15856
  },
15833
15857
  {
15834
15858
  "label": "Opportunities to score",
15859
+ "role": "candidate",
15835
15860
  "entityTypeId": "opportunity",
15836
15861
  "description": "Opportunities scored on the same ICE Scoring inputs as features."
15837
15862
  },
15838
15863
  {
15839
15864
  "label": "Needs to score",
15865
+ "role": "candidate",
15840
15866
  "entityTypeId": "need",
15841
15867
  "description": "Needs scored on the same ICE Scoring inputs as features."
15842
15868
  }
@@ -15966,7 +15992,48 @@ var UPG_FRAMEWORKS = [
15966
15992
  "label": "ICE Score",
15967
15993
  "format": "number"
15968
15994
  }
15969
- ]
15995
+ ],
15996
+ "scoring_lens": {
15997
+ "applies_to": [
15998
+ "feature",
15999
+ "opportunity",
16000
+ "need"
16001
+ ],
16002
+ "inputs": [
16003
+ {
16004
+ "property": "impact",
16005
+ "type": "number",
16006
+ "required": true,
16007
+ "scope": "framework",
16008
+ "label": "Impact",
16009
+ "description": "Expected impact on the target metric (1-10)"
16010
+ },
16011
+ {
16012
+ "property": "confidence",
16013
+ "type": "number",
16014
+ "required": true,
16015
+ "scope": "framework",
16016
+ "label": "Confidence",
16017
+ "description": "Confidence in the impact estimate (1-10)"
16018
+ },
16019
+ {
16020
+ "property": "ease",
16021
+ "type": "number",
16022
+ "required": true,
16023
+ "scope": "framework",
16024
+ "label": "Ease",
16025
+ "description": "Ease of implementation (1-10)"
16026
+ }
16027
+ ],
16028
+ "computed": [
16029
+ {
16030
+ "property": "ice_score",
16031
+ "expression": "impact * confidence * ease",
16032
+ "label": "ICE Score",
16033
+ "format": "number"
16034
+ }
16035
+ ]
16036
+ }
15970
16037
  },
15971
16038
  "structure": {
15972
16039
  "pattern": "table"
@@ -16046,31 +16113,37 @@ var UPG_FRAMEWORKS = [
16046
16113
  "slots": [
16047
16114
  {
16048
16115
  "label": "Backlog Items",
16116
+ "role": "candidate",
16049
16117
  "entityTypeId": "feature",
16050
16118
  "description": "Backlog Items: feature entries to evaluate"
16051
16119
  },
16052
16120
  {
16053
16121
  "label": "User/Business Value",
16122
+ "role": "user_business_value",
16054
16123
  "entityTypeId": "metric",
16055
16124
  "description": "User/Business Value: metric entries to evaluate"
16056
16125
  },
16057
16126
  {
16058
16127
  "label": "Time Criticality",
16128
+ "role": "time_criticality",
16059
16129
  "entityTypeId": "metric",
16060
16130
  "description": "How much value decays if delivery is delayed (deadlines, competition, seasonal windows)"
16061
16131
  },
16062
16132
  {
16063
16133
  "label": "Risk Reduction / Opportunity Enablement",
16134
+ "role": "risk_reduction",
16064
16135
  "entityTypeId": "metric",
16065
16136
  "description": "Risk Reduction / Opportunity Enablement: metric entries to evaluate"
16066
16137
  },
16067
16138
  {
16068
16139
  "label": "Job Size",
16140
+ "role": "job_size",
16069
16141
  "entityTypeId": "metric",
16070
16142
  "description": "Estimated effort (story points, t-shirt size, or person-weeks)"
16071
16143
  },
16072
16144
  {
16073
16145
  "label": "Opportunities to score",
16146
+ "role": "candidate",
16074
16147
  "entityTypeId": "opportunity",
16075
16148
  "description": "Opportunities scored on the same WSJF (Weighted Shortest Job First) inputs as features."
16076
16149
  }
@@ -16175,7 +16248,55 @@ var UPG_FRAMEWORKS = [
16175
16248
  "label": "WSJF Score",
16176
16249
  "format": "number"
16177
16250
  }
16178
- ]
16251
+ ],
16252
+ "scoring_lens": {
16253
+ "applies_to": [
16254
+ "feature",
16255
+ "opportunity"
16256
+ ],
16257
+ "inputs": [
16258
+ {
16259
+ "property": "user_value",
16260
+ "type": "number",
16261
+ "required": true,
16262
+ "scope": "framework",
16263
+ "label": "User/Business Value",
16264
+ "description": "Relative value to users and the business if delivered"
16265
+ },
16266
+ {
16267
+ "property": "time_criticality",
16268
+ "type": "number",
16269
+ "required": true,
16270
+ "scope": "framework",
16271
+ "label": "Time Criticality",
16272
+ "description": "How much value decays if delivery is delayed (deadlines, competition, seasonal windows)"
16273
+ },
16274
+ {
16275
+ "property": "risk_reduction",
16276
+ "type": "number",
16277
+ "required": true,
16278
+ "scope": "framework",
16279
+ "label": "Risk Reduction / Opportunity Enablement",
16280
+ "description": "Value from reducing risk or enabling future opportunities"
16281
+ },
16282
+ {
16283
+ "property": "job_size",
16284
+ "type": "number",
16285
+ "required": true,
16286
+ "scope": "framework",
16287
+ "label": "Job Size",
16288
+ "description": "Estimated effort (story points, t-shirt size, or person-weeks)"
16289
+ }
16290
+ ],
16291
+ "computed": [
16292
+ {
16293
+ "property": "wsjf_score",
16294
+ "expression": "(user_value + time_criticality + risk_reduction) / job_size",
16295
+ "label": "WSJF Score",
16296
+ "format": "number"
16297
+ }
16298
+ ]
16299
+ }
16179
16300
  },
16180
16301
  "structure": {
16181
16302
  "pattern": "table"
@@ -16248,7 +16369,7 @@ var UPG_FRAMEWORKS = [
16248
16369
  ],
16249
16370
  "name": "Cost of Delay",
16250
16371
  "version": "1.0.0",
16251
- "description": "Quantify the economic cost of not shipping a feature to drive priority decisions. Combines urgency with value.",
16372
+ "description": "Quantify the economic cost of not shipping a feature or opportunity to drive priority decisions. Combines urgency with value.",
16252
16373
  "category": "prioritization",
16253
16374
  "origin": {
16254
16375
  "type": "practitioner",
@@ -16364,7 +16485,39 @@ var UPG_FRAMEWORKS = [
16364
16485
  "label": "WSJF Score",
16365
16486
  "format": "number"
16366
16487
  }
16367
- ]
16488
+ ],
16489
+ "scoring_lens": {
16490
+ "applies_to": [
16491
+ "feature",
16492
+ "opportunity"
16493
+ ],
16494
+ "inputs": [
16495
+ {
16496
+ "property": "cost_of_delay",
16497
+ "type": "number",
16498
+ "required": true,
16499
+ "scope": "framework",
16500
+ "label": "Cost of Delay",
16501
+ "description": "Weekly revenue impact of not shipping"
16502
+ },
16503
+ {
16504
+ "property": "job_size",
16505
+ "type": "number",
16506
+ "required": true,
16507
+ "scope": "framework",
16508
+ "label": "Job Size",
16509
+ "description": "Weeks of development effort"
16510
+ }
16511
+ ],
16512
+ "computed": [
16513
+ {
16514
+ "property": "wsjf_score",
16515
+ "expression": "cost_of_delay / job_size",
16516
+ "label": "WSJF Score",
16517
+ "format": "number"
16518
+ }
16519
+ ]
16520
+ }
16368
16521
  },
16369
16522
  "structure": {
16370
16523
  "pattern": "table"
@@ -16446,16 +16599,19 @@ var UPG_FRAMEWORKS = [
16446
16599
  "slots": [
16447
16600
  {
16448
16601
  "label": "Symptom",
16602
+ "role": "symptom",
16449
16603
  "entityTypeId": "need",
16450
16604
  "description": "The observed problem the analysis starts from."
16451
16605
  },
16452
16606
  {
16453
16607
  "label": "Why chain",
16608
+ "role": "why",
16454
16609
  "entityTypeId": "insight",
16455
16610
  "description": 'Each "why?" answer along the chain, typically five iterations deep.'
16456
16611
  },
16457
16612
  {
16458
16613
  "label": "Root cause",
16614
+ "role": "root_cause",
16459
16615
  "entityTypeId": "insight",
16460
16616
  "description": "The terminal answer at the bottom of the chain: the underlying cause to address."
16461
16617
  }
@@ -16800,16 +16956,19 @@ var UPG_FRAMEWORKS = [
16800
16956
  "slots": [
16801
16957
  {
16802
16958
  "label": "First-order move",
16959
+ "role": "first_order",
16803
16960
  "entityTypeId": "decision",
16804
16961
  "description": "The decision or move under consideration."
16805
16962
  },
16806
16963
  {
16807
16964
  "label": "Second-order consequences",
16965
+ "role": "second_order",
16808
16966
  "entityTypeId": "insight",
16809
16967
  "description": "Downstream effects that follow from the first-order move."
16810
16968
  },
16811
16969
  {
16812
16970
  "label": "Higher-order consequences",
16971
+ "role": "higher_order",
16813
16972
  "entityTypeId": "insight",
16814
16973
  "description": "Third-, fourth-, fifth-order ripples: second-order consequences of the second-order consequences."
16815
16974
  }
@@ -16869,6 +17028,439 @@ for (const fw of UPG_FRAMEWORKS) {
16869
17028
  UPG_FRAMEWORKS_BY_CATEGORY[fw.category].push(fw);
16870
17029
  }
16871
17030
 
17031
+ // src/grammar/validate.ts
17032
+ var crossProductEdgeTypeSet = new Set(UPG_CROSS_EDGE_TYPES);
17033
+ var CONTENT_DEPTH_WARNING_RULES = /* @__PURE__ */ new Set([
17034
+ "property-type",
17035
+ "property-enum",
17036
+ "self-loop",
17037
+ "framework-score"
17038
+ ]);
17039
+ function validateUPGDocument(doc) {
17040
+ const errors = [];
17041
+ const warnings = [];
17042
+ if (!doc || typeof doc !== "object") {
17043
+ return { valid: false, errors: [{ path: "$", message: "Document must be an object" }], warnings };
17044
+ }
17045
+ const d = doc;
17046
+ if (!d.upg_version || typeof d.upg_version !== "string") {
17047
+ errors.push({ path: "$.upg_version", message: "upg_version is required and must be a string" });
17048
+ }
17049
+ if (!d.exported_at || typeof d.exported_at !== "string") {
17050
+ errors.push({ path: "$.exported_at", message: "exported_at is required and must be an ISO 8601 string" });
17051
+ }
17052
+ if (!d.source || typeof d.source !== "object") {
17053
+ errors.push({ path: "$.source", message: "source is required and must be an object" });
17054
+ } else {
17055
+ const source = d.source;
17056
+ if (!source.tool || typeof source.tool !== "string") {
17057
+ errors.push({ path: "$.source.tool", message: "source.tool is required and must be a string" });
17058
+ }
17059
+ }
17060
+ if (!d.product || typeof d.product !== "object") {
17061
+ errors.push({ path: "$.product", message: "product is required and must be an object" });
17062
+ } else {
17063
+ const product = d.product;
17064
+ if (!product.id || typeof product.id !== "string") {
17065
+ errors.push({ path: "$.product.id", message: "product.id is required and must be a string" });
17066
+ }
17067
+ if (!product.title || typeof product.title !== "string") {
17068
+ errors.push({ path: "$.product.title", message: "product.title is required and must be a string" });
17069
+ }
17070
+ }
17071
+ const allKnownTypes = new Set(getTypes());
17072
+ const allKnownEdgeTypes = new Set(Object.keys(UPG_EDGE_CATALOG));
17073
+ if (!Array.isArray(d.nodes)) {
17074
+ errors.push({
17075
+ path: "$.nodes",
17076
+ message: d.nodes === void 0 ? "nodes is required and must be an array" : `nodes must be an array, got ${describeKind(d.nodes)}`
17077
+ });
17078
+ } else {
17079
+ const nodeIds = /* @__PURE__ */ new Set();
17080
+ const nodesById = /* @__PURE__ */ new Map();
17081
+ d.nodes.forEach((node, i) => {
17082
+ const path = `$.nodes[${i}]`;
17083
+ if (!node || typeof node !== "object") {
17084
+ errors.push({ path, message: "Each node must be an object" });
17085
+ return;
17086
+ }
17087
+ const n = node;
17088
+ if (!n.id || typeof n.id !== "string") {
17089
+ errors.push({ path: `${path}.id`, message: "Node id is required and must be a string" });
17090
+ } else {
17091
+ if (nodeIds.has(n.id)) {
17092
+ errors.push({ path: `${path}.id`, message: `Duplicate node id: ${n.id}` });
17093
+ }
17094
+ nodeIds.add(n.id);
17095
+ nodesById.set(n.id, n);
17096
+ }
17097
+ if (!n.type || typeof n.type !== "string") {
17098
+ errors.push({ path: `${path}.type`, message: "Node type is required and must be a string" });
17099
+ } else if (!allKnownTypes.has(n.type)) {
17100
+ warnings.push({ path: `${path}.type`, message: `Unknown UPG type: "${n.type}". Node will be preserved with its original type.` });
17101
+ }
17102
+ if (!n.title || typeof n.title !== "string") {
17103
+ errors.push({ path: `${path}.title`, message: "Node title is required and must be a string" });
17104
+ } else if (n.title.trim().length === 0) {
17105
+ errors.push({ path: `${path}.title`, message: "Node title must not be blank (whitespace-only)" });
17106
+ }
17107
+ if (n.properties !== void 0 && n.properties !== null) {
17108
+ if (typeof n.properties !== "object" || Array.isArray(n.properties)) {
17109
+ errors.push({
17110
+ path: `${path}.properties`,
17111
+ message: `Node properties must be a plain object when present, got ${describeKind(n.properties)}.`
17112
+ });
17113
+ }
17114
+ }
17115
+ if (typeof n.type === "string") {
17116
+ validateNodeProperties(n, `${path}`, warnings);
17117
+ }
17118
+ });
17119
+ if (!Array.isArray(d.edges)) {
17120
+ errors.push({
17121
+ path: "$.edges",
17122
+ message: d.edges === void 0 ? "edges is required and must be an array" : `edges must be an array, got ${describeKind(d.edges)}`
17123
+ });
17124
+ } else {
17125
+ d.edges.forEach((edge, i) => {
17126
+ const path = `$.edges[${i}]`;
17127
+ if (!edge || typeof edge !== "object") {
17128
+ errors.push({ path, message: "Each edge must be an object" });
17129
+ return;
17130
+ }
17131
+ const e = edge;
17132
+ if (!e.id || typeof e.id !== "string") {
17133
+ errors.push({ path: `${path}.id`, message: "Edge id is required and must be a string" });
17134
+ }
17135
+ if (typeof e.type === "string" && crossProductEdgeTypeSet.has(e.type)) {
17136
+ errors.push({
17137
+ path: `${path}.type`,
17138
+ message: `Cross-product edge type "${e.type}" must live in portfolio.cross_edges[], not product edges[].`
17139
+ });
17140
+ return;
17141
+ }
17142
+ if (!e.source || typeof e.source !== "string") {
17143
+ errors.push({ path: `${path}.source`, message: "Edge source is required and must be a string" });
17144
+ } else if (!nodeIds.has(e.source)) {
17145
+ errors.push({ path: `${path}.source`, message: `Edge source references unknown node id: ${e.source}` });
17146
+ }
17147
+ if (!e.target || typeof e.target !== "string") {
17148
+ errors.push({ path: `${path}.target`, message: "Edge target is required and must be a string" });
17149
+ } else if (!nodeIds.has(e.target)) {
17150
+ errors.push({ path: `${path}.target`, message: `Edge target references unknown node id: ${e.target}` });
17151
+ }
17152
+ if (typeof e.source === "string" && typeof e.target === "string" && e.source === e.target) {
17153
+ warnings.push({
17154
+ path: `${path}`,
17155
+ message: `Self-loop edge: source and target are the same node ("${e.source}"). A node related to itself is almost always an error.`,
17156
+ rule: "self-loop"
17157
+ });
17158
+ }
17159
+ if (!e.type || typeof e.type !== "string") {
17160
+ errors.push({ path: `${path}.type`, message: "Edge type is required and must be a string" });
17161
+ } else if (!allKnownEdgeTypes.has(e.type)) {
17162
+ warnings.push({
17163
+ path: `${path}.type`,
17164
+ message: `Unknown edge type: "${e.type}". Edge preserved but not in the edge registry.`
17165
+ });
17166
+ }
17167
+ });
17168
+ const valueToAxis = /* @__PURE__ */ new Map();
17169
+ const edgesArr = d.edges;
17170
+ for (const edge of edgesArr) {
17171
+ if (!edge || typeof edge !== "object") continue;
17172
+ const e = edge;
17173
+ if (e.type === "classification_axis_includes_classification_value" && typeof e.source === "string" && typeof e.target === "string") {
17174
+ valueToAxis.set(e.target, e.source);
17175
+ }
17176
+ }
17177
+ for (const [id, n] of nodesById.entries()) {
17178
+ if (n.type !== "classification_value") continue;
17179
+ if (valueToAxis.has(id)) continue;
17180
+ const pid = n.parent_id;
17181
+ if (typeof pid === "string") {
17182
+ const parent = nodesById.get(pid);
17183
+ if (parent && parent.type === "classification_axis") {
17184
+ valueToAxis.set(id, pid);
17185
+ }
17186
+ }
17187
+ }
17188
+ const axesPerAnalysis = /* @__PURE__ */ new Map();
17189
+ for (const edge of edgesArr) {
17190
+ if (!edge || typeof edge !== "object") continue;
17191
+ const e = edge;
17192
+ if (e.type === "competitive_analysis_dimensioned_by_classification_axis" && typeof e.source === "string") {
17193
+ axesPerAnalysis.set(e.source, (axesPerAnalysis.get(e.source) ?? 0) + 1);
17194
+ }
17195
+ }
17196
+ d.nodes.forEach((node, i) => {
17197
+ if (!node || typeof node !== "object") return;
17198
+ const n = node;
17199
+ if (n.type !== "competitive_analysis") return;
17200
+ const props = n.properties;
17201
+ if (!props || typeof props !== "object") return;
17202
+ const empty = props.empty_cells;
17203
+ if (!Array.isArray(empty)) return;
17204
+ const axisCount = axesPerAnalysis.get(n.id ?? "") ?? 0;
17205
+ if (axisCount !== 2) {
17206
+ warnings.push({
17207
+ path: `$.nodes[${i}].properties.empty_cells`,
17208
+ message: `empty_cells is meaningful only on a 2-axis competitive_analysis; this node is dimensioned by ${axisCount} classification_axis ${axisCount === 1 ? "child" : "children"}.`
17209
+ });
17210
+ }
17211
+ empty.forEach((cell, j) => {
17212
+ const cpath = `$.nodes[${i}].properties.empty_cells[${j}]`;
17213
+ if (!cell || typeof cell !== "object") {
17214
+ errors.push({ path: cpath, message: "empty_cells entry must be an object" });
17215
+ return;
17216
+ }
17217
+ const c = cell;
17218
+ const aRef = c.axis_a_value_ref;
17219
+ const bRef = c.axis_b_value_ref;
17220
+ if (typeof aRef !== "string") {
17221
+ errors.push({ path: `${cpath}.axis_a_value_ref`, message: "axis_a_value_ref is required and must be a string" });
17222
+ }
17223
+ if (typeof bRef !== "string") {
17224
+ errors.push({ path: `${cpath}.axis_b_value_ref`, message: "axis_b_value_ref is required and must be a string" });
17225
+ }
17226
+ if (typeof aRef !== "string" || typeof bRef !== "string") return;
17227
+ const aNode = nodesById.get(aRef);
17228
+ const bNode = nodesById.get(bRef);
17229
+ if (!aNode) {
17230
+ errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Unknown node id: ${aRef}` });
17231
+ } else if (aNode.type !== "classification_value") {
17232
+ errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Expected classification_value, found "${String(aNode.type)}" at ${aRef}` });
17233
+ }
17234
+ if (!bNode) {
17235
+ errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Unknown node id: ${bRef}` });
17236
+ } else if (bNode.type !== "classification_value") {
17237
+ errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Expected classification_value, found "${String(bNode.type)}" at ${bRef}` });
17238
+ }
17239
+ if (aNode && bNode && aNode.type === "classification_value" && bNode.type === "classification_value") {
17240
+ const aAxis = valueToAxis.get(aRef);
17241
+ const bAxis = valueToAxis.get(bRef);
17242
+ if (aAxis && bAxis && aAxis === bAxis) {
17243
+ errors.push({
17244
+ path: cpath,
17245
+ message: `empty_cells references two classification_values from the same axis (${aAxis}); axis_a and axis_b must come from distinct classification_axis parents.`
17246
+ });
17247
+ }
17248
+ }
17249
+ const kind = c.rationale_kind;
17250
+ if (kind !== void 0 && kind !== "structural" && kind !== "ideological" && kind !== "opportunity") {
17251
+ errors.push({
17252
+ path: `${cpath}.rationale_kind`,
17253
+ message: `rationale_kind must be one of 'structural' | 'ideological' | 'opportunity'; got "${String(kind)}".`
17254
+ });
17255
+ }
17256
+ });
17257
+ });
17258
+ validateFrameworkScores(d.nodes, d.edges, warnings);
17259
+ }
17260
+ }
17261
+ return { valid: errors.length === 0, errors, warnings };
17262
+ }
17263
+ function validateFrameworkScores(nodes, edges, warnings) {
17264
+ const nodeById = /* @__PURE__ */ new Map();
17265
+ const exercises = [];
17266
+ for (const node of nodes) {
17267
+ if (!node || typeof node !== "object") continue;
17268
+ const n = node;
17269
+ if (typeof n.id === "string") nodeById.set(n.id, n);
17270
+ if (n.type !== "framework_exercise") continue;
17271
+ const props = n.properties;
17272
+ if (!props || typeof props !== "object" || Array.isArray(props)) continue;
17273
+ const frameworkId = props.framework_id;
17274
+ if (typeof frameworkId !== "string") continue;
17275
+ const framework = UPG_FRAMEWORKS_BY_ID[frameworkId];
17276
+ if (!framework) continue;
17277
+ if (typeof n.id === "string") exercises.push({ id: n.id, framework });
17278
+ }
17279
+ if (exercises.length === 0) return;
17280
+ const exerciseById = new Map(exercises.map((e) => [e.id, e.framework]));
17281
+ edges.forEach((edge, i) => {
17282
+ if (!edge || typeof edge !== "object") return;
17283
+ const e = edge;
17284
+ if (e.type !== "framework_exercise_includes_node") return;
17285
+ if (typeof e.source !== "string" || typeof e.target !== "string") return;
17286
+ const framework = exerciseById.get(e.source);
17287
+ if (!framework) return;
17288
+ const score = e.properties;
17289
+ if (!score || typeof score !== "object" || Array.isArray(score)) return;
17290
+ const targetNode = nodeById.get(e.target);
17291
+ const targetType = typeof targetNode?.type === "string" ? targetNode.type : void 0;
17292
+ if (!targetType) return;
17293
+ const requirements = framework.data?.required_properties?.[targetType];
17294
+ if (!requirements || requirements.length === 0) return;
17295
+ const divisors = divisorInputs(framework);
17296
+ const path = `$.edges[${i}].properties`;
17297
+ const scoreObj = score;
17298
+ for (const req of requirements) {
17299
+ const value = scoreObj[req.property];
17300
+ if (value === void 0 || value === null) continue;
17301
+ const finding = checkScoreValue(req, value, divisors.has(req.property), framework.id);
17302
+ if (finding) {
17303
+ warnings.push({
17304
+ path: `${path}.${req.property}`,
17305
+ message: finding,
17306
+ rule: "framework-score"
17307
+ });
17308
+ }
17309
+ }
17310
+ });
17311
+ }
17312
+ function divisorInputs(framework) {
17313
+ const expr = framework.data?.computed_properties?.[0]?.expression;
17314
+ if (typeof expr !== "string" || expr.length === 0) return EMPTY_STRING_SET;
17315
+ const out = /* @__PURE__ */ new Set();
17316
+ const re = /\/\s*\(?\s*([A-Za-z_][A-Za-z0-9_]*)/g;
17317
+ let m;
17318
+ while ((m = re.exec(expr)) !== null) out.add(m[1]);
17319
+ return out;
17320
+ }
17321
+ var EMPTY_STRING_SET = /* @__PURE__ */ new Set();
17322
+ function checkScoreValue(req, value, isDivisor, frameworkId) {
17323
+ switch (req.type) {
17324
+ case "enum": {
17325
+ const allowed = req.enum_values ?? [];
17326
+ if (typeof value !== "string") {
17327
+ return `${frameworkId} score "${req.property}" must be one of ${allowed.join(", ")}; got ${describeKind(value)}.`;
17328
+ }
17329
+ if (allowed.length > 0 && !allowed.includes(value)) {
17330
+ return `${frameworkId} score "${req.property}" has value "${value}" outside its allowed set: ${allowed.join(", ")}.`;
17331
+ }
17332
+ return null;
17333
+ }
17334
+ case "assessment": {
17335
+ const num = numericScore(value);
17336
+ if (num === null) {
17337
+ return `${frameworkId} score "${req.property}" must be a number; got ${describeKind(value)}.`;
17338
+ }
17339
+ const scale = req.scale_id ? getScale(req.scale_id) : void 0;
17340
+ if (scale) {
17341
+ if (num < scale.min || num > scale.max) {
17342
+ return `${frameworkId} score "${req.property}" is ${num}, outside the ${req.scale_id} scale range [${scale.min}, ${scale.max}].`;
17343
+ }
17344
+ } else if (num <= 0) {
17345
+ return `${frameworkId} score "${req.property}" must be greater than 0; got ${num}.`;
17346
+ }
17347
+ if (isDivisor && num === 0) {
17348
+ return `${frameworkId} score "${req.property}" must not be 0 (it is a divisor in the framework's formula).`;
17349
+ }
17350
+ return null;
17351
+ }
17352
+ case "number": {
17353
+ const num = numericScore(value);
17354
+ if (num === null) {
17355
+ return `${frameworkId} score "${req.property}" must be a number; got ${describeKind(value)}.`;
17356
+ }
17357
+ if (isDivisor && num === 0) {
17358
+ return `${frameworkId} score "${req.property}" must not be 0 (it is a divisor in the framework's formula).`;
17359
+ }
17360
+ if (num < 0) {
17361
+ return `${frameworkId} score "${req.property}" must not be negative; got ${num}.`;
17362
+ }
17363
+ return null;
17364
+ }
17365
+ case "boolean": {
17366
+ if (typeof value !== "boolean") {
17367
+ return `${frameworkId} score "${req.property}" must be a boolean; got ${describeKind(value)}.`;
17368
+ }
17369
+ return null;
17370
+ }
17371
+ case "string": {
17372
+ if (typeof value !== "string") {
17373
+ return `${frameworkId} score "${req.property}" must be a string; got ${describeKind(value)}.`;
17374
+ }
17375
+ return null;
17376
+ }
17377
+ default:
17378
+ return null;
17379
+ }
17380
+ }
17381
+ function numericScore(value) {
17382
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
17383
+ if (typeof value === "string") {
17384
+ const n = Number.parseFloat(value);
17385
+ return Number.isFinite(n) ? n : null;
17386
+ }
17387
+ if (value && typeof value === "object" && !Array.isArray(value)) {
17388
+ const v = value.value;
17389
+ if (typeof v === "number") return Number.isFinite(v) ? v : null;
17390
+ }
17391
+ return null;
17392
+ }
17393
+ function runtimeKind(val) {
17394
+ if (val === null || val === void 0) return null;
17395
+ if (Array.isArray(val)) return "string[]";
17396
+ switch (typeof val) {
17397
+ case "string":
17398
+ return "string";
17399
+ case "number":
17400
+ return "number";
17401
+ case "boolean":
17402
+ return "boolean";
17403
+ case "object":
17404
+ return "object";
17405
+ default:
17406
+ return null;
17407
+ }
17408
+ }
17409
+ function valueMatchesType(defType, val) {
17410
+ const kind = runtimeKind(val);
17411
+ if (kind === null) return true;
17412
+ switch (defType) {
17413
+ case "string":
17414
+ return kind === "string";
17415
+ case "number":
17416
+ return kind === "number";
17417
+ case "boolean":
17418
+ return kind === "boolean";
17419
+ case "string[]":
17420
+ return kind === "string[]";
17421
+ case "object":
17422
+ case "assessment":
17423
+ return kind === "object";
17424
+ default:
17425
+ return true;
17426
+ }
17427
+ }
17428
+ function describeKind(val) {
17429
+ if (Array.isArray(val)) return "array";
17430
+ return typeof val;
17431
+ }
17432
+ function validateNodeProperties(node, basePath, warnings) {
17433
+ const type = node.type;
17434
+ if (typeof type !== "string") return;
17435
+ const schema = getPropertySchema(type);
17436
+ if (!schema) return;
17437
+ const props = node.properties;
17438
+ if (!props || typeof props !== "object" || Array.isArray(props)) return;
17439
+ for (const [key, value] of Object.entries(props)) {
17440
+ const def = schema[key];
17441
+ if (!def) continue;
17442
+ if (value === null || value === void 0) continue;
17443
+ if (!valueMatchesType(def.type, value)) {
17444
+ warnings.push({
17445
+ path: `${basePath}.properties.${key}`,
17446
+ message: `Property "${key}" on ${type} should be ${def.type}, got ${describeKind(value)}.`,
17447
+ rule: "property-type"
17448
+ });
17449
+ continue;
17450
+ }
17451
+ if (def.enum && typeof value === "string" && !def.enum.includes(value)) {
17452
+ warnings.push({
17453
+ path: `${basePath}.properties.${key}`,
17454
+ message: `Property "${key}" on ${type} has value "${value}" outside its allowed set: ${def.enum.join(", ")}.`,
17455
+ rule: "property-enum"
17456
+ });
17457
+ }
17458
+ }
17459
+ }
17460
+ function isUPGDocument(doc) {
17461
+ return validateUPGDocument(doc).valid;
17462
+ }
17463
+
16872
17464
  // src/presentation/labels.ts
16873
17465
  var PRIORITY_LABELS = [
16874
17466
  // ── need (CONSOLIDATED: replaces pain_point + user_need) ─────────────────────
@@ -25265,7 +25857,7 @@ function isCanonical(text) {
25265
25857
  }
25266
25858
 
25267
25859
  // src/index.ts
25268
- var UPG_VERSION = "0.8.6";
25860
+ var UPG_VERSION = "0.8.7";
25269
25861
  var UPG_FORMAT_VERSION = "0.4.0";
25270
25862
  var MARKDOWN_FORMAT_VERSION = "0.1";
25271
25863
  var UPG_TYPES = getTypes();
@@ -25334,6 +25926,7 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
25334
25926
  BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK,
25335
25927
  BUSINESS_GTM_GROWTH_PLAYBOOK,
25336
25928
  BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK,
25929
+ CONTENT_DEPTH_WARNING_RULES,
25337
25930
  DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK,
25338
25931
  ENGINEERING_PLATFORM_PLAYBOOK,
25339
25932
  EXPERIENCE_DESIGN_BRAND_PLAYBOOK,