@unified-product-graph/mcp-server 0.8.16 → 0.9.1

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
@@ -2,16 +2,16 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { parseArgs } from "util";
5
- import * as fs3 from "fs/promises";
6
- import * as path6 from "path";
7
- import { UPGFileStore } from "@unified-product-graph/sdk";
5
+ import * as fs4 from "fs/promises";
6
+ import * as path7 from "path";
7
+ import { UPGFileStore as UPGFileStore2 } from "@unified-product-graph/sdk";
8
8
 
9
9
  // src/server.ts
10
10
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
- import fs2 from "fs";
12
+ import fs3 from "fs";
13
13
  import { fileURLToPath as fileURLToPath2 } from "url";
14
- import * as path5 from "path";
14
+ import * as path6 from "path";
15
15
  import {
16
16
  CallToolRequestSchema,
17
17
  ListToolsRequestSchema
@@ -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
@@ -2734,7 +2741,8 @@ var UPG_CROSS_EDGE_TYPES = [
2734
2741
  "depends_on_product",
2735
2742
  "cannibalises",
2736
2743
  "succeeds",
2737
- "hosts"
2744
+ "hosts",
2745
+ "contributes_to"
2738
2746
  ];
2739
2747
  var TYPES = getTypes();
2740
2748
  var TYPES_SET = new Set(TYPES);
@@ -2828,7 +2836,7 @@ var UPG_VALID_CHILDREN = {
2828
2836
  "feature_area",
2829
2837
  "release",
2830
2838
  "roadmap",
2831
- "theme",
2839
+ "roadmap_theme",
2832
2840
  // Engineering
2833
2841
  "bounded_context",
2834
2842
  "code_repository",
@@ -3224,8 +3232,8 @@ var UPG_VALID_CHILDREN = {
3224
3232
  a11y_standard: ["a11y_guideline", "a11y_audit", "a11y_annotation"],
3225
3233
  a11y_audit: ["a11y_issue"],
3226
3234
  // ── Product Specification expansion ─────────────────────────────────────────
3227
- roadmap: ["roadmap_item", "theme", "release"],
3228
- theme: ["feature"],
3235
+ roadmap: ["roadmap_item", "roadmap_theme", "release"],
3236
+ roadmap_theme: ["feature"],
3229
3237
  // ── Unified Context Layer hierarchy ─────────────────────────────────────────
3230
3238
  design_system: [
3231
3239
  "design_component",
@@ -5821,7 +5829,7 @@ var UPG_LIFECYCLE_FREE_TYPES = /* @__PURE__ */ new Set([
5821
5829
  // v0.7.0/UPG-571. ─
5822
5830
  "acceptance_criterion",
5823
5831
  "changelog",
5824
- "theme",
5832
+ "roadmap_theme",
5825
5833
  "user_story",
5826
5834
  // ── Strategy: metric is a measurement definition; metric_quality_assessment
5827
5835
  // is a point-in-time snapshot; value_stream is a mapped flow;
@@ -6122,6 +6130,21 @@ var UPG_SCALES = {
6122
6130
  }
6123
6131
  };
6124
6132
  var UPG_MIGRATIONS = {
6133
+ "0.9.0": [
6134
+ // (since v0.9.0, UPG-660) theme → roadmap_theme. `theme` was the only bare,
6135
+ // unqualified theme among four domain-prefixed siblings (strategic_theme,
6136
+ // content_theme, feedback_theme); it claimed the unqualified word and read as
6137
+ // interchangeable with strategic_theme (the N6/UPG-652 confusion). Renamed to
6138
+ // roadmap_theme so all four read consistently and the customer-problem roadmap
6139
+ // grouping is explicit. Lifecycle-free, identical property surface (theme_scope
6140
+ // / priority); no property migration required. Paired edge renames live in
6141
+ // UPG_EDGE_MIGRATIONS['0.9.0'].
6142
+ {
6143
+ from: "theme",
6144
+ to: "roadmap_theme",
6145
+ 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.'
6146
+ }
6147
+ ],
6125
6148
  "0.7.0": [
6126
6149
  // (since v0.7.0, UPG-571) story_statement → user_story. The v0.2.7 split
6127
6150
  // correctly separated the templated promise from the engineering work
@@ -7104,6 +7127,17 @@ var UPG_SPLIT_MIGRATIONS = {
7104
7127
  ]
7105
7128
  };
7106
7129
  var UPG_EDGE_MIGRATIONS = {
7130
+ "0.9.0": [
7131
+ // (since v0.9.0, UPG-660) theme → roadmap_theme. The four canonical edges that
7132
+ // touch the roadmap theme are renamed to the roadmap_theme form. Endpoint guards
7133
+ // reference the POST-migration (roadmap_theme) types; edge migration runs after
7134
+ // node migration, so by the time these rules apply the node has already been
7135
+ // renamed theme → roadmap_theme (UPG_MIGRATIONS['0.9.0']).
7136
+ { 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." },
7137
+ { 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." },
7138
+ { 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." },
7139
+ { 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." }
7140
+ ],
7107
7141
  "0.7.0": [
7108
7142
  // (since v0.7.0, UPG-571) story_statement → user_story re-canon. The four
7109
7143
  // canonical edges that touch the statement are renamed to the user_story
@@ -9920,7 +9954,7 @@ var UPG_PROPERTY_SCHEMA = {
9920
9954
  reach: {
9921
9955
  type: "assessment",
9922
9956
  scale_id: "reach_5",
9923
- description: "How many users experience this problem",
9957
+ 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.",
9924
9958
  properties: {
9925
9959
  value: { type: "number", description: "The numeric value, used for computation." },
9926
9960
  label: { type: "string", description: "The qualitative label (what the assessor meant)." },
@@ -9932,7 +9966,7 @@ var UPG_PROPERTY_SCHEMA = {
9932
9966
  frequency: {
9933
9967
  type: "assessment",
9934
9968
  scale_id: "frequency_5",
9935
- description: "How often users experience this problem",
9969
+ description: "How often users experience this problem. @deprecated 0.9.0:framework-scoped (opportunity-sizing). Removed in 0.9.1.",
9936
9970
  properties: {
9937
9971
  value: { type: "number", description: "The numeric value, used for computation." },
9938
9972
  label: { type: "string", description: "The qualitative label (what the assessor meant)." },
@@ -9944,7 +9978,7 @@ var UPG_PROPERTY_SCHEMA = {
9944
9978
  pain: {
9945
9979
  type: "assessment",
9946
9980
  scale_id: "pain_5",
9947
- description: "How painful it is when unaddressed",
9981
+ description: "How painful it is when unaddressed. @deprecated 0.9.0:framework-scoped (opportunity-sizing). Removed in 0.9.1.",
9948
9982
  properties: {
9949
9983
  value: { type: "number", description: "The numeric value, used for computation." },
9950
9984
  label: { type: "string", description: "The qualitative label (what the assessor meant)." },
@@ -9953,7 +9987,7 @@ var UPG_PROPERTY_SCHEMA = {
9953
9987
  },
9954
9988
  required: ["value", "label"]
9955
9989
  },
9956
- opportunity_score: { type: "number", description: "Computed: normalized_reach x normalized_frequency x normalized_pain (0-1)" }
9990
+ 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." }
9957
9991
  },
9958
9992
  // OrganizationProperties: Organization entity.
9959
9993
  organization: {
@@ -10367,6 +10401,11 @@ var UPG_PROPERTY_SCHEMA = {
10367
10401
  start_date: { type: "string", description: "ISO date work begins. More precise than `quarter` for continuous planning." },
10368
10402
  target_date: { type: "string", description: "ISO date completion is expected. For shipped items, the actual completion date." }
10369
10403
  },
10404
+ // RoadmapThemeProperties: Thematic grouping of roadmap work, around the customer problem it solves.
10405
+ roadmap_theme: {
10406
+ theme_scope: { type: "string", description: "Scope description" },
10407
+ priority: { type: "string", enum: ["urgent", "high", "medium", "low", "none"], description: "Priority" }
10408
+ },
10370
10409
  // RoleProperties: Role entity.
10371
10410
  role: {
10372
10411
  responsibilities: { type: "string[]", description: "Key responsibilities of the role" },
@@ -10552,7 +10591,7 @@ var UPG_PROPERTY_SCHEMA = {
10552
10591
  reach: {
10553
10592
  type: "assessment",
10554
10593
  scale_id: "reach_5",
10555
- description: "How many users this solution reaches (1 = few, 5 = most)",
10594
+ 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.",
10556
10595
  properties: {
10557
10596
  value: { type: "number", description: "The numeric value, used for computation." },
10558
10597
  label: { type: "string", description: "The qualitative label (what the assessor meant)." },
@@ -10564,7 +10603,7 @@ var UPG_PROPERTY_SCHEMA = {
10564
10603
  impact: {
10565
10604
  type: "assessment",
10566
10605
  scale_id: "impact_5",
10567
- description: "Expected impact on the target outcome (1 = minimal, 5 = transformative)",
10606
+ description: "Expected impact on the target outcome (1 = minimal, 5 = transformative). @deprecated 0.9.0:framework-scoped (rice-scoring). Removed in 0.9.1.",
10568
10607
  properties: {
10569
10608
  value: { type: "number", description: "The numeric value, used for computation." },
10570
10609
  label: { type: "string", description: "The qualitative label (what the assessor meant)." },
@@ -10576,7 +10615,7 @@ var UPG_PROPERTY_SCHEMA = {
10576
10615
  confidence: {
10577
10616
  type: "assessment",
10578
10617
  scale_id: "confidence_5",
10579
- description: "How confident the team is in this solution (1 = speculative, 5 = proven)",
10618
+ 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.",
10580
10619
  properties: {
10581
10620
  value: { type: "number", description: "The numeric value, used for computation." },
10582
10621
  label: { type: "string", description: "The qualitative label (what the assessor meant)." },
@@ -10588,7 +10627,7 @@ var UPG_PROPERTY_SCHEMA = {
10588
10627
  effort: {
10589
10628
  type: "assessment",
10590
10629
  scale_id: "effort_5",
10591
- description: "Level of effort required to implement (1 = trivial, 5 = very large)",
10630
+ 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.",
10592
10631
  properties: {
10593
10632
  value: { type: "number", description: "The numeric value, used for computation." },
10594
10633
  label: { type: "string", description: "The qualitative label (what the assessor meant)." },
@@ -10597,7 +10636,7 @@ var UPG_PROPERTY_SCHEMA = {
10597
10636
  },
10598
10637
  required: ["value", "label"]
10599
10638
  },
10600
- rice_score: { type: "number", description: "Computed: (reach \xD7 impact \xD7 confidence) / effort" }
10639
+ 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." }
10601
10640
  },
10602
10641
  // StakeholderProperties: Stakeholder entity.
10603
10642
  stakeholder: {
@@ -10844,11 +10883,6 @@ var UPG_PROPERTY_SCHEMA = {
10844
10883
  skipped_count: { type: "number", description: "Number of tests that were skipped in the last run" },
10845
10884
  flaky_count: { type: "number", description: "Number of tests that passed only on retry (flaky) in the last run" }
10846
10885
  },
10847
- // ThemeProperties: Thematic grouping of work.
10848
- theme: {
10849
- theme_scope: { type: "string", description: "Scope description" },
10850
- priority: { type: "string", enum: ["urgent", "high", "medium", "low", "none"], description: "Priority" }
10851
- },
10852
10886
  // ThreatProperties: Threat.
10853
10887
  threat: {
10854
10888
  category: { type: "string", description: 'Attack or threat scenario. @example "injection", "misconfiguration", "social engineering", "supply chain"' },
@@ -11077,6 +11111,188 @@ function getPropertySchema(entityType) {
11077
11111
  return UPG_PROPERTY_SCHEMA[entityType];
11078
11112
  }
11079
11113
  var UPG_FRAMEWORKS = [
11114
+ {
11115
+ "id": "opportunity-sizing",
11116
+ "approach_ids": [
11117
+ "prioritise"
11118
+ ],
11119
+ "name": "Opportunity Sizing",
11120
+ "version": "1.0.0",
11121
+ "description": "Size opportunities by Reach, Frequency, and Pain to rank which problems are most worth solving before committing to solutions.",
11122
+ "category": "prioritization",
11123
+ "origin": {
11124
+ "type": "practitioner",
11125
+ "attribution": "Continuous discovery practice",
11126
+ "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.",
11127
+ "url": "https://www.producttalk.org/2016/08/opportunity-solution-tree/",
11128
+ "year": 2016,
11129
+ "license": "open_attribution"
11130
+ },
11131
+ "tags": [
11132
+ "prioritization",
11133
+ "discovery",
11134
+ "table"
11135
+ ],
11136
+ "slots": [
11137
+ {
11138
+ "label": "Opportunities to size",
11139
+ "entityTypeId": "opportunity",
11140
+ "description": "Opportunities scored on Reach, Frequency, and Pain."
11141
+ }
11142
+ ],
11143
+ "data": {
11144
+ "entity_types": [
11145
+ {
11146
+ "type": "opportunity",
11147
+ "role": "scored_item"
11148
+ }
11149
+ ],
11150
+ "required_properties": {
11151
+ "opportunity": [
11152
+ {
11153
+ "property": "reach",
11154
+ "type": "assessment",
11155
+ "scale_id": "reach_5",
11156
+ "required": true,
11157
+ "scope": "framework",
11158
+ "label": "Reach",
11159
+ "description": "How many users experience this problem?"
11160
+ },
11161
+ {
11162
+ "property": "frequency",
11163
+ "type": "assessment",
11164
+ "scale_id": "frequency_5",
11165
+ "required": true,
11166
+ "scope": "framework",
11167
+ "label": "Frequency",
11168
+ "description": "How often do they run into it?"
11169
+ },
11170
+ {
11171
+ "property": "pain",
11172
+ "type": "assessment",
11173
+ "scale_id": "pain_5",
11174
+ "required": true,
11175
+ "scope": "framework",
11176
+ "label": "Pain",
11177
+ "description": "How painful is it when left unaddressed?"
11178
+ }
11179
+ ]
11180
+ },
11181
+ "computed_properties": [
11182
+ {
11183
+ "property": "opportunity_score",
11184
+ "expression": "reach * frequency * pain",
11185
+ "entity_type": "opportunity",
11186
+ "label": "Opportunity Score",
11187
+ "format": "number"
11188
+ }
11189
+ ],
11190
+ "scoring_method": {
11191
+ "applies_to": [
11192
+ "opportunity"
11193
+ ],
11194
+ "inputs": [
11195
+ {
11196
+ "property": "reach",
11197
+ "type": "assessment",
11198
+ "scale_id": "reach_5",
11199
+ "required": true,
11200
+ "scope": "framework",
11201
+ "label": "Reach",
11202
+ "description": "How many users experience this problem?"
11203
+ },
11204
+ {
11205
+ "property": "frequency",
11206
+ "type": "assessment",
11207
+ "scale_id": "frequency_5",
11208
+ "required": true,
11209
+ "scope": "framework",
11210
+ "label": "Frequency",
11211
+ "description": "How often do they run into it?"
11212
+ },
11213
+ {
11214
+ "property": "pain",
11215
+ "type": "assessment",
11216
+ "scale_id": "pain_5",
11217
+ "required": true,
11218
+ "scope": "framework",
11219
+ "label": "Pain",
11220
+ "description": "How painful is it when left unaddressed?"
11221
+ }
11222
+ ],
11223
+ "computed": [
11224
+ {
11225
+ "property": "opportunity_score",
11226
+ "expression": "reach * frequency * pain",
11227
+ "label": "Opportunity Score",
11228
+ "format": "number"
11229
+ }
11230
+ ]
11231
+ }
11232
+ },
11233
+ "structure": {
11234
+ "pattern": "table"
11235
+ },
11236
+ "presentation": {
11237
+ "layout": {
11238
+ "type": "table",
11239
+ "columns": [
11240
+ {
11241
+ "property": "title",
11242
+ "label": "Opportunities to size",
11243
+ "sortable": true
11244
+ },
11245
+ {
11246
+ "property": "reach",
11247
+ "label": "Reach",
11248
+ "sortable": true,
11249
+ "format": "number"
11250
+ },
11251
+ {
11252
+ "property": "frequency",
11253
+ "label": "Frequency",
11254
+ "sortable": true,
11255
+ "format": "number"
11256
+ },
11257
+ {
11258
+ "property": "pain",
11259
+ "label": "Pain",
11260
+ "sortable": true,
11261
+ "format": "number"
11262
+ },
11263
+ {
11264
+ "property": "opportunity_score",
11265
+ "label": "Opportunity Score",
11266
+ "sortable": true,
11267
+ "format": "score_pill"
11268
+ }
11269
+ ]
11270
+ },
11271
+ "sort_by": {
11272
+ "property": "opportunity_score",
11273
+ "direction": "desc"
11274
+ },
11275
+ "colour_by": "score",
11276
+ "card_fields": [
11277
+ "title",
11278
+ "description",
11279
+ "status"
11280
+ ]
11281
+ },
11282
+ "education": {
11283
+ "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.",
11284
+ "core_question": "Of the problems we could pursue, which affect the most users, most often, with the most pain?",
11285
+ "when_to_use": [
11286
+ "You have more opportunities than you can pursue",
11287
+ "You need to compare problems before committing to solutions",
11288
+ "You want a defensible, transparent way to choose what to explore"
11289
+ ],
11290
+ "when_not_to_use": [
11291
+ "A single opportunity is already validated and obvious",
11292
+ "You have no signal yet on reach, frequency, or pain"
11293
+ ]
11294
+ }
11295
+ },
11080
11296
  {
11081
11297
  "id": "opportunity-solution-tree",
11082
11298
  "approach_ids": [
@@ -12333,7 +12549,7 @@ var UPG_FRAMEWORKS = [
12333
12549
  ],
12334
12550
  "name": "RICE Scoring",
12335
12551
  "version": "1.0.0",
12336
- "description": "Score features, opportunities, and needs by Reach, Impact, Confidence, and Effort to produce a ranked priority list.",
12552
+ "description": "Score features, solutions, opportunities, and needs by Reach, Impact, Confidence, and Effort to produce a ranked priority list.",
12337
12553
  "category": "prioritization",
12338
12554
  "origin": {
12339
12555
  "type": "practitioner",
@@ -12358,6 +12574,11 @@ var UPG_FRAMEWORKS = [
12358
12574
  "entityTypeId": "opportunity",
12359
12575
  "description": "Opportunities scored on the same RICE Scoring inputs as features."
12360
12576
  },
12577
+ {
12578
+ "label": "Solutions to score",
12579
+ "entityTypeId": "solution",
12580
+ "description": "Solutions scored on the same RICE Scoring inputs as features."
12581
+ },
12361
12582
  {
12362
12583
  "label": "Needs to score",
12363
12584
  "entityTypeId": "need",
@@ -12374,6 +12595,10 @@ var UPG_FRAMEWORKS = [
12374
12595
  "type": "opportunity",
12375
12596
  "role": "scored_item"
12376
12597
  },
12598
+ {
12599
+ "type": "solution",
12600
+ "role": "scored_item"
12601
+ },
12377
12602
  {
12378
12603
  "type": "need",
12379
12604
  "role": "scored_item"
@@ -12456,6 +12681,44 @@ var UPG_FRAMEWORKS = [
12456
12681
  "description": "How much work is required to build and ship this, on the effort scale?"
12457
12682
  }
12458
12683
  ],
12684
+ "solution": [
12685
+ {
12686
+ "property": "reach",
12687
+ "type": "assessment",
12688
+ "scale_id": "reach_5",
12689
+ "required": true,
12690
+ "scope": "framework",
12691
+ "label": "Reach",
12692
+ "description": "How many users will this impact per quarter?"
12693
+ },
12694
+ {
12695
+ "property": "impact",
12696
+ "type": "assessment",
12697
+ "scale_id": "impact_5",
12698
+ "required": true,
12699
+ "scope": "framework",
12700
+ "label": "Impact",
12701
+ "description": "How much will this impact each user, on the impact scale?"
12702
+ },
12703
+ {
12704
+ "property": "confidence",
12705
+ "type": "assessment",
12706
+ "scale_id": "confidence_5",
12707
+ "required": true,
12708
+ "scope": "framework",
12709
+ "label": "Confidence",
12710
+ "description": "How confident are you in the reach, impact, and effort estimates?"
12711
+ },
12712
+ {
12713
+ "property": "effort",
12714
+ "type": "assessment",
12715
+ "scale_id": "effort_5",
12716
+ "required": true,
12717
+ "scope": "framework",
12718
+ "label": "Effort",
12719
+ "description": "How much work is required to build and ship this, on the effort scale?"
12720
+ }
12721
+ ],
12459
12722
  "need": [
12460
12723
  {
12461
12724
  "property": "reach",
@@ -12510,6 +12773,13 @@ var UPG_FRAMEWORKS = [
12510
12773
  "label": "RICE Score",
12511
12774
  "format": "number"
12512
12775
  },
12776
+ {
12777
+ "property": "rice_score",
12778
+ "expression": "(reach * impact * confidence) / effort",
12779
+ "entity_type": "solution",
12780
+ "label": "RICE Score",
12781
+ "format": "number"
12782
+ },
12513
12783
  {
12514
12784
  "property": "rice_score",
12515
12785
  "expression": "(reach * impact * confidence) / effort",
@@ -12522,6 +12792,7 @@ var UPG_FRAMEWORKS = [
12522
12792
  "applies_to": [
12523
12793
  "feature",
12524
12794
  "opportunity",
12795
+ "solution",
12525
12796
  "need"
12526
12797
  ],
12527
12798
  "inputs": [
@@ -16768,7 +17039,8 @@ var STANDARD_LABELS = {
16768
17039
  product: { alt_labels: ["offering", "app", "service", "platform"] },
16769
17040
  vision: { alt_labels: ["product vision", "north star vision", "long-term vision"] },
16770
17041
  mission: { alt_labels: ["mission statement", "purpose"] },
16771
- strategic_theme: { alt_labels: ["theme", "strategic pillar", "focus area"] },
17042
+ strategic_theme: { alt_labels: ["focus area", "strategic focus area"] },
17043
+ // N6: not 'theme'/'strategic pillar' (own types)
16772
17044
  initiative: { alt_labels: ["strategic initiative", "program initiative", "workstream"] },
16773
17045
  capability: { alt_labels: ["business capability", "organizational capability"] },
16774
17046
  value_stream: { alt_labels: ["value chain", "stream"] },
@@ -16830,7 +17102,8 @@ var STANDARD_LABELS = {
16830
17102
  fix: { alt_labels: ["bugfix", "patch", "remediation"] },
16831
17103
  roadmap: { alt_labels: ["product roadmap", "release plan", "timeline"] },
16832
17104
  roadmap_item: { alt_labels: ["roadmap entry", "planned item"] },
16833
- theme: { alt_labels: ["product theme", "bucket", "category"] },
17105
+ roadmap_theme: { alt_labels: ["product theme", "roadmap theme"] },
17106
+ // UPG-660: renamed from bare 'theme' (N6 lineage)
16834
17107
  // Engineering layer
16835
17108
  bounded_context: { alt_labels: ["context", "domain boundary", "module boundary"] },
16836
17109
  service: { alt_labels: ["microservice", "backend service", "api service"] },
@@ -17029,7 +17302,8 @@ var STANDARD_LABELS = {
17029
17302
  feedback_vote: { alt_labels: ["upvote", "vote", "user vote"] },
17030
17303
  user_advisory_board: { alt_labels: ["cab", "customer advisory board", "advisory council"] },
17031
17304
  beta_program: { alt_labels: ["beta", "early access", "preview program"] },
17032
- feedback_theme: { alt_labels: ["feedback cluster", "theme", "feedback category"] },
17305
+ feedback_theme: { alt_labels: ["feedback cluster", "feedback category"] },
17306
+ // N6: not bare 'theme'
17033
17307
  // Pricing & Packaging layer
17034
17308
  pricing_strategy: { alt_labels: ["pricing model", "monetization strategy"] },
17035
17309
  package: { alt_labels: ["product package", "bundle", "sku"] },
@@ -17533,9 +17807,9 @@ var PRODUCT_DELIVERY_PLAYBOOK = {
17533
17807
  ),
17534
17808
  seqStep(
17535
17809
  6,
17536
- "Themes & Changelog",
17537
- ["theme", "changelog"],
17538
- "Group releases into strategic themes. Maintain a changelog the team and customers can read together."
17810
+ "Roadmap Themes & Changelog",
17811
+ ["roadmap_theme", "changelog"],
17812
+ "Group roadmap work into roadmap themes around the customer problem. Maintain a changelog the team and customers can read together."
17539
17813
  )
17540
17814
  ]
17541
17815
  };
@@ -18789,7 +19063,7 @@ var PRODUCT_SPEC_GUIDE = {
18789
19063
  // registered to product_spec but missing from the navigation order).
18790
19064
  // `changelog` lives here because it is a structural product-shipping
18791
19065
  // artefact; content domain references it only via cross-domain bridges.
18792
- creation_sequence: ["feature_area", "feature", "epic", "user_story", "acceptance_criterion", "task", "bug", "release", "roadmap", "roadmap_item", "theme", "changelog"],
19066
+ creation_sequence: ["feature_area", "feature", "epic", "user_story", "acceptance_criterion", "task", "bug", "release", "roadmap", "roadmap_item", "roadmap_theme", "changelog"],
18793
19067
  patterns: [
18794
19068
  {
18795
19069
  name: "Feature Decomposition",
@@ -23214,7 +23488,7 @@ var UPG_REGIONS = [
23214
23488
  role: "leaf"
23215
23489
  },
23216
23490
  {
23217
- type: "theme",
23491
+ type: "roadmap_theme",
23218
23492
  role: "container",
23219
23493
  notes: "semantic spanner, not containment"
23220
23494
  },
@@ -23250,10 +23524,10 @@ var UPG_REGIONS = [
23250
23524
  "user_story_verified_by_acceptance_criterion",
23251
23525
  "task_implements_user_story",
23252
23526
  "roadmap_contains_roadmap_item",
23253
- "roadmap_categorised_by_theme",
23527
+ "roadmap_categorised_by_roadmap_theme",
23254
23528
  "roadmap_schedules_release",
23255
23529
  "release_documented_in_changelog",
23256
- "theme_spans_feature_area",
23530
+ "roadmap_theme_spans_feature_area",
23257
23531
  "milestone_gates_release",
23258
23532
  "roadmap_item_references_feature",
23259
23533
  "feature_request_voted_on_by_feedback_vote"
@@ -24373,7 +24647,7 @@ function serializePortfolioWithHeader(doc, opts) {
24373
24647
  header.integrity = { algorithm: INTEGRITY_ALGORITHM, body: computeBodyChecksum(doc) };
24374
24648
  return JSON.stringify({ $upg: header, ...body }, null, 2) + "\n";
24375
24649
  }
24376
- var UPG_VERSION = "0.8.16";
24650
+ var UPG_VERSION = "0.9.1";
24377
24651
  var MARKDOWN_FORMAT_VERSION = "0.1";
24378
24652
  var UPG_TYPES = getTypes();
24379
24653
  var UPG_TYPES_SET = new Set(UPG_TYPES);
@@ -26576,6 +26850,38 @@ import {
26576
26850
  WorkspaceAlreadyExistsError,
26577
26851
  WorkspaceNotInitialisedError
26578
26852
  } from "@unified-product-graph/sdk";
26853
+ function isExistingFile(p) {
26854
+ try {
26855
+ return fs.statSync(p).isFile();
26856
+ } catch {
26857
+ return false;
26858
+ }
26859
+ }
26860
+ function findWorkspaceUpgFiles(cwd) {
26861
+ const candidates = [];
26862
+ let topEntries;
26863
+ try {
26864
+ topEntries = fs.readdirSync(cwd, { withFileTypes: true });
26865
+ } catch {
26866
+ return candidates;
26867
+ }
26868
+ for (const entry of topEntries) {
26869
+ if (entry.isFile() && entry.name.endsWith(".upg")) {
26870
+ candidates.push(path3.join(cwd, entry.name));
26871
+ } else if (entry.isDirectory() && (entry.name === ".upg" || !entry.name.startsWith("."))) {
26872
+ try {
26873
+ const subEntries = fs.readdirSync(path3.join(cwd, entry.name), { withFileTypes: true });
26874
+ for (const sub of subEntries) {
26875
+ if (sub.isFile() && sub.name.endsWith(".upg")) {
26876
+ candidates.push(path3.join(cwd, entry.name, sub.name));
26877
+ }
26878
+ }
26879
+ } catch {
26880
+ }
26881
+ }
26882
+ }
26883
+ return candidates;
26884
+ }
26579
26885
  var listLocalProducts = (_args, _ctx) => {
26580
26886
  const cwd = process.cwd();
26581
26887
  const products = [];
@@ -26598,26 +26904,7 @@ var listLocalProducts = (_args, _ctx) => {
26598
26904
  }
26599
26905
  } catch {
26600
26906
  }
26601
- const candidates = [];
26602
- const topEntries = fs.readdirSync(cwd, { withFileTypes: true });
26603
- for (const entry of topEntries) {
26604
- if (entry.isFile() && entry.name.endsWith(".upg")) {
26605
- candidates.push(path3.join(cwd, entry.name));
26606
- } else if (entry.isDirectory() && (entry.name === ".upg" || !entry.name.startsWith("."))) {
26607
- try {
26608
- const subEntries = fs.readdirSync(
26609
- path3.join(cwd, entry.name),
26610
- { withFileTypes: true }
26611
- );
26612
- for (const sub of subEntries) {
26613
- if (sub.isFile() && sub.name.endsWith(".upg")) {
26614
- candidates.push(path3.join(cwd, entry.name, sub.name));
26615
- }
26616
- }
26617
- } catch {
26618
- }
26619
- }
26620
- }
26907
+ const candidates = findWorkspaceUpgFiles(cwd);
26621
26908
  for (const filePath of candidates) {
26622
26909
  try {
26623
26910
  const raw = fs.readFileSync(filePath, "utf-8");
@@ -26647,21 +26934,19 @@ var switchProduct = async (args, ctx) => {
26647
26934
  if (typeof fileArg !== "string" || fileArg.length === 0) {
26648
26935
  return textError("Missing required parameter: file (alias: product). Pass a .upg path or a bare product name.");
26649
26936
  }
26650
- let resolved = path3.resolve(fileArg);
26651
- if (!fs.existsSync(resolved)) {
26652
- const cwd = process.cwd();
26653
- const workspaceCandidates = [
26654
- path3.join(cwd, ".upg", fileArg),
26655
- path3.join(cwd, ".upg", fileArg + ".upg")
26656
- ];
26657
- const found = workspaceCandidates.find((c) => fs.existsSync(c));
26658
- if (found) {
26659
- resolved = found;
26660
- } else {
26661
- return textError(
26662
- `File not found: ${resolved} (also checked .upg/${fileArg} and .upg/${fileArg}.upg)`
26663
- );
26664
- }
26937
+ const cwd = process.cwd();
26938
+ const direct = path3.resolve(fileArg);
26939
+ const candidates = [
26940
+ path3.join(cwd, ".upg", fileArg),
26941
+ path3.join(cwd, ".upg", `${fileArg}.upg`),
26942
+ direct,
26943
+ `${direct}.upg`
26944
+ ];
26945
+ const resolved = candidates.find(isExistingFile);
26946
+ if (!resolved) {
26947
+ return textError(
26948
+ `File not found: ${direct} (also checked .upg/${fileArg} and .upg/${fileArg}.upg). Pass a .upg path or a bare product name from list_local_products.`
26949
+ );
26665
26950
  }
26666
26951
  try {
26667
26952
  await store.flush();
@@ -27146,6 +27431,312 @@ var batchCreateCrossProductEdges = async (args, _ctx) => {
27146
27431
  );
27147
27432
  };
27148
27433
 
27434
+ // src/tools/portfolio-read.ts
27435
+ import * as path4 from "path";
27436
+ import * as fs2 from "fs";
27437
+ import { UPGFileStore, computeGraphDigest as computeGraphDigest2 } from "@unified-product-graph/sdk";
27438
+
27439
+ // src/lib/graph-traverse.ts
27440
+ function traverseGraph(reader, params) {
27441
+ const fromType = params.from;
27442
+ const fromId = params.from_id;
27443
+ if (!fromType && !fromId) {
27444
+ return { ok: false, error: 'Provide either "from" (entity type) or "from_id" (node ID)' };
27445
+ }
27446
+ const traverseEdgeTypes = params.traverse;
27447
+ const maxDepth = Math.min(Math.max(params.depth ?? 3, 1), 10);
27448
+ const maxNodes = Math.min(Math.max(params.limit ?? 200, 1), 1e3);
27449
+ const includeFields = new Set(params.include ?? ["title", "status", "type"]);
27450
+ includeFields.add("id");
27451
+ includeFields.add("type");
27452
+ let startNodes;
27453
+ if (fromId) {
27454
+ const node = reader.getNode(fromId);
27455
+ if (!node) return { ok: false, error: `Node not found: ${fromId}` };
27456
+ startNodes = [node];
27457
+ } else {
27458
+ startNodes = reader.getAllNodes().filter((n) => n.type === fromType);
27459
+ }
27460
+ if (startNodes.length === 0) {
27461
+ return { ok: true, result: { nodes: [], edges: [], total_nodes: 0, total_edges: 0, truncated: false } };
27462
+ }
27463
+ const visited = /* @__PURE__ */ new Set();
27464
+ const collectedNodes = [];
27465
+ const collectedEdges = /* @__PURE__ */ new Map();
27466
+ const queue = [];
27467
+ let truncated = false;
27468
+ let maxDepthReached = 0;
27469
+ for (const n of startNodes) {
27470
+ if (collectedNodes.length >= maxNodes) {
27471
+ truncated = true;
27472
+ break;
27473
+ }
27474
+ visited.add(n.id);
27475
+ collectedNodes.push(n);
27476
+ queue.push({ id: n.id, level: 0 });
27477
+ }
27478
+ while (queue.length > 0) {
27479
+ if (collectedNodes.length >= maxNodes) {
27480
+ truncated = true;
27481
+ break;
27482
+ }
27483
+ const { id, level } = queue.shift();
27484
+ if (level > maxDepthReached) maxDepthReached = level;
27485
+ if (level >= maxDepth) continue;
27486
+ const edges = reader.getEdgesForNode(id);
27487
+ for (const edge of edges) {
27488
+ if (edge.source !== id) continue;
27489
+ if (traverseEdgeTypes && traverseEdgeTypes.length > 0) {
27490
+ const edgeTypeForLevel = level < traverseEdgeTypes.length ? traverseEdgeTypes[level] : traverseEdgeTypes[traverseEdgeTypes.length - 1];
27491
+ if (edgeTypeForLevel.startsWith("!")) {
27492
+ if (edge.type === edgeTypeForLevel.slice(1)) continue;
27493
+ } else {
27494
+ if (edge.type !== edgeTypeForLevel) continue;
27495
+ }
27496
+ }
27497
+ collectedEdges.set(edge.id, edge);
27498
+ const neighborId = edge.target;
27499
+ if (!visited.has(neighborId)) {
27500
+ visited.add(neighborId);
27501
+ const neighbor = reader.getNode(neighborId);
27502
+ if (neighbor) {
27503
+ if (collectedNodes.length >= maxNodes) {
27504
+ truncated = true;
27505
+ break;
27506
+ }
27507
+ collectedNodes.push(neighbor);
27508
+ queue.push({ id: neighborId, level: level + 1 });
27509
+ }
27510
+ }
27511
+ }
27512
+ }
27513
+ const propInclude = params.property_include;
27514
+ const propFilter = propInclude && propInclude.length > 0 ? new Set(propInclude) : null;
27515
+ const projectedNodes = collectedNodes.map((n) => {
27516
+ const projected = { id: n.id, type: n.type };
27517
+ if (includeFields.has("title")) projected.title = n.title;
27518
+ if (includeFields.has("status")) projected.status = n.status;
27519
+ if (includeFields.has("tags")) projected.tags = n.tags;
27520
+ if (includeFields.has("description")) projected.description = n.description;
27521
+ if (includeFields.has("properties")) {
27522
+ if (propFilter && n.properties) {
27523
+ const filtered = {};
27524
+ for (const key of propFilter) {
27525
+ if (key in n.properties) filtered[key] = n.properties[key];
27526
+ }
27527
+ projected.properties = filtered;
27528
+ } else {
27529
+ projected.properties = n.properties;
27530
+ }
27531
+ }
27532
+ return projected;
27533
+ });
27534
+ const edgeInclude = params.edge_include;
27535
+ let edgeArray;
27536
+ if (edgeInclude !== void 0 && edgeInclude.length === 0) {
27537
+ edgeArray = [];
27538
+ } else {
27539
+ const edgeFields = edgeInclude ? new Set(edgeInclude) : null;
27540
+ edgeArray = [...collectedEdges.values()].map((e) => {
27541
+ if (!edgeFields) return { id: e.id, type: e.type, source: e.source, target: e.target };
27542
+ const projected = {};
27543
+ if (edgeFields.has("id")) projected.id = e.id;
27544
+ if (edgeFields.has("type")) projected.type = e.type;
27545
+ if (edgeFields.has("source")) projected.source = e.source;
27546
+ if (edgeFields.has("target")) projected.target = e.target;
27547
+ return projected;
27548
+ });
27549
+ }
27550
+ const result = {
27551
+ nodes: projectedNodes,
27552
+ edges: edgeArray,
27553
+ total_nodes: projectedNodes.length,
27554
+ total_edges: edgeArray.length,
27555
+ truncated
27556
+ };
27557
+ if (truncated) result.truncated_at_depth = maxDepthReached;
27558
+ return { ok: true, result };
27559
+ }
27560
+
27561
+ // src/tools/portfolio-read.ts
27562
+ function resolveScopedProducts(cwd, scope) {
27563
+ const all = [];
27564
+ for (const absPath of findWorkspaceUpgFiles(cwd)) {
27565
+ try {
27566
+ const doc = JSON.parse(fs2.readFileSync(absPath, "utf-8"));
27567
+ if (!doc.product) continue;
27568
+ all.push({
27569
+ id: doc.product.id ?? null,
27570
+ title: doc.product.title ?? "(untitled)",
27571
+ file: path4.relative(cwd, absPath),
27572
+ absPath
27573
+ });
27574
+ } catch {
27575
+ }
27576
+ }
27577
+ if (!scope || scope.length === 0) {
27578
+ return { products: all, unmatched: [] };
27579
+ }
27580
+ const matches = (p, want) => p.id === want || p.file === want || path4.basename(p.file) === want || path4.basename(p.file, ".upg") === want;
27581
+ const products = all.filter((p) => scope.some((want) => matches(p, want)));
27582
+ const unmatched = scope.filter((want) => !all.some((p) => matches(p, want)));
27583
+ return { products, unmatched };
27584
+ }
27585
+ async function readerFor(product, activeStore) {
27586
+ const activePath = activeStore.getFilePath();
27587
+ if (activePath && path4.resolve(activePath) === path4.resolve(product.absPath)) {
27588
+ return { reader: activeStore, store: activeStore, active: true };
27589
+ }
27590
+ const store = new UPGFileStore();
27591
+ await store.loadReadOnly(product.absPath);
27592
+ return { reader: store, store, active: false };
27593
+ }
27594
+ var portfolioQuery = async (args, ctx) => {
27595
+ const { store } = ctx;
27596
+ const from = args.from;
27597
+ const fromId = args.from_id;
27598
+ if (!from && !fromId) {
27599
+ return textError('Provide either "from" (entity type) or "from_id" (node ID)');
27600
+ }
27601
+ const scope = args.scope;
27602
+ const cwd = process.cwd();
27603
+ const { products, unmatched } = resolveScopedProducts(cwd, scope);
27604
+ if (products.length === 0) {
27605
+ return text(
27606
+ JSON.stringify(
27607
+ {
27608
+ products: [],
27609
+ products_searched: 0,
27610
+ products_with_matches: 0,
27611
+ empty_products: [],
27612
+ ...unmatched.length > 0 ? { unmatched_scope: unmatched } : {},
27613
+ note: scope && scope.length > 0 ? "No workspace products matched the requested scope." : "No products found in the workspace. Run from a directory with a .upg/ workspace."
27614
+ },
27615
+ null,
27616
+ 2
27617
+ )
27618
+ );
27619
+ }
27620
+ const perProductLimit = Math.min(Math.max(args.limit ?? 100, 1), 1e3);
27621
+ const params = {
27622
+ from,
27623
+ from_id: fromId,
27624
+ traverse: args.traverse,
27625
+ depth: args.depth,
27626
+ limit: perProductLimit,
27627
+ include: args.include,
27628
+ edge_include: args.edge_include,
27629
+ property_include: args.property_include
27630
+ };
27631
+ const matched = [];
27632
+ const emptyProducts = [];
27633
+ const errored = [];
27634
+ let totalNodes = 0;
27635
+ let totalEdges = 0;
27636
+ for (const product of products) {
27637
+ let reader;
27638
+ try {
27639
+ ;
27640
+ ({ reader } = await readerFor(product, store));
27641
+ } catch (err) {
27642
+ errored.push({ product_id: product.id, file: product.file, error: err.message });
27643
+ continue;
27644
+ }
27645
+ const outcome = traverseGraph(reader, params);
27646
+ if (!outcome.ok) {
27647
+ emptyProducts.push(product.id ?? product.file);
27648
+ continue;
27649
+ }
27650
+ const r = outcome.result;
27651
+ if (r.total_nodes === 0) {
27652
+ emptyProducts.push(product.id ?? product.file);
27653
+ continue;
27654
+ }
27655
+ totalNodes += r.total_nodes;
27656
+ totalEdges += r.total_edges;
27657
+ matched.push({
27658
+ product_id: product.id,
27659
+ file: product.file,
27660
+ title: product.title,
27661
+ total_nodes: r.total_nodes,
27662
+ total_edges: r.total_edges,
27663
+ nodes: r.nodes,
27664
+ edges: r.edges,
27665
+ ...r.truncated ? { truncated: true, truncated_at_depth: r.truncated_at_depth } : {}
27666
+ });
27667
+ }
27668
+ const guard = preflightPayload({
27669
+ toolName: "portfolio_query",
27670
+ nodeCount: totalNodes,
27671
+ edgeCount: totalEdges,
27672
+ compactEdges: true,
27673
+ argsHint: `from=${from ?? fromId}, products=${matched.length}, limit=${perProductLimit}`
27674
+ });
27675
+ if (guard.kind === "refuse") return guard.result;
27676
+ const response = {
27677
+ products: matched,
27678
+ products_searched: products.length,
27679
+ products_with_matches: matched.length,
27680
+ total_nodes: totalNodes,
27681
+ total_edges: totalEdges,
27682
+ empty_products: emptyProducts
27683
+ };
27684
+ if (errored.length > 0) response.errored_products = errored;
27685
+ if (unmatched.length > 0) response.unmatched_scope = unmatched;
27686
+ if (guard.kind === "warn") Object.assign(response, guard.fields);
27687
+ return text(JSON.stringify(response, null, 2));
27688
+ };
27689
+ var portfolioDigest = async (args, ctx) => {
27690
+ const { store } = ctx;
27691
+ const scope = args.scope;
27692
+ const cwd = process.cwd();
27693
+ const { products, unmatched } = resolveScopedProducts(cwd, scope);
27694
+ const summaries = [];
27695
+ const errored = [];
27696
+ const byStage = {};
27697
+ let totalNodes = 0;
27698
+ let totalEdges = 0;
27699
+ for (const product of products) {
27700
+ try {
27701
+ const { store: reader } = await readerFor(product, store);
27702
+ const digest = computeGraphDigest2(reader);
27703
+ const stage = digest.product.stage || "unset";
27704
+ byStage[stage] = (byStage[stage] ?? 0) + 1;
27705
+ totalNodes += digest.counts.total_nodes;
27706
+ totalEdges += digest.counts.total_edges;
27707
+ const topTypes = Object.entries(digest.counts.by_type).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([type, count]) => ({ type, count }));
27708
+ summaries.push({
27709
+ product_id: product.id,
27710
+ file: product.file,
27711
+ title: digest.product.title,
27712
+ stage: digest.product.stage || null,
27713
+ total_nodes: digest.counts.total_nodes,
27714
+ total_edges: digest.counts.total_edges,
27715
+ health: digest.health,
27716
+ coverage_pct: digest.coverage.stage_summary?.overall_pct ?? null,
27717
+ top_types: topTypes
27718
+ });
27719
+ } catch (err) {
27720
+ errored.push({ product_id: product.id, file: product.file, error: err.message });
27721
+ }
27722
+ }
27723
+ const response = {
27724
+ products: summaries,
27725
+ rollup: {
27726
+ products: summaries.length,
27727
+ total_nodes: totalNodes,
27728
+ total_edges: totalEdges,
27729
+ by_stage: byStage
27730
+ }
27731
+ };
27732
+ if (errored.length > 0) response.errored_products = errored;
27733
+ if (unmatched.length > 0) response.unmatched_scope = unmatched;
27734
+ if (products.length === 0) {
27735
+ response.note = scope && scope.length > 0 ? "No workspace products matched the requested scope." : "No products found in the workspace. Run from a directory with a .upg/ workspace.";
27736
+ }
27737
+ return text(JSON.stringify(response, null, 2));
27738
+ };
27739
+
27149
27740
  // src/tools/schema.ts
27150
27741
  var getEntitySchema = (args, _ctx) => {
27151
27742
  const rawType = args.type;
@@ -27847,24 +28438,24 @@ var prioritise = (args, ctx) => {
27847
28438
  };
27848
28439
  var trace = (args, ctx) => {
27849
28440
  const anchor = args.anchor;
27850
- const path7 = args.path;
28441
+ const path8 = args.path;
27851
28442
  const edgesOverride = args.edges_override;
27852
28443
  if (!anchor) {
27853
28444
  return textError("Missing required parameter: anchor (entity_id)");
27854
28445
  }
27855
- if (!path7 || !Array.isArray(path7) || path7.length === 0) {
28446
+ if (!path8 || !Array.isArray(path8) || path8.length === 0) {
27856
28447
  return textError("Missing required parameter: path (UPGEntityType[])");
27857
28448
  }
27858
- if (edgesOverride && edgesOverride.length !== path7.length) {
28449
+ if (edgesOverride && edgesOverride.length !== path8.length) {
27859
28450
  return textError(
27860
- `edges_override length (${edgesOverride.length}) must match path length (${path7.length})`
28451
+ `edges_override length (${edgesOverride.length}) must match path length (${path8.length})`
27861
28452
  );
27862
28453
  }
27863
- const result = executeTrace(ctx.store, anchor, path7, edgesOverride);
28454
+ const result = executeTrace(ctx.store, anchor, path8, edgesOverride);
27864
28455
  const payload = {
27865
28456
  params: {
27866
28457
  anchor,
27867
- path: path7,
28458
+ path: path8,
27868
28459
  edges_override: edgesOverride ?? null
27869
28460
  },
27870
28461
  trail: result.trail,
@@ -28460,7 +29051,7 @@ var migrateStatus = (args, ctx) => {
28460
29051
 
28461
29052
  // src/tools/sync.ts
28462
29053
  import * as fsp4 from "fs/promises";
28463
- import * as path4 from "path";
29054
+ import * as path5 from "path";
28464
29055
  import { nodeId, edgeId as edgeId4 } from "@unified-product-graph/sdk";
28465
29056
  var getSyncState = async (_args, ctx) => {
28466
29057
  const { store, sync } = ctx;
@@ -28628,7 +29219,7 @@ var pushToCloud = async (args, ctx) => {
28628
29219
  const productId = args.product_id;
28629
29220
  if (!cloudEndpoint || !apiKey) {
28630
29221
  try {
28631
- const mcpConfigPath = path4.join(process.cwd(), ".mcp.json");
29222
+ const mcpConfigPath = path5.join(process.cwd(), ".mcp.json");
28632
29223
  const mcpRaw = await fsp4.readFile(mcpConfigPath, "utf-8");
28633
29224
  const mcpConfig = JSON.parse(mcpRaw);
28634
29225
  const upgCloud = mcpConfig.mcpServers?.["upg-cloud"];
@@ -28713,8 +29304,8 @@ var pushToCloud = async (args, ctx) => {
28713
29304
  };
28714
29305
 
28715
29306
  // src/tools/skills.ts
28716
- import { existsSync as existsSync2, lstatSync, readlinkSync, readFileSync as readFileSync2, readdirSync as readdirSync2, realpathSync } from "fs";
28717
- import { join as join5, resolve as resolve2, dirname as dirname3 } from "path";
29307
+ import { existsSync as existsSync2, lstatSync, readlinkSync, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync } from "fs";
29308
+ import { join as join5, resolve as resolve3, dirname as dirname3 } from "path";
28718
29309
  import { fileURLToPath } from "url";
28719
29310
  function repoRoot() {
28720
29311
  return process.cwd();
@@ -28743,7 +29334,7 @@ function resolveBundledSkillsDir() {
28743
29334
  } catch {
28744
29335
  md = process.cwd();
28745
29336
  }
28746
- for (const c of [resolve2(md, "..", "skills"), resolve2(md, "..", "..", "skills"), resolve2(md, "skills")]) {
29337
+ for (const c of [resolve3(md, "..", "skills"), resolve3(md, "..", "..", "skills"), resolve3(md, "skills")]) {
28747
29338
  if (isSkillsDir(c)) return c;
28748
29339
  }
28749
29340
  let dir = md;
@@ -28757,12 +29348,12 @@ function resolveBundledSkillsDir() {
28757
29348
  return null;
28758
29349
  }
28759
29350
  function sourceSkillsDir() {
28760
- const cwdPath = resolve2(repoRoot(), "packages/upg-mcp-server/skills");
29351
+ const cwdPath = resolve3(repoRoot(), "packages/upg-mcp-server/skills");
28761
29352
  if (existsSync2(cwdPath)) return cwdPath;
28762
29353
  return resolveBundledSkillsDir() ?? cwdPath;
28763
29354
  }
28764
29355
  function deployedSkillsDir() {
28765
- return resolve2(repoRoot(), ".claude/skills");
29356
+ return resolve3(repoRoot(), ".claude/skills");
28766
29357
  }
28767
29358
  function parseFrontmatter(body) {
28768
29359
  if (!body.startsWith("---\n")) return null;
@@ -28806,11 +29397,11 @@ function auditOne(name) {
28806
29397
  let deployedFrontmatter = null;
28807
29398
  let deployedFirstHeading = null;
28808
29399
  if (deployedExists) {
28809
- const deployedBody = readFileSync2(deployedPath, "utf8");
29400
+ const deployedBody = readFileSync3(deployedPath, "utf8");
28810
29401
  deployedFrontmatter = parseFrontmatter(deployedBody);
28811
29402
  deployedFirstHeading = firstHeading(deployedBody);
28812
29403
  if (sourceExists) {
28813
- const sourceBody = readFileSync2(sourcePath, "utf8");
29404
+ const sourceBody = readFileSync3(sourcePath, "utf8");
28814
29405
  inSync = deployedBody === sourceBody;
28815
29406
  if (!inSync) {
28816
29407
  issues.push("Deployed SKILL.md differs from canonical source; symlink is stale or broken");
@@ -29771,7 +30362,7 @@ var TOOL_DEFINITIONS = [
29771
30362
  },
29772
30363
  {
29773
30364
  name: "list_cross_edge_types",
29774
- 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`.",
30365
+ 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`, `contributes_to`. Portfolio-level relationships across products. Distinct from the within-product `UPG_EDGE_CATALOG`.",
29775
30366
  inputSchema: { type: "object", properties: {} }
29776
30367
  },
29777
30368
  {
@@ -30252,7 +30843,7 @@ var TOOL_DEFINITIONS = [
30252
30843
  },
30253
30844
  {
30254
30845
  name: "create_cross_product_edge",
30255
- 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).",
30846
+ 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), `contributes_to` (a product strategy entity rolls up to a higher-level one, e.g. product objective \u2192 company objective, product key_result \u2192 company key_result; directed subordinate to superior).",
30256
30847
  inputSchema: {
30257
30848
  type: "object",
30258
30849
  properties: {
@@ -30260,7 +30851,7 @@ var TOOL_DEFINITIONS = [
30260
30851
  target_id: { type: "string", description: "Target node ID" },
30261
30852
  type: {
30262
30853
  type: "string",
30263
- enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds", "hosts"],
30854
+ enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds", "hosts", "contributes_to"],
30264
30855
  description: "Cross-product relationship type"
30265
30856
  },
30266
30857
  source_product_id: { type: "string", description: "Product ID of the source node" },
@@ -30296,7 +30887,7 @@ var TOOL_DEFINITIONS = [
30296
30887
  target_id: { type: "string", description: "Target node ID (bare or qualified {product_id}/{node_id})" },
30297
30888
  type: {
30298
30889
  type: "string",
30299
- enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds", "hosts"],
30890
+ enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds", "hosts", "contributes_to"],
30300
30891
  description: "Cross-product relationship type"
30301
30892
  },
30302
30893
  source_product_id: { type: "string", description: "Product ID of the source node (qualifies a bare source_id)" },
@@ -30315,6 +30906,58 @@ var TOOL_DEFINITIONS = [
30315
30906
  description: "List all cross-product edges stored in the portfolio document (`.upg/portfolio.upg`). Empty list when the portfolio document is absent.",
30316
30907
  inputSchema: { type: "object", properties: {} }
30317
30908
  },
30909
+ {
30910
+ name: "portfolio_query",
30911
+ description: 'Traverse the graph ACROSS products in one call (the multi-product `query`). Runs the same BFS (typed-edge traversal + field projection) against every product in scope and tags each subgraph with its source `product_id`, without `switch_product` (the active product is read live; others are read-only). Use for portfolio-level questions ("every product\'s strategy region", "which products have a persona"). `from_id` only matches in its owning product. Read-only.',
30912
+ inputSchema: {
30913
+ type: "object",
30914
+ properties: {
30915
+ from: { type: "string", description: "Start from all nodes of this type (in each product)" },
30916
+ from_id: { type: "string", description: "Start from a specific node ID. Node IDs are product-local; only the owning product returns results." },
30917
+ traverse: {
30918
+ type: "array",
30919
+ items: { type: "string" },
30920
+ description: "Edge types to follow at each level (in order). If omitted, follows all edges. Prefix with ! to exclude."
30921
+ },
30922
+ depth: { type: "number", description: "Max traversal depth (default 3, max 10)" },
30923
+ include: {
30924
+ type: "array",
30925
+ items: { type: "string" },
30926
+ description: 'Fields per node: "title", "status", "tags", "description", "properties" (default: title, status, type)'
30927
+ },
30928
+ limit: { type: "number", description: "Max nodes per product (default 100, max 1000)" },
30929
+ edge_include: {
30930
+ type: "array",
30931
+ items: { type: "string" },
30932
+ description: 'Edge fields to return: "id", "type", "source", "target". Empty array = no edges. Default: all fields.'
30933
+ },
30934
+ property_include: {
30935
+ type: "array",
30936
+ items: { type: "string" },
30937
+ description: 'When "properties" is in include, only return these property keys.'
30938
+ },
30939
+ scope: {
30940
+ type: "array",
30941
+ items: { type: "string" },
30942
+ description: "Product IDs (or files) to query. Omit to query ALL products in the workspace. Match by product id, relative file, or basename."
30943
+ }
30944
+ }
30945
+ }
30946
+ },
30947
+ {
30948
+ name: "portfolio_digest",
30949
+ description: "Roll up every product's counts, health, and stage-coverage in one call (the multi-product `get_graph_digest`). The strategic-surface read that otherwise required `switch_product` + `get_graph_digest` per graph. Returns per-product summaries plus a portfolio rollup (totals, products-by-stage). Read-only; never mutates active-product state.",
30950
+ inputSchema: {
30951
+ type: "object",
30952
+ properties: {
30953
+ scope: {
30954
+ type: "array",
30955
+ items: { type: "string" },
30956
+ description: "Product IDs (or files) to summarise. Omit to summarise ALL products in the workspace."
30957
+ }
30958
+ }
30959
+ }
30960
+ },
30318
30961
  {
30319
30962
  name: "migrate_cross_edges",
30320
30963
  description: "Migrate inline cross-product edges from the current product's `edges[]` into the portfolio document (`.upg/portfolio.upg`) with qualified IDs. `dry_run: true` (default) previews; `dry_run: false` applies. Requires `source_product_id` to qualify source node IDs.",
@@ -30501,6 +31144,8 @@ var HANDLERS = {
30501
31144
  attach_product_to_portfolio: attachProductToPortfolioTool,
30502
31145
  detach_product_from_portfolio: detachProductFromPortfolioTool,
30503
31146
  list_portfolio_cross_edges: listPortfolioCrossEdges,
31147
+ portfolio_query: portfolioQuery,
31148
+ portfolio_digest: portfolioDigest,
30504
31149
  migrate_cross_edges: migrateCrossEdges,
30505
31150
  get_sync_state: getSyncState,
30506
31151
  apply_pull_changeset: applyPullChangeset,
@@ -30559,9 +31204,9 @@ var SERVER_INSTRUCTIONS = [
30559
31204
  ].join("\n");
30560
31205
  function resolvePackageVersion() {
30561
31206
  try {
30562
- const here = path5.dirname(fileURLToPath2(import.meta.url));
30563
- const pkgPath = path5.resolve(here, "..", "package.json");
30564
- const raw = fs2.readFileSync(pkgPath, "utf-8");
31207
+ const here = path6.dirname(fileURLToPath2(import.meta.url));
31208
+ const pkgPath = path6.resolve(here, "..", "package.json");
31209
+ const raw = fs3.readFileSync(pkgPath, "utf-8");
30565
31210
  const pkg = JSON.parse(raw);
30566
31211
  if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
30567
31212
  } catch {
@@ -30603,7 +31248,7 @@ function createServer(store) {
30603
31248
  const result = handler ? await handler(args, ctx) : textError(`Unknown tool: ${name}`);
30604
31249
  if (logFile) {
30605
31250
  const entry = JSON.stringify({ ts: t0, tool: name, params: args, result, durationMs: Date.now() - t0 });
30606
- fs2.appendFileSync(logFile, entry + "\n");
31251
+ fs3.appendFileSync(logFile, entry + "\n");
30607
31252
  }
30608
31253
  return result;
30609
31254
  });
@@ -30620,15 +31265,15 @@ import { nanoid } from "nanoid";
30620
31265
  import { fileURLToPath as fileURLToPath3 } from "url";
30621
31266
  import { realpathSync as realpathSync2 } from "fs";
30622
31267
  async function discoverUPGFile(explicitFile) {
30623
- if (explicitFile) return path6.resolve(explicitFile);
31268
+ if (explicitFile) return path7.resolve(explicitFile);
30624
31269
  const cwd = process.cwd();
30625
- const workspacePath = path6.join(cwd, ".upg", "workspace.json");
31270
+ const workspacePath = path7.join(cwd, ".upg", "workspace.json");
30626
31271
  try {
30627
- const raw = await fs3.readFile(workspacePath, "utf-8");
31272
+ const raw = await fs4.readFile(workspacePath, "utf-8");
30628
31273
  const workspace = JSON.parse(raw);
30629
31274
  if (workspace.default_product) {
30630
- const filePath = path6.join(cwd, ".upg", workspace.default_product);
30631
- await fs3.access(filePath);
31275
+ const filePath = path7.join(cwd, ".upg", workspace.default_product);
31276
+ await fs4.access(filePath);
30632
31277
  const title = workspace.products?.find(
30633
31278
  (p) => p.file === workspace.default_product
30634
31279
  )?.title ?? workspace.default_product;
@@ -30639,16 +31284,16 @@ async function discoverUPGFile(explicitFile) {
30639
31284
  return filePath;
30640
31285
  }
30641
31286
  } catch {
30642
- const upgDir = path6.join(cwd, ".upg");
31287
+ const upgDir = path7.join(cwd, ".upg");
30643
31288
  try {
30644
- const dirEntries = await fs3.readdir(upgDir);
31289
+ const dirEntries = await fs4.readdir(upgDir);
30645
31290
  const upgFiles = dirEntries.filter((f) => f.endsWith(".upg")).sort();
30646
31291
  if (upgFiles.length > 0) {
30647
31292
  const products = [];
30648
31293
  for (const file of upgFiles) {
30649
- let title = path6.basename(file, ".upg");
31294
+ let title = path7.basename(file, ".upg");
30650
31295
  try {
30651
- const raw = await fs3.readFile(path6.join(upgDir, file), "utf-8");
31296
+ const raw = await fs4.readFile(path7.join(upgDir, file), "utf-8");
30652
31297
  const doc = JSON.parse(raw);
30653
31298
  if (doc.product?.title) title = doc.product.title;
30654
31299
  } catch {
@@ -30660,12 +31305,12 @@ async function discoverUPGFile(explicitFile) {
30660
31305
  default_product: upgFiles[0],
30661
31306
  products
30662
31307
  };
30663
- await fs3.writeFile(workspacePath, JSON.stringify(workspace, null, 2) + "\n", "utf-8");
31308
+ await fs4.writeFile(workspacePath, JSON.stringify(workspace, null, 2) + "\n", "utf-8");
30664
31309
  process.stderr.write(
30665
31310
  `UPG workspace: auto-created workspace.json (${upgFiles.length} product${upgFiles.length > 1 ? "s" : ""})
30666
31311
  `
30667
31312
  );
30668
- const filePath = path6.join(upgDir, upgFiles[0]);
31313
+ const filePath = path7.join(upgDir, upgFiles[0]);
30669
31314
  process.stderr.write(`UPG workspace: loading "${products[0].title}"
30670
31315
  `);
30671
31316
  return filePath;
@@ -30674,17 +31319,17 @@ async function discoverUPGFile(explicitFile) {
30674
31319
  }
30675
31320
  }
30676
31321
  try {
30677
- const entries = await fs3.readdir(cwd);
31322
+ const entries = await fs4.readdir(cwd);
30678
31323
  const upgFiles = entries.filter((f) => f.endsWith(".upg")).sort();
30679
31324
  if (upgFiles.length === 1) {
30680
- return path6.resolve(upgFiles[0]);
31325
+ return path7.resolve(upgFiles[0]);
30681
31326
  }
30682
31327
  if (upgFiles.length > 1) {
30683
31328
  process.stderr.write(
30684
31329
  `Found ${upgFiles.length} .upg files: loading ${upgFiles[0]}. Use --file to pick a specific one.
30685
31330
  `
30686
31331
  );
30687
- return path6.resolve(upgFiles[0]);
31332
+ return path7.resolve(upgFiles[0]);
30688
31333
  }
30689
31334
  } catch {
30690
31335
  }
@@ -30704,7 +31349,7 @@ async function runMcpServer() {
30704
31349
  });
30705
31350
  let resolvedPath = await discoverUPGFile(values.file);
30706
31351
  if (!resolvedPath) {
30707
- const defaultFile = path6.resolve("product.upg");
31352
+ const defaultFile = path7.resolve("product.upg");
30708
31353
  const title = values.title ?? "My Product";
30709
31354
  const blank = {
30710
31355
  upg_version: UPG_VERSION,
@@ -30720,16 +31365,16 @@ async function runMcpServer() {
30720
31365
  nodes: [],
30721
31366
  edges: []
30722
31367
  };
30723
- await fs3.mkdir(path6.dirname(defaultFile), { recursive: true });
30724
- await fs3.writeFile(defaultFile, serializeCanonical(blank), "utf-8");
31368
+ await fs4.mkdir(path7.dirname(defaultFile), { recursive: true });
31369
+ await fs4.writeFile(defaultFile, serializeCanonical(blank), "utf-8");
30725
31370
  process.stderr.write(`Created new UPG file: ${defaultFile}
30726
31371
  `);
30727
31372
  resolvedPath = defaultFile;
30728
31373
  } else {
30729
31374
  try {
30730
- await fs3.access(resolvedPath);
31375
+ await fs4.access(resolvedPath);
30731
31376
  } catch {
30732
- const title = values.title ?? path6.basename(resolvedPath, ".upg");
31377
+ const title = values.title ?? path7.basename(resolvedPath, ".upg");
30733
31378
  const blank = {
30734
31379
  upg_version: UPG_VERSION,
30735
31380
  exported_at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -30744,13 +31389,13 @@ async function runMcpServer() {
30744
31389
  nodes: [],
30745
31390
  edges: []
30746
31391
  };
30747
- await fs3.mkdir(path6.dirname(resolvedPath), { recursive: true });
30748
- await fs3.writeFile(resolvedPath, serializeCanonical(blank), "utf-8");
31392
+ await fs4.mkdir(path7.dirname(resolvedPath), { recursive: true });
31393
+ await fs4.writeFile(resolvedPath, serializeCanonical(blank), "utf-8");
30749
31394
  process.stderr.write(`Created new UPG file: ${resolvedPath}
30750
31395
  `);
30751
31396
  }
30752
31397
  }
30753
- const store = new UPGFileStore();
31398
+ const store = new UPGFileStore2();
30754
31399
  store.setWriter("upg-mcp-local", SERVER_VERSION);
30755
31400
  await store.load(resolvedPath);
30756
31401
  const nodes = store.getAllNodes();