@unified-product-graph/mcp-server 0.8.15 → 0.9.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/dist/index.js CHANGED
@@ -116,7 +116,7 @@ var UPG_DOMAINS = [
116
116
  {
117
117
  id: "product_spec",
118
118
  label: "Product Specification",
119
- description: "What you are building and shipping. Feature areas group related capabilities. Features, epics, and user stories break work down. Acceptance criteria define done. Tasks and bugs track execution. Releases and changelogs mark what shipped. Roadmaps and roadmap items plan what comes next. Themes group strategic bets. Translates Strategy into Engineering and tracks delivery through Program Management.",
119
+ description: "What you are building and shipping. Feature areas group related capabilities. Features, epics, and user stories break work down. Acceptance criteria define done. Tasks and bugs track execution. Releases and changelogs mark what shipped. Roadmaps and roadmap items plan what comes next. Roadmap themes group roadmap work around the customer problem it solves, one level down from the strategic themes in Strategy. Translates Strategy into Engineering and tracks delivery through Program Management.",
120
120
  types: [
121
121
  "feature",
122
122
  "feature_area",
@@ -128,7 +128,7 @@ var UPG_DOMAINS = [
128
128
  "bug",
129
129
  "roadmap",
130
130
  "roadmap_item",
131
- "theme",
131
+ "roadmap_theme",
132
132
  "changelog"
133
133
  ]
134
134
  },
@@ -556,8 +556,8 @@ var UPG_ENTITY_META = [
556
556
  { name: "need", type_id: "ent_313", maturity: "stable", since: "0.1.0" },
557
557
  { name: "switching_cost", type_id: "ent_022", maturity: "stable", since: "0.1.0" },
558
558
  // ── Discovery ──
559
- { name: "opportunity", type_id: "ent_023", maturity: "stable", since: "0.1.0" },
560
- { name: "solution", type_id: "ent_024", maturity: "stable", since: "0.1.0" },
559
+ { name: "opportunity", type_id: "ent_023", maturity: "stable", since: "0.1.0", default_frameworks: ["opportunity-sizing", "rice-scoring"] },
560
+ { name: "solution", type_id: "ent_024", maturity: "stable", since: "0.1.0", default_frameworks: ["rice-scoring"] },
561
561
  { name: "feasibility_study", type_id: "ent_025", maturity: "stable", since: "0.1.0" },
562
562
  { name: "design_sprint", type_id: "ent_026", maturity: "stable", since: "0.1.0" },
563
563
  // ── Validation ──
@@ -658,7 +658,8 @@ var UPG_ENTITY_META = [
658
658
  { name: "bug", type_id: "ent_077", maturity: "stable", since: "0.1.0" },
659
659
  { name: "roadmap", type_id: "ent_078", maturity: "stable", since: "0.1.0" },
660
660
  { name: "roadmap_item", type_id: "ent_079", maturity: "stable", since: "0.1.0" },
661
- { name: "theme", type_id: "ent_080", maturity: "stable", since: "0.1.0" },
661
+ { name: "theme", type_id: "ent_080", maturity: "deprecated", since: "0.1.0", deprecated_in: "0.9.0", replacement: "roadmap_theme" },
662
+ { name: "roadmap_theme", type_id: "ent_351", maturity: "stable", since: "0.1.0" },
662
663
  { name: "changelog", type_id: "ent_081", maturity: "stable", since: "0.1.0" },
663
664
  // ── Engineering ──
664
665
  { name: "bounded_context", type_id: "ent_082", maturity: "stable", since: "0.1.0" },
@@ -1165,6 +1166,12 @@ var UPG_EDGE_CATALOG = {
1165
1166
  strategic_pillar_delivers_value_stream: { forward_verb: "delivers", reverse_verb: "delivered_by", classification: "hierarchy", source_type: "strategic_pillar", target_type: "value_stream" },
1166
1167
  strategic_pillar_decided_via_decision: { forward_verb: "decided_via", reverse_verb: "decided_for", classification: "hierarchy", source_type: "strategic_pillar", target_type: "decision" },
1167
1168
  strategic_theme_pursues_initiative: { forward_verb: "pursues", reverse_verb: "pursued_under", classification: "hierarchy", source_type: "strategic_theme", target_type: "initiative" },
1169
+ // v0.9.0 (UPG-660): the soft bridge from the annual strategy focus area to the
1170
+ // roadmap grouping that realises it. Semantic, NOT hierarchy: strategic_theme and
1171
+ // roadmap_theme sit on different spines (strategy cascade vs roadmap cascade), so
1172
+ // this is a cross-reference, not containment. Pairs with the theme → roadmap_theme
1173
+ // rename that removed the bare-'theme' collision (N6/UPG-652 lineage).
1174
+ strategic_theme_realised_by_roadmap_theme: { forward_verb: "realised_by", reverse_verb: "realises", classification: "semantic", source_type: "strategic_theme", target_type: "roadmap_theme" },
1168
1175
  // v0.5.4 (UPG-511): three edges that lift strategic_theme from structural
1169
1176
  // isolation to a conceptually central strategy node.
1170
1177
  //
@@ -1222,7 +1229,7 @@ var UPG_EDGE_CATALOG = {
1222
1229
  product_builds_feature: { forward_verb: "builds", reverse_verb: "built_by", classification: "hierarchy", source_type: "product", target_type: "feature" },
1223
1230
  product_ships_via_release: { forward_verb: "ships_via", reverse_verb: "ships", classification: "hierarchy", source_type: "product", target_type: "release" },
1224
1231
  product_plans_via_roadmap: { forward_verb: "plans_via", reverse_verb: "plans_for", classification: "hierarchy", source_type: "product", target_type: "roadmap" },
1225
- product_categorises_by_theme: { forward_verb: "categorises_by", reverse_verb: "categorises", classification: "hierarchy", source_type: "product", target_type: "theme" },
1232
+ product_categorises_by_roadmap_theme: { forward_verb: "categorises_by", reverse_verb: "categorises", classification: "hierarchy", source_type: "product", target_type: "roadmap_theme" },
1226
1233
  feature_area_contains_feature: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "feature_area", target_type: "feature" },
1227
1234
  feature_area_contains_feature_area: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "feature_area", target_type: "feature_area" },
1228
1235
  outcome_delivered_by_feature: { forward_verb: "delivered_by", reverse_verb: "delivers", classification: "cross-domain", source_type: "outcome", target_type: "feature" },
@@ -1246,12 +1253,12 @@ var UPG_EDGE_CATALOG = {
1246
1253
  release_contains_bug: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "release", target_type: "bug" },
1247
1254
  release_documented_in_changelog: { forward_verb: "documented_in", reverse_verb: "documents", classification: "hierarchy", source_type: "release", target_type: "changelog" },
1248
1255
  roadmap_contains_roadmap_item: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "roadmap", target_type: "roadmap_item" },
1249
- roadmap_categorised_by_theme: { forward_verb: "categorised_by", reverse_verb: "categorises", classification: "hierarchy", source_type: "roadmap", target_type: "theme" },
1256
+ roadmap_categorised_by_roadmap_theme: { forward_verb: "categorised_by", reverse_verb: "categorises", classification: "hierarchy", source_type: "roadmap", target_type: "roadmap_theme" },
1250
1257
  roadmap_schedules_release: { forward_verb: "schedules", reverse_verb: "scheduled_in", classification: "hierarchy", source_type: "roadmap", target_type: "release" },
1251
- theme_groups_feature: { forward_verb: "groups", reverse_verb: "grouped_in", classification: "hierarchy", source_type: "theme", target_type: "feature" },
1252
- // feature_area is not contained by theme; themes span multiple
1258
+ roadmap_theme_groups_feature: { forward_verb: "groups", reverse_verb: "grouped_in", classification: "hierarchy", source_type: "roadmap_theme", target_type: "feature" },
1259
+ // feature_area is not contained by roadmap_theme; roadmap themes span multiple
1253
1260
  // areas cross-cuttingly. Containment path: product → feature_area.
1254
- theme_spans_feature_area: { forward_verb: "spans", reverse_verb: "spanned_by", classification: "semantic", source_type: "theme", target_type: "feature_area" },
1261
+ roadmap_theme_spans_feature_area: { forward_verb: "spans", reverse_verb: "spanned_by", classification: "semantic", source_type: "roadmap_theme", target_type: "feature_area" },
1255
1262
  // The legacy `story_task` collapsed into `task` (v0.4.0), so the implements
1256
1263
  // relationship is the canonical `task_implements_user_story` above; there is
1257
1264
  // no separate story_task edge. (v0.2.7 introduced the Statement/Implementation
@@ -2733,7 +2740,8 @@ var UPG_CROSS_EDGE_TYPES = [
2733
2740
  "shares_metric",
2734
2741
  "depends_on_product",
2735
2742
  "cannibalises",
2736
- "succeeds"
2743
+ "succeeds",
2744
+ "hosts"
2737
2745
  ];
2738
2746
  var TYPES = getTypes();
2739
2747
  var TYPES_SET = new Set(TYPES);
@@ -2827,7 +2835,7 @@ var UPG_VALID_CHILDREN = {
2827
2835
  "feature_area",
2828
2836
  "release",
2829
2837
  "roadmap",
2830
- "theme",
2838
+ "roadmap_theme",
2831
2839
  // Engineering
2832
2840
  "bounded_context",
2833
2841
  "code_repository",
@@ -3223,8 +3231,8 @@ var UPG_VALID_CHILDREN = {
3223
3231
  a11y_standard: ["a11y_guideline", "a11y_audit", "a11y_annotation"],
3224
3232
  a11y_audit: ["a11y_issue"],
3225
3233
  // ── Product Specification expansion ─────────────────────────────────────────
3226
- roadmap: ["roadmap_item", "theme", "release"],
3227
- theme: ["feature"],
3234
+ roadmap: ["roadmap_item", "roadmap_theme", "release"],
3235
+ roadmap_theme: ["feature"],
3228
3236
  // ── Unified Context Layer hierarchy ─────────────────────────────────────────
3229
3237
  design_system: [
3230
3238
  "design_component",
@@ -5820,7 +5828,7 @@ var UPG_LIFECYCLE_FREE_TYPES = /* @__PURE__ */ new Set([
5820
5828
  // v0.7.0/UPG-571. ─
5821
5829
  "acceptance_criterion",
5822
5830
  "changelog",
5823
- "theme",
5831
+ "roadmap_theme",
5824
5832
  "user_story",
5825
5833
  // ── Strategy: metric is a measurement definition; metric_quality_assessment
5826
5834
  // is a point-in-time snapshot; value_stream is a mapped flow;
@@ -6121,6 +6129,21 @@ var UPG_SCALES = {
6121
6129
  }
6122
6130
  };
6123
6131
  var UPG_MIGRATIONS = {
6132
+ "0.9.0": [
6133
+ // (since v0.9.0, UPG-660) theme → roadmap_theme. `theme` was the only bare,
6134
+ // unqualified theme among four domain-prefixed siblings (strategic_theme,
6135
+ // content_theme, feedback_theme); it claimed the unqualified word and read as
6136
+ // interchangeable with strategic_theme (the N6/UPG-652 confusion). Renamed to
6137
+ // roadmap_theme so all four read consistently and the customer-problem roadmap
6138
+ // grouping is explicit. Lifecycle-free, identical property surface (theme_scope
6139
+ // / priority); no property migration required. Paired edge renames live in
6140
+ // UPG_EDGE_MIGRATIONS['0.9.0'].
6141
+ {
6142
+ from: "theme",
6143
+ to: "roadmap_theme",
6144
+ reason: 'theme renamed to roadmap_theme (UPG-660). The bare "theme" was the only domain-unprefixed theme among strategic_theme / content_theme / feedback_theme; it grabbed the unqualified word and read as interchangeable with strategic_theme. roadmap_theme makes the customer-problem roadmap grouping explicit and consistent across the four. Same property surface (theme_scope / priority); no property migration needed.'
6145
+ }
6146
+ ],
6124
6147
  "0.7.0": [
6125
6148
  // (since v0.7.0, UPG-571) story_statement → user_story. The v0.2.7 split
6126
6149
  // correctly separated the templated promise from the engineering work
@@ -7103,6 +7126,17 @@ var UPG_SPLIT_MIGRATIONS = {
7103
7126
  ]
7104
7127
  };
7105
7128
  var UPG_EDGE_MIGRATIONS = {
7129
+ "0.9.0": [
7130
+ // (since v0.9.0, UPG-660) theme → roadmap_theme. The four canonical edges that
7131
+ // touch the roadmap theme are renamed to the roadmap_theme form. Endpoint guards
7132
+ // reference the POST-migration (roadmap_theme) types; edge migration runs after
7133
+ // node migration, so by the time these rules apply the node has already been
7134
+ // renamed theme → roadmap_theme (UPG_MIGRATIONS['0.9.0']).
7135
+ { kind: "rename", from: "product_categorises_by_theme", to: "product_categorises_by_roadmap_theme", requires_source_type: "product", requires_target_type: "roadmap_theme", reason: "theme \u2192 roadmap_theme (UPG-660). Product categorises by the roadmap theme; edge key updated to the renamed target type." },
7136
+ { kind: "rename", from: "roadmap_categorised_by_theme", to: "roadmap_categorised_by_roadmap_theme", requires_source_type: "roadmap", requires_target_type: "roadmap_theme", reason: "theme \u2192 roadmap_theme (UPG-660). Roadmap is categorised by the roadmap theme; edge key updated to the renamed target type." },
7137
+ { kind: "rename", from: "theme_groups_feature", to: "roadmap_theme_groups_feature", requires_source_type: "roadmap_theme", requires_target_type: "feature", reason: "theme \u2192 roadmap_theme (UPG-660). The roadmap theme groups features; edge key updated to the renamed source type." },
7138
+ { kind: "rename", from: "theme_spans_feature_area", to: "roadmap_theme_spans_feature_area", requires_source_type: "roadmap_theme", requires_target_type: "feature_area", reason: "theme \u2192 roadmap_theme (UPG-660). The roadmap theme spans feature areas; edge key updated to the renamed source type." }
7139
+ ],
7106
7140
  "0.7.0": [
7107
7141
  // (since v0.7.0, UPG-571) story_statement → user_story re-canon. The four
7108
7142
  // canonical edges that touch the statement are renamed to the user_story
@@ -9919,7 +9953,7 @@ var UPG_PROPERTY_SCHEMA = {
9919
9953
  reach: {
9920
9954
  type: "assessment",
9921
9955
  scale_id: "reach_5",
9922
- description: "How many users experience this problem",
9956
+ description: "How many users experience this problem. @deprecated 0.9.0:a framework-scoped scoring input, not an intrinsic property. Apply the `opportunity-sizing` framework; the value lives on the framework_exercise includes-edge. Removed in 0.9.1.",
9923
9957
  properties: {
9924
9958
  value: { type: "number", description: "The numeric value, used for computation." },
9925
9959
  label: { type: "string", description: "The qualitative label (what the assessor meant)." },
@@ -9931,7 +9965,7 @@ var UPG_PROPERTY_SCHEMA = {
9931
9965
  frequency: {
9932
9966
  type: "assessment",
9933
9967
  scale_id: "frequency_5",
9934
- description: "How often users experience this problem",
9968
+ description: "How often users experience this problem. @deprecated 0.9.0:framework-scoped (opportunity-sizing). Removed in 0.9.1.",
9935
9969
  properties: {
9936
9970
  value: { type: "number", description: "The numeric value, used for computation." },
9937
9971
  label: { type: "string", description: "The qualitative label (what the assessor meant)." },
@@ -9943,7 +9977,7 @@ var UPG_PROPERTY_SCHEMA = {
9943
9977
  pain: {
9944
9978
  type: "assessment",
9945
9979
  scale_id: "pain_5",
9946
- description: "How painful it is when unaddressed",
9980
+ description: "How painful it is when unaddressed. @deprecated 0.9.0:framework-scoped (opportunity-sizing). Removed in 0.9.1.",
9947
9981
  properties: {
9948
9982
  value: { type: "number", description: "The numeric value, used for computation." },
9949
9983
  label: { type: "string", description: "The qualitative label (what the assessor meant)." },
@@ -9952,7 +9986,7 @@ var UPG_PROPERTY_SCHEMA = {
9952
9986
  },
9953
9987
  required: ["value", "label"]
9954
9988
  },
9955
- opportunity_score: { type: "number", description: "Computed: normalized_reach x normalized_frequency x normalized_pain (0-1)" }
9989
+ opportunity_score: { type: "number", description: "Computed: normalized_reach x normalized_frequency x normalized_pain (0-1). @deprecated 0.9.0:computed by the `opportunity-sizing` framework on its application edge, not stored on the entity. Removed in 0.9.1." }
9956
9990
  },
9957
9991
  // OrganizationProperties: Organization entity.
9958
9992
  organization: {
@@ -10366,6 +10400,11 @@ var UPG_PROPERTY_SCHEMA = {
10366
10400
  start_date: { type: "string", description: "ISO date work begins. More precise than `quarter` for continuous planning." },
10367
10401
  target_date: { type: "string", description: "ISO date completion is expected. For shipped items, the actual completion date." }
10368
10402
  },
10403
+ // RoadmapThemeProperties: Thematic grouping of roadmap work, around the customer problem it solves.
10404
+ roadmap_theme: {
10405
+ theme_scope: { type: "string", description: "Scope description" },
10406
+ priority: { type: "string", enum: ["urgent", "high", "medium", "low", "none"], description: "Priority" }
10407
+ },
10369
10408
  // RoleProperties: Role entity.
10370
10409
  role: {
10371
10410
  responsibilities: { type: "string[]", description: "Key responsibilities of the role" },
@@ -10551,7 +10590,7 @@ var UPG_PROPERTY_SCHEMA = {
10551
10590
  reach: {
10552
10591
  type: "assessment",
10553
10592
  scale_id: "reach_5",
10554
- description: "How many users this solution reaches (1 = few, 5 = most)",
10593
+ description: "How many users this solution reaches (1 = few, 5 = most). @deprecated 0.9.0:a framework-scoped scoring input, not an intrinsic property. Apply the `rice-scoring` framework; the value lives on the framework_exercise includes-edge. Removed in 0.9.1.",
10555
10594
  properties: {
10556
10595
  value: { type: "number", description: "The numeric value, used for computation." },
10557
10596
  label: { type: "string", description: "The qualitative label (what the assessor meant)." },
@@ -10563,7 +10602,7 @@ var UPG_PROPERTY_SCHEMA = {
10563
10602
  impact: {
10564
10603
  type: "assessment",
10565
10604
  scale_id: "impact_5",
10566
- description: "Expected impact on the target outcome (1 = minimal, 5 = transformative)",
10605
+ description: "Expected impact on the target outcome (1 = minimal, 5 = transformative). @deprecated 0.9.0:framework-scoped (rice-scoring). Removed in 0.9.1.",
10567
10606
  properties: {
10568
10607
  value: { type: "number", description: "The numeric value, used for computation." },
10569
10608
  label: { type: "string", description: "The qualitative label (what the assessor meant)." },
@@ -10575,7 +10614,7 @@ var UPG_PROPERTY_SCHEMA = {
10575
10614
  confidence: {
10576
10615
  type: "assessment",
10577
10616
  scale_id: "confidence_5",
10578
- description: "How confident the team is in this solution (1 = speculative, 5 = proven)",
10617
+ description: "How confident the team is in this solution (1 = speculative, 5 = proven). @deprecated 0.9.0:framework-scoped (rice-scoring). Removed in 0.9.1.",
10579
10618
  properties: {
10580
10619
  value: { type: "number", description: "The numeric value, used for computation." },
10581
10620
  label: { type: "string", description: "The qualitative label (what the assessor meant)." },
@@ -10587,7 +10626,7 @@ var UPG_PROPERTY_SCHEMA = {
10587
10626
  effort: {
10588
10627
  type: "assessment",
10589
10628
  scale_id: "effort_5",
10590
- description: "Level of effort required to implement (1 = trivial, 5 = very large)",
10629
+ description: "Level of effort required to implement (1 = trivial, 5 = very large). @deprecated 0.9.0:effort is a framework-scoped scoring input (it varies by assessor and method), not an intrinsic property. Removed in 0.9.1.",
10591
10630
  properties: {
10592
10631
  value: { type: "number", description: "The numeric value, used for computation." },
10593
10632
  label: { type: "string", description: "The qualitative label (what the assessor meant)." },
@@ -10596,7 +10635,7 @@ var UPG_PROPERTY_SCHEMA = {
10596
10635
  },
10597
10636
  required: ["value", "label"]
10598
10637
  },
10599
- rice_score: { type: "number", description: "Computed: (reach \xD7 impact \xD7 confidence) / effort" }
10638
+ rice_score: { type: "number", description: "Computed: (reach \xD7 impact \xD7 confidence) / effort. @deprecated 0.9.0:computed by the `rice-scoring` framework on its application edge, not stored on the entity. Removed in 0.9.1." }
10600
10639
  },
10601
10640
  // StakeholderProperties: Stakeholder entity.
10602
10641
  stakeholder: {
@@ -10843,11 +10882,6 @@ var UPG_PROPERTY_SCHEMA = {
10843
10882
  skipped_count: { type: "number", description: "Number of tests that were skipped in the last run" },
10844
10883
  flaky_count: { type: "number", description: "Number of tests that passed only on retry (flaky) in the last run" }
10845
10884
  },
10846
- // ThemeProperties: Thematic grouping of work.
10847
- theme: {
10848
- theme_scope: { type: "string", description: "Scope description" },
10849
- priority: { type: "string", enum: ["urgent", "high", "medium", "low", "none"], description: "Priority" }
10850
- },
10851
10885
  // ThreatProperties: Threat.
10852
10886
  threat: {
10853
10887
  category: { type: "string", description: 'Attack or threat scenario. @example "injection", "misconfiguration", "social engineering", "supply chain"' },
@@ -11076,6 +11110,188 @@ function getPropertySchema(entityType) {
11076
11110
  return UPG_PROPERTY_SCHEMA[entityType];
11077
11111
  }
11078
11112
  var UPG_FRAMEWORKS = [
11113
+ {
11114
+ "id": "opportunity-sizing",
11115
+ "approach_ids": [
11116
+ "prioritise"
11117
+ ],
11118
+ "name": "Opportunity Sizing",
11119
+ "version": "1.0.0",
11120
+ "description": "Size opportunities by Reach, Frequency, and Pain to rank which problems are most worth solving before committing to solutions.",
11121
+ "category": "prioritization",
11122
+ "origin": {
11123
+ "type": "practitioner",
11124
+ "attribution": "Continuous discovery practice",
11125
+ "description": "A lightweight discovery-prioritisation method: weigh how many users hit a problem, how often, and how much it hurts, to rank opportunities before investing in solutions.",
11126
+ "url": "https://www.producttalk.org/2016/08/opportunity-solution-tree/",
11127
+ "year": 2016,
11128
+ "license": "open_attribution"
11129
+ },
11130
+ "tags": [
11131
+ "prioritization",
11132
+ "discovery",
11133
+ "table"
11134
+ ],
11135
+ "slots": [
11136
+ {
11137
+ "label": "Opportunities to size",
11138
+ "entityTypeId": "opportunity",
11139
+ "description": "Opportunities scored on Reach, Frequency, and Pain."
11140
+ }
11141
+ ],
11142
+ "data": {
11143
+ "entity_types": [
11144
+ {
11145
+ "type": "opportunity",
11146
+ "role": "scored_item"
11147
+ }
11148
+ ],
11149
+ "required_properties": {
11150
+ "opportunity": [
11151
+ {
11152
+ "property": "reach",
11153
+ "type": "assessment",
11154
+ "scale_id": "reach_5",
11155
+ "required": true,
11156
+ "scope": "framework",
11157
+ "label": "Reach",
11158
+ "description": "How many users experience this problem?"
11159
+ },
11160
+ {
11161
+ "property": "frequency",
11162
+ "type": "assessment",
11163
+ "scale_id": "frequency_5",
11164
+ "required": true,
11165
+ "scope": "framework",
11166
+ "label": "Frequency",
11167
+ "description": "How often do they run into it?"
11168
+ },
11169
+ {
11170
+ "property": "pain",
11171
+ "type": "assessment",
11172
+ "scale_id": "pain_5",
11173
+ "required": true,
11174
+ "scope": "framework",
11175
+ "label": "Pain",
11176
+ "description": "How painful is it when left unaddressed?"
11177
+ }
11178
+ ]
11179
+ },
11180
+ "computed_properties": [
11181
+ {
11182
+ "property": "opportunity_score",
11183
+ "expression": "reach * frequency * pain",
11184
+ "entity_type": "opportunity",
11185
+ "label": "Opportunity Score",
11186
+ "format": "number"
11187
+ }
11188
+ ],
11189
+ "scoring_method": {
11190
+ "applies_to": [
11191
+ "opportunity"
11192
+ ],
11193
+ "inputs": [
11194
+ {
11195
+ "property": "reach",
11196
+ "type": "assessment",
11197
+ "scale_id": "reach_5",
11198
+ "required": true,
11199
+ "scope": "framework",
11200
+ "label": "Reach",
11201
+ "description": "How many users experience this problem?"
11202
+ },
11203
+ {
11204
+ "property": "frequency",
11205
+ "type": "assessment",
11206
+ "scale_id": "frequency_5",
11207
+ "required": true,
11208
+ "scope": "framework",
11209
+ "label": "Frequency",
11210
+ "description": "How often do they run into it?"
11211
+ },
11212
+ {
11213
+ "property": "pain",
11214
+ "type": "assessment",
11215
+ "scale_id": "pain_5",
11216
+ "required": true,
11217
+ "scope": "framework",
11218
+ "label": "Pain",
11219
+ "description": "How painful is it when left unaddressed?"
11220
+ }
11221
+ ],
11222
+ "computed": [
11223
+ {
11224
+ "property": "opportunity_score",
11225
+ "expression": "reach * frequency * pain",
11226
+ "label": "Opportunity Score",
11227
+ "format": "number"
11228
+ }
11229
+ ]
11230
+ }
11231
+ },
11232
+ "structure": {
11233
+ "pattern": "table"
11234
+ },
11235
+ "presentation": {
11236
+ "layout": {
11237
+ "type": "table",
11238
+ "columns": [
11239
+ {
11240
+ "property": "title",
11241
+ "label": "Opportunities to size",
11242
+ "sortable": true
11243
+ },
11244
+ {
11245
+ "property": "reach",
11246
+ "label": "Reach",
11247
+ "sortable": true,
11248
+ "format": "number"
11249
+ },
11250
+ {
11251
+ "property": "frequency",
11252
+ "label": "Frequency",
11253
+ "sortable": true,
11254
+ "format": "number"
11255
+ },
11256
+ {
11257
+ "property": "pain",
11258
+ "label": "Pain",
11259
+ "sortable": true,
11260
+ "format": "number"
11261
+ },
11262
+ {
11263
+ "property": "opportunity_score",
11264
+ "label": "Opportunity Score",
11265
+ "sortable": true,
11266
+ "format": "score_pill"
11267
+ }
11268
+ ]
11269
+ },
11270
+ "sort_by": {
11271
+ "property": "opportunity_score",
11272
+ "direction": "desc"
11273
+ },
11274
+ "colour_by": "score",
11275
+ "card_fields": [
11276
+ "title",
11277
+ "description",
11278
+ "status"
11279
+ ]
11280
+ },
11281
+ "education": {
11282
+ "purpose": "Rank opportunities by how widely, how often, and how painfully a problem is felt, so discovery effort flows to the problems most worth solving.",
11283
+ "core_question": "Of the problems we could pursue, which affect the most users, most often, with the most pain?",
11284
+ "when_to_use": [
11285
+ "You have more opportunities than you can pursue",
11286
+ "You need to compare problems before committing to solutions",
11287
+ "You want a defensible, transparent way to choose what to explore"
11288
+ ],
11289
+ "when_not_to_use": [
11290
+ "A single opportunity is already validated and obvious",
11291
+ "You have no signal yet on reach, frequency, or pain"
11292
+ ]
11293
+ }
11294
+ },
11079
11295
  {
11080
11296
  "id": "opportunity-solution-tree",
11081
11297
  "approach_ids": [
@@ -12332,7 +12548,7 @@ var UPG_FRAMEWORKS = [
12332
12548
  ],
12333
12549
  "name": "RICE Scoring",
12334
12550
  "version": "1.0.0",
12335
- "description": "Score features, opportunities, and needs by Reach, Impact, Confidence, and Effort to produce a ranked priority list.",
12551
+ "description": "Score features, solutions, opportunities, and needs by Reach, Impact, Confidence, and Effort to produce a ranked priority list.",
12336
12552
  "category": "prioritization",
12337
12553
  "origin": {
12338
12554
  "type": "practitioner",
@@ -12357,6 +12573,11 @@ var UPG_FRAMEWORKS = [
12357
12573
  "entityTypeId": "opportunity",
12358
12574
  "description": "Opportunities scored on the same RICE Scoring inputs as features."
12359
12575
  },
12576
+ {
12577
+ "label": "Solutions to score",
12578
+ "entityTypeId": "solution",
12579
+ "description": "Solutions scored on the same RICE Scoring inputs as features."
12580
+ },
12360
12581
  {
12361
12582
  "label": "Needs to score",
12362
12583
  "entityTypeId": "need",
@@ -12373,6 +12594,10 @@ var UPG_FRAMEWORKS = [
12373
12594
  "type": "opportunity",
12374
12595
  "role": "scored_item"
12375
12596
  },
12597
+ {
12598
+ "type": "solution",
12599
+ "role": "scored_item"
12600
+ },
12376
12601
  {
12377
12602
  "type": "need",
12378
12603
  "role": "scored_item"
@@ -12455,6 +12680,44 @@ var UPG_FRAMEWORKS = [
12455
12680
  "description": "How much work is required to build and ship this, on the effort scale?"
12456
12681
  }
12457
12682
  ],
12683
+ "solution": [
12684
+ {
12685
+ "property": "reach",
12686
+ "type": "assessment",
12687
+ "scale_id": "reach_5",
12688
+ "required": true,
12689
+ "scope": "framework",
12690
+ "label": "Reach",
12691
+ "description": "How many users will this impact per quarter?"
12692
+ },
12693
+ {
12694
+ "property": "impact",
12695
+ "type": "assessment",
12696
+ "scale_id": "impact_5",
12697
+ "required": true,
12698
+ "scope": "framework",
12699
+ "label": "Impact",
12700
+ "description": "How much will this impact each user, on the impact scale?"
12701
+ },
12702
+ {
12703
+ "property": "confidence",
12704
+ "type": "assessment",
12705
+ "scale_id": "confidence_5",
12706
+ "required": true,
12707
+ "scope": "framework",
12708
+ "label": "Confidence",
12709
+ "description": "How confident are you in the reach, impact, and effort estimates?"
12710
+ },
12711
+ {
12712
+ "property": "effort",
12713
+ "type": "assessment",
12714
+ "scale_id": "effort_5",
12715
+ "required": true,
12716
+ "scope": "framework",
12717
+ "label": "Effort",
12718
+ "description": "How much work is required to build and ship this, on the effort scale?"
12719
+ }
12720
+ ],
12458
12721
  "need": [
12459
12722
  {
12460
12723
  "property": "reach",
@@ -12509,6 +12772,13 @@ var UPG_FRAMEWORKS = [
12509
12772
  "label": "RICE Score",
12510
12773
  "format": "number"
12511
12774
  },
12775
+ {
12776
+ "property": "rice_score",
12777
+ "expression": "(reach * impact * confidence) / effort",
12778
+ "entity_type": "solution",
12779
+ "label": "RICE Score",
12780
+ "format": "number"
12781
+ },
12512
12782
  {
12513
12783
  "property": "rice_score",
12514
12784
  "expression": "(reach * impact * confidence) / effort",
@@ -12521,6 +12791,7 @@ var UPG_FRAMEWORKS = [
12521
12791
  "applies_to": [
12522
12792
  "feature",
12523
12793
  "opportunity",
12794
+ "solution",
12524
12795
  "need"
12525
12796
  ],
12526
12797
  "inputs": [
@@ -16767,7 +17038,8 @@ var STANDARD_LABELS = {
16767
17038
  product: { alt_labels: ["offering", "app", "service", "platform"] },
16768
17039
  vision: { alt_labels: ["product vision", "north star vision", "long-term vision"] },
16769
17040
  mission: { alt_labels: ["mission statement", "purpose"] },
16770
- strategic_theme: { alt_labels: ["theme", "strategic pillar", "focus area"] },
17041
+ strategic_theme: { alt_labels: ["focus area", "strategic focus area"] },
17042
+ // N6: not 'theme'/'strategic pillar' (own types)
16771
17043
  initiative: { alt_labels: ["strategic initiative", "program initiative", "workstream"] },
16772
17044
  capability: { alt_labels: ["business capability", "organizational capability"] },
16773
17045
  value_stream: { alt_labels: ["value chain", "stream"] },
@@ -16829,7 +17101,8 @@ var STANDARD_LABELS = {
16829
17101
  fix: { alt_labels: ["bugfix", "patch", "remediation"] },
16830
17102
  roadmap: { alt_labels: ["product roadmap", "release plan", "timeline"] },
16831
17103
  roadmap_item: { alt_labels: ["roadmap entry", "planned item"] },
16832
- theme: { alt_labels: ["product theme", "bucket", "category"] },
17104
+ roadmap_theme: { alt_labels: ["product theme", "roadmap theme"] },
17105
+ // UPG-660: renamed from bare 'theme' (N6 lineage)
16833
17106
  // Engineering layer
16834
17107
  bounded_context: { alt_labels: ["context", "domain boundary", "module boundary"] },
16835
17108
  service: { alt_labels: ["microservice", "backend service", "api service"] },
@@ -17028,7 +17301,8 @@ var STANDARD_LABELS = {
17028
17301
  feedback_vote: { alt_labels: ["upvote", "vote", "user vote"] },
17029
17302
  user_advisory_board: { alt_labels: ["cab", "customer advisory board", "advisory council"] },
17030
17303
  beta_program: { alt_labels: ["beta", "early access", "preview program"] },
17031
- feedback_theme: { alt_labels: ["feedback cluster", "theme", "feedback category"] },
17304
+ feedback_theme: { alt_labels: ["feedback cluster", "feedback category"] },
17305
+ // N6: not bare 'theme'
17032
17306
  // Pricing & Packaging layer
17033
17307
  pricing_strategy: { alt_labels: ["pricing model", "monetization strategy"] },
17034
17308
  package: { alt_labels: ["product package", "bundle", "sku"] },
@@ -17532,9 +17806,9 @@ var PRODUCT_DELIVERY_PLAYBOOK = {
17532
17806
  ),
17533
17807
  seqStep(
17534
17808
  6,
17535
- "Themes & Changelog",
17536
- ["theme", "changelog"],
17537
- "Group releases into strategic themes. Maintain a changelog the team and customers can read together."
17809
+ "Roadmap Themes & Changelog",
17810
+ ["roadmap_theme", "changelog"],
17811
+ "Group roadmap work into roadmap themes around the customer problem. Maintain a changelog the team and customers can read together."
17538
17812
  )
17539
17813
  ]
17540
17814
  };
@@ -18788,7 +19062,7 @@ var PRODUCT_SPEC_GUIDE = {
18788
19062
  // registered to product_spec but missing from the navigation order).
18789
19063
  // `changelog` lives here because it is a structural product-shipping
18790
19064
  // artefact; content domain references it only via cross-domain bridges.
18791
- creation_sequence: ["feature_area", "feature", "epic", "user_story", "acceptance_criterion", "task", "bug", "release", "roadmap", "roadmap_item", "theme", "changelog"],
19065
+ creation_sequence: ["feature_area", "feature", "epic", "user_story", "acceptance_criterion", "task", "bug", "release", "roadmap", "roadmap_item", "roadmap_theme", "changelog"],
18792
19066
  patterns: [
18793
19067
  {
18794
19068
  name: "Feature Decomposition",
@@ -23213,7 +23487,7 @@ var UPG_REGIONS = [
23213
23487
  role: "leaf"
23214
23488
  },
23215
23489
  {
23216
- type: "theme",
23490
+ type: "roadmap_theme",
23217
23491
  role: "container",
23218
23492
  notes: "semantic spanner, not containment"
23219
23493
  },
@@ -23249,10 +23523,10 @@ var UPG_REGIONS = [
23249
23523
  "user_story_verified_by_acceptance_criterion",
23250
23524
  "task_implements_user_story",
23251
23525
  "roadmap_contains_roadmap_item",
23252
- "roadmap_categorised_by_theme",
23526
+ "roadmap_categorised_by_roadmap_theme",
23253
23527
  "roadmap_schedules_release",
23254
23528
  "release_documented_in_changelog",
23255
- "theme_spans_feature_area",
23529
+ "roadmap_theme_spans_feature_area",
23256
23530
  "milestone_gates_release",
23257
23531
  "roadmap_item_references_feature",
23258
23532
  "feature_request_voted_on_by_feedback_vote"
@@ -24372,7 +24646,7 @@ function serializePortfolioWithHeader(doc, opts) {
24372
24646
  header.integrity = { algorithm: INTEGRITY_ALGORITHM, body: computeBodyChecksum(doc) };
24373
24647
  return JSON.stringify({ $upg: header, ...body }, null, 2) + "\n";
24374
24648
  }
24375
- var UPG_VERSION = "0.8.15";
24649
+ var UPG_VERSION = "0.9.0";
24376
24650
  var MARKDOWN_FORMAT_VERSION = "0.1";
24377
24651
  var UPG_TYPES = getTypes();
24378
24652
  var UPG_TYPES_SET = new Set(UPG_TYPES);
@@ -26257,6 +26531,10 @@ import {
26257
26531
  writePortfolioScopedNode as writePortfolioScopedNode2,
26258
26532
  openPortfolioStoreIfExists,
26259
26533
  assignProductToArea,
26534
+ updateProductArea,
26535
+ removeProductFromArea,
26536
+ deleteArea,
26537
+ moveProductToArea,
26260
26538
  PortfolioRoutingError as PortfolioRoutingError2
26261
26539
  } from "@unified-product-graph/sdk";
26262
26540
  var listProductAreas = async (_args, _ctx) => {
@@ -26479,6 +26757,73 @@ var getChanges = (args, ctx) => {
26479
26757
  )
26480
26758
  );
26481
26759
  };
26760
+ var updateAreaTool = async (args, _ctx) => {
26761
+ const areaId = args.area_id;
26762
+ if (!areaId) return textError("Missing required parameter: area_id");
26763
+ const hasField = args.title !== void 0 || args.description !== void 0 || args.strategic_priority !== void 0 || args.owner !== void 0 || "parent_area_id" in args;
26764
+ if (!hasField) {
26765
+ return textError(
26766
+ "Nothing to update: pass at least one of: title, description, strategic_priority, owner, parent_area_id."
26767
+ );
26768
+ }
26769
+ try {
26770
+ const result = await updateProductArea(process.cwd(), areaId, {
26771
+ title: args.title,
26772
+ description: args.description,
26773
+ strategic_priority: args.strategic_priority,
26774
+ owner: args.owner,
26775
+ // Tri-state: present (incl. null) re-parents/un-nests; absent leaves unchanged.
26776
+ ..."parent_area_id" in args ? { parent_area_id: args.parent_area_id ?? null } : {}
26777
+ });
26778
+ return text(
26779
+ JSON.stringify({ message: `Updated area (${result.updated.join(", ")})`, ...result }, null, 2)
26780
+ );
26781
+ } catch (err) {
26782
+ if (err instanceof PortfolioRoutingError2) return textError(err.message);
26783
+ return textError(err.message);
26784
+ }
26785
+ };
26786
+ var removeProductFromAreaTool = async (args, _ctx) => {
26787
+ const productId = args.product_id;
26788
+ const areaId = args.area_id;
26789
+ if (!productId) return textError("Missing required parameter: product_id");
26790
+ if (!areaId) return textError("Missing required parameter: area_id");
26791
+ try {
26792
+ const result = await removeProductFromArea(process.cwd(), { product_id: productId, area_id: areaId });
26793
+ return text(JSON.stringify(result, null, 2));
26794
+ } catch (err) {
26795
+ if (err instanceof PortfolioRoutingError2) return textError(err.message);
26796
+ return textError(err.message);
26797
+ }
26798
+ };
26799
+ var deleteAreaTool = async (args, _ctx) => {
26800
+ const areaId = args.area_id;
26801
+ if (!areaId) return textError("Missing required parameter: area_id");
26802
+ try {
26803
+ const result = await deleteArea(process.cwd(), areaId, { force: args.force });
26804
+ return text(JSON.stringify({ message: `Deleted area ${areaId}`, ...result }, null, 2));
26805
+ } catch (err) {
26806
+ if (err instanceof PortfolioRoutingError2) return textError(err.message);
26807
+ return textError(err.message);
26808
+ }
26809
+ };
26810
+ var moveProductToAreaTool = async (args, _ctx) => {
26811
+ const productId = args.product_id;
26812
+ const toAreaId = args.to_area_id;
26813
+ if (!productId) return textError("Missing required parameter: product_id");
26814
+ if (!toAreaId) return textError("Missing required parameter: to_area_id");
26815
+ try {
26816
+ const result = await moveProductToArea(process.cwd(), {
26817
+ product_id: productId,
26818
+ to_area_id: toAreaId,
26819
+ from_area_id: args.from_area_id
26820
+ });
26821
+ return text(JSON.stringify(result, null, 2));
26822
+ } catch (err) {
26823
+ if (err instanceof PortfolioRoutingError2) return textError(err.message);
26824
+ return textError(err.message);
26825
+ }
26826
+ };
26482
26827
 
26483
26828
  // src/tools/workspace.ts
26484
26829
  import * as fs from "fs";
@@ -26491,7 +26836,9 @@ import {
26491
26836
  openPortfolioStoreIfExists as openPortfolioStoreIfExists2,
26492
26837
  registerProductOnPortfolio,
26493
26838
  findProductFileById,
26494
- attachProductToPortfolio
26839
+ attachProductToPortfolio,
26840
+ detachProductFromPortfolio,
26841
+ deleteCrossProductEdge
26495
26842
  } from "@unified-product-graph/sdk";
26496
26843
  import {
26497
26844
  createProduct,
@@ -26505,6 +26852,25 @@ import {
26505
26852
  var listLocalProducts = (_args, _ctx) => {
26506
26853
  const cwd = process.cwd();
26507
26854
  const products = [];
26855
+ const membership = /* @__PURE__ */ new Map();
26856
+ try {
26857
+ const pdoc = JSON.parse(fs.readFileSync(path3.join(cwd, ".upg", "portfolio.upg"), "utf-8"));
26858
+ for (const area of pdoc.product_areas ?? []) {
26859
+ for (const pid of area.products ?? []) {
26860
+ const m = membership.get(pid) ?? { areas: [], portfolios: [] };
26861
+ m.areas.push(area.title ?? area.id);
26862
+ membership.set(pid, m);
26863
+ }
26864
+ }
26865
+ for (const pf of pdoc.portfolios ?? []) {
26866
+ for (const pid of pf.products ?? []) {
26867
+ const m = membership.get(pid) ?? { areas: [], portfolios: [] };
26868
+ m.portfolios.push(pf.title ?? pf.id);
26869
+ membership.set(pid, m);
26870
+ }
26871
+ }
26872
+ } catch {
26873
+ }
26508
26874
  const candidates = [];
26509
26875
  const topEntries = fs.readdirSync(cwd, { withFileTypes: true });
26510
26876
  for (const entry of topEntries) {
@@ -26529,13 +26895,19 @@ var listLocalProducts = (_args, _ctx) => {
26529
26895
  try {
26530
26896
  const raw = fs.readFileSync(filePath, "utf-8");
26531
26897
  const doc = JSON.parse(raw);
26898
+ if (!doc.product) continue;
26532
26899
  const coerced = coerceProductStage(doc.product?.stage);
26900
+ const pid = doc.product?.id ?? null;
26901
+ const m = pid ? membership.get(pid) : void 0;
26533
26902
  products.push({
26903
+ id: pid,
26534
26904
  file: path3.relative(cwd, filePath),
26535
26905
  title: doc.product?.title ?? "(untitled)",
26536
26906
  stage: coerced.canonical ?? null,
26537
26907
  nodes: Array.isArray(doc.nodes) ? doc.nodes.length : 0,
26538
- edges: Array.isArray(doc.edges) ? doc.edges.length : 0
26908
+ edges: Array.isArray(doc.edges) ? doc.edges.length : 0,
26909
+ ...m && m.areas.length > 0 ? { areas: m.areas } : {},
26910
+ ...m && m.portfolios.length > 0 ? { portfolios: m.portfolios } : {}
26539
26911
  });
26540
26912
  } catch {
26541
26913
  }
@@ -26926,6 +27298,126 @@ var attachProductToPortfolioTool = async (args, _ctx) => {
26926
27298
  return textError(err.message);
26927
27299
  }
26928
27300
  };
27301
+ var detachProductFromPortfolioTool = async (args, _ctx) => {
27302
+ const productId = args.product_id;
27303
+ const portfolioId = args.portfolio_id;
27304
+ if (!productId) return textError("Missing required parameter: product_id");
27305
+ if (!portfolioId) return textError("Missing required parameter: portfolio_id");
27306
+ try {
27307
+ const result = await detachProductFromPortfolio(process.cwd(), {
27308
+ product_id: productId,
27309
+ portfolio_id: portfolioId
27310
+ });
27311
+ return text(JSON.stringify(result, null, 2));
27312
+ } catch (err) {
27313
+ return textError(err.message);
27314
+ }
27315
+ };
27316
+ var deleteCrossProductEdgeTool = async (args, _ctx) => {
27317
+ const edgeIdArg = args.edge_id;
27318
+ if (!edgeIdArg) return textError("Missing required parameter: edge_id");
27319
+ try {
27320
+ const result = await deleteCrossProductEdge(process.cwd(), edgeIdArg);
27321
+ return text(JSON.stringify(result, null, 2));
27322
+ } catch (err) {
27323
+ return textError(err.message);
27324
+ }
27325
+ };
27326
+ var batchCreateCrossProductEdges = async (args, _ctx) => {
27327
+ const edgesArg = args.edges;
27328
+ if (!Array.isArray(edgesArg) || edgesArg.length === 0) {
27329
+ return textError("Missing required parameter: edges (a non-empty array).");
27330
+ }
27331
+ if (edgesArg.length > 50) {
27332
+ return textError(`Too many edges: ${edgesArg.length}. Max 50 per batch_create_cross_product_edges call.`);
27333
+ }
27334
+ const cwd = process.cwd();
27335
+ const portfolioPath = resolvePortfolioPath(cwd);
27336
+ if (!portfolioPath) {
27337
+ return textError("No workspace found. Run `init_workspace` first to enable portfolio cross-product edges.");
27338
+ }
27339
+ const autoCreatePortfolio = args.auto_create_portfolio ?? false;
27340
+ const portfolioExisted = fs.existsSync(portfolioPath);
27341
+ if (!portfolioExisted && !autoCreatePortfolio) {
27342
+ return textError(
27343
+ 'No portfolio document found at .upg/portfolio.upg. Cross-product edges express portfolio-level relationships and should be anchored to a portfolio that contains the products. Create a portfolio first (`create_node({type: "portfolio", title: "..."})`), or pass `auto_create_portfolio: true`.'
27344
+ );
27345
+ }
27346
+ const prepared = [];
27347
+ for (let i = 0; i < edgesArg.length; i++) {
27348
+ const e = edgesArg[i];
27349
+ const sourceIdArg = e.source_id;
27350
+ const targetIdArg = e.target_id;
27351
+ const edgeTypeArg = e.type;
27352
+ const sourceProductId = e.source_product_id;
27353
+ const targetProductId = e.target_product_id;
27354
+ if (!sourceIdArg) return textError(`edges[${i}]: missing source_id`);
27355
+ if (!targetIdArg) return textError(`edges[${i}]: missing target_id`);
27356
+ if (!edgeTypeArg) return textError(`edges[${i}]: missing type`);
27357
+ if (!UPG_CROSS_EDGE_TYPES.includes(edgeTypeArg)) {
27358
+ return textError(`edges[${i}]: invalid cross-product edge type "${edgeTypeArg}". Valid types: ${UPG_CROSS_EDGE_TYPES.join(", ")}`);
27359
+ }
27360
+ let qualifiedSource;
27361
+ if (sourceIdArg.includes("/")) qualifiedSource = sourceIdArg;
27362
+ else if (sourceProductId) qualifiedSource = `${sourceProductId}/${sourceIdArg}`;
27363
+ else return textError(`edges[${i}]: source_id "${sourceIdArg}" is a bare node id. Supply source_product_id or a qualified {product_id}/{node_id}.`);
27364
+ let qualifiedTarget;
27365
+ if (targetIdArg.includes("/")) qualifiedTarget = targetIdArg;
27366
+ else if (targetProductId) qualifiedTarget = `${targetProductId}/${targetIdArg}`;
27367
+ else return textError(`edges[${i}]: target_id "${targetIdArg}" is a bare node id. Supply target_product_id or a qualified {product_id}/{node_id}.`);
27368
+ prepared.push({
27369
+ id: edgeId3(),
27370
+ source: qualifiedSource,
27371
+ target: qualifiedTarget,
27372
+ type: edgeTypeArg,
27373
+ source_product_id: sourceProductId ?? qualifiedSource.split("/")[0],
27374
+ target_product_id: targetProductId ?? qualifiedTarget.split("/")[0]
27375
+ });
27376
+ }
27377
+ const portfolioStore = new UPGPortfolioStore();
27378
+ try {
27379
+ await portfolioStore.loadOrInit(portfolioPath);
27380
+ } catch (err) {
27381
+ return textError(`Failed to load portfolio document: ${err.message}`);
27382
+ }
27383
+ const registeredProducts = [];
27384
+ const portfolioDoc = portfolioStore.getDocument();
27385
+ if (portfolioDoc) {
27386
+ const productIds = /* @__PURE__ */ new Set();
27387
+ for (const e of prepared) {
27388
+ if (e.source_product_id) productIds.add(e.source_product_id);
27389
+ if (e.target_product_id) productIds.add(e.target_product_id);
27390
+ }
27391
+ for (const pid of productIds) {
27392
+ const lookup = findProductFileById(cwd, pid);
27393
+ const wasNew = registerProductOnPortfolio(portfolioDoc, {
27394
+ id: pid,
27395
+ ...lookup ? { file_path: lookup.file_path, title: lookup.title } : {}
27396
+ });
27397
+ if (wasNew) registeredProducts.push({ id: pid, ...lookup ? { file_path: lookup.file_path, title: lookup.title } : {} });
27398
+ }
27399
+ if (registeredProducts.length > 0) portfolioStore.markDirty();
27400
+ }
27401
+ try {
27402
+ for (const e of prepared) portfolioStore.addCrossEdge(e);
27403
+ await portfolioStore.flush();
27404
+ } catch (err) {
27405
+ return textError(`Failed to write cross-product edges: ${err.message}`);
27406
+ }
27407
+ return text(
27408
+ JSON.stringify(
27409
+ {
27410
+ message: `Created ${prepared.length} cross-product edge(s)`,
27411
+ created: prepared,
27412
+ count: prepared.length,
27413
+ portfolio_file: path3.relative(cwd, portfolioPath),
27414
+ ...registeredProducts.length > 0 ? { registered_products: registeredProducts } : {}
27415
+ },
27416
+ null,
27417
+ 2
27418
+ )
27419
+ );
27420
+ };
26929
27421
 
26930
27422
  // src/tools/schema.ts
26931
27423
  var getEntitySchema = (args, _ctx) => {
@@ -29552,7 +30044,7 @@ var TOOL_DEFINITIONS = [
29552
30044
  },
29553
30045
  {
29554
30046
  name: "list_cross_edge_types",
29555
- description: "List the canonical cross-product edge types from `UPG_CROSS_EDGE_TYPES`: `shares_persona`, `shares_competitor`, `shares_metric`, `depends_on_product`, `cannibalises`, `succeeds`. Portfolio-level relationships across products. Distinct from the within-product `UPG_EDGE_CATALOG`.",
30047
+ description: "List the canonical cross-product edge types from `UPG_CROSS_EDGE_TYPES`: `shares_persona`, `shares_competitor`, `shares_metric`, `depends_on_product`, `cannibalises`, `succeeds`, `hosts`. Portfolio-level relationships across products. Distinct from the within-product `UPG_EDGE_CATALOG`.",
29556
30048
  inputSchema: { type: "object", properties: {} }
29557
30049
  },
29558
30050
  {
@@ -29937,6 +30429,66 @@ var TOOL_DEFINITIONS = [
29937
30429
  required: ["product_id", "area_id"]
29938
30430
  }
29939
30431
  },
30432
+ {
30433
+ name: "update_area",
30434
+ description: "Edit a product area in `.upg/portfolio.upg` (title, description, strategic_priority, owner) and/or re-parent it via `parent_area_id`. The mirror of `update_product` for the organisational axis. `parent_area_id` is tri-state: omit to leave unchanged, pass null to un-nest (top-level), or pass an area id to re-parent (rejected if it would create a cycle).",
30435
+ inputSchema: {
30436
+ type: "object",
30437
+ properties: {
30438
+ area_id: { type: "string", description: "Product area id to edit (from list_product_areas)" },
30439
+ title: { type: "string", description: "New area title" },
30440
+ description: { type: "string", description: "New area description" },
30441
+ strategic_priority: {
30442
+ type: "string",
30443
+ enum: ["urgent", "high", "medium", "low", "none"],
30444
+ description: "Strategic priority (canonical Priority scale)"
30445
+ },
30446
+ parent_area_id: {
30447
+ type: ["string", "null"],
30448
+ description: "Re-parent under this area id; null un-nests (top-level); omit to leave unchanged"
30449
+ },
30450
+ owner: { type: "string", description: "Person or team that owns this area" }
30451
+ },
30452
+ required: ["area_id"]
30453
+ }
30454
+ },
30455
+ {
30456
+ name: "remove_product_from_area",
30457
+ description: "Remove a product from a product area's `products[]` in `.upg/portfolio.upg` (the product stays registered on the portfolio and in any other container). The inverse of `assign_product_to_area`.",
30458
+ inputSchema: {
30459
+ type: "object",
30460
+ properties: {
30461
+ product_id: { type: "string", description: "Product id (from list_local_products)" },
30462
+ area_id: { type: "string", description: "Product area id (from list_product_areas)" }
30463
+ },
30464
+ required: ["product_id", "area_id"]
30465
+ }
30466
+ },
30467
+ {
30468
+ name: "delete_area",
30469
+ description: "Delete a product area from `.upg/portfolio.upg`. Guarded: refuses while the area still has products unless `force: true`. Child areas are un-nested (their parent link is cleared) so no parent reference dangles.",
30470
+ inputSchema: {
30471
+ type: "object",
30472
+ properties: {
30473
+ area_id: { type: "string", description: "Product area id to delete (from list_product_areas)" },
30474
+ force: { type: "boolean", description: "Delete even if the area still has products (default false)" }
30475
+ },
30476
+ required: ["area_id"]
30477
+ }
30478
+ },
30479
+ {
30480
+ name: "move_product_to_area",
30481
+ description: "Move a product to a different product area: remove it from `from_area_id` (or, when omitted, from every area it currently sits in) and add it to `to_area_id`. Convenience over remove_product_from_area + assign_product_to_area.",
30482
+ inputSchema: {
30483
+ type: "object",
30484
+ properties: {
30485
+ product_id: { type: "string", description: "Product id (from list_local_products)" },
30486
+ to_area_id: { type: "string", description: "Destination product area id (from list_product_areas)" },
30487
+ from_area_id: { type: "string", description: "Source area id to remove from; omit to remove from all areas" }
30488
+ },
30489
+ required: ["product_id", "to_area_id"]
30490
+ }
30491
+ },
29940
30492
  {
29941
30493
  name: "attach_product_to_portfolio",
29942
30494
  description: "Place an existing product under a portfolio (adds it to the portfolio's `products[]` in `.upg/portfolio.upg`). Resolves the portfolio against the portfolio document and auto-registers the product on the portfolio registry. Use after `create_product`, or pass `portfolio_id` to `create_product` directly.",
@@ -29949,6 +30501,18 @@ var TOOL_DEFINITIONS = [
29949
30501
  required: ["product_id", "portfolio_id"]
29950
30502
  }
29951
30503
  },
30504
+ {
30505
+ name: "detach_product_from_portfolio",
30506
+ description: "Remove a product from a portfolio's `products[]` in `.upg/portfolio.upg` (the product stays registered and in any other container). The inverse of `attach_product_to_portfolio`.",
30507
+ inputSchema: {
30508
+ type: "object",
30509
+ properties: {
30510
+ product_id: { type: "string", description: "Product id (from list_local_products)" },
30511
+ portfolio_id: { type: "string", description: "Portfolio id (from list_portfolios)" }
30512
+ },
30513
+ required: ["product_id", "portfolio_id"]
30514
+ }
30515
+ },
29952
30516
  {
29953
30517
  name: "list_portfolios",
29954
30518
  description: "List portfolios from the portfolio document (`.upg/portfolio.upg`). Portfolios represent the strategic axis (where we invest). Returns an empty list when no portfolio document exists yet.",
@@ -29961,7 +30525,7 @@ var TOOL_DEFINITIONS = [
29961
30525
  },
29962
30526
  {
29963
30527
  name: "create_cross_product_edge",
29964
- description: "Create a cross-product relationship between two entities in different products within a portfolio graph. Types: `shares_persona`, `shares_competitor`, `shares_metric`, `depends_on_product`, `cannibalises`, `succeeds`.",
30528
+ description: "Create a cross-product relationship between two entities in different products within a portfolio graph. Types: `shares_persona`, `shares_competitor`, `shares_metric`, `depends_on_product`, `cannibalises`, `succeeds`, `hosts` (host product runs the hosted product inside itself, directed host to hosted).",
29965
30529
  inputSchema: {
29966
30530
  type: "object",
29967
30531
  properties: {
@@ -29969,7 +30533,7 @@ var TOOL_DEFINITIONS = [
29969
30533
  target_id: { type: "string", description: "Target node ID" },
29970
30534
  type: {
29971
30535
  type: "string",
29972
- enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds"],
30536
+ enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds", "hosts"],
29973
30537
  description: "Cross-product relationship type"
29974
30538
  },
29975
30539
  source_product_id: { type: "string", description: "Product ID of the source node" },
@@ -29978,6 +30542,47 @@ var TOOL_DEFINITIONS = [
29978
30542
  required: ["source_id", "target_id", "type"]
29979
30543
  }
29980
30544
  },
30545
+ {
30546
+ name: "delete_cross_product_edge",
30547
+ description: "Delete a cross-product edge from `.upg/portfolio.upg` by id. The inverse of `create_cross_product_edge`. Returns `deleted: false` (not an error) when no edge with that id exists.",
30548
+ inputSchema: {
30549
+ type: "object",
30550
+ properties: {
30551
+ edge_id: { type: "string", description: "Cross-product edge id (from list_portfolio_cross_edges)" }
30552
+ },
30553
+ required: ["edge_id"]
30554
+ }
30555
+ },
30556
+ {
30557
+ name: "batch_create_cross_product_edges",
30558
+ description: "Create up to 50 cross-product edges in one atomic write (the portfolio-tier mirror of batch_create_edges). Every edge is validated and qualified before anything is written; if any is invalid the whole batch is rejected. Referenced products are auto-registered.",
30559
+ inputSchema: {
30560
+ type: "object",
30561
+ properties: {
30562
+ edges: {
30563
+ type: "array",
30564
+ description: "Cross-product edges to create (max 50). Each: { source_id, target_id, type, source_product_id?, target_product_id? }.",
30565
+ items: {
30566
+ type: "object",
30567
+ properties: {
30568
+ source_id: { type: "string", description: "Source node ID (bare or qualified {product_id}/{node_id})" },
30569
+ target_id: { type: "string", description: "Target node ID (bare or qualified {product_id}/{node_id})" },
30570
+ type: {
30571
+ type: "string",
30572
+ enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds", "hosts"],
30573
+ description: "Cross-product relationship type"
30574
+ },
30575
+ source_product_id: { type: "string", description: "Product ID of the source node (qualifies a bare source_id)" },
30576
+ target_product_id: { type: "string", description: "Product ID of the target node (qualifies a bare target_id)" }
30577
+ },
30578
+ required: ["source_id", "target_id", "type"]
30579
+ }
30580
+ },
30581
+ auto_create_portfolio: { type: "boolean", description: "Create an empty portfolio document if none exists (default false)" }
30582
+ },
30583
+ required: ["edges"]
30584
+ }
30585
+ },
29981
30586
  {
29982
30587
  name: "list_portfolio_cross_edges",
29983
30588
  description: "List all cross-product edges stored in the portfolio document (`.upg/portfolio.upg`). Empty list when the portfolio document is absent.",
@@ -30157,10 +30762,17 @@ var HANDLERS = {
30157
30762
  get_area_context: getAreaContext,
30158
30763
  create_area: createArea,
30159
30764
  assign_product_to_area: assignProductToAreaTool,
30765
+ update_area: updateAreaTool,
30766
+ remove_product_from_area: removeProductFromAreaTool,
30767
+ delete_area: deleteAreaTool,
30768
+ move_product_to_area: moveProductToAreaTool,
30160
30769
  list_portfolios: listPortfolios,
30161
30770
  get_organization: getOrganization,
30162
30771
  create_cross_product_edge: createCrossProductEdge,
30772
+ delete_cross_product_edge: deleteCrossProductEdgeTool,
30773
+ batch_create_cross_product_edges: batchCreateCrossProductEdges,
30163
30774
  attach_product_to_portfolio: attachProductToPortfolioTool,
30775
+ detach_product_from_portfolio: detachProductFromPortfolioTool,
30164
30776
  list_portfolio_cross_edges: listPortfolioCrossEdges,
30165
30777
  migrate_cross_edges: migrateCrossEdges,
30166
30778
  get_sync_state: getSyncState,