@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/TOOLS.md +62 -5
- package/dist/index.js +771 -126
- package/dist/index.js.map +1 -1
- package/dist/tools-manifest.json +133 -21
- package/package.json +3 -3
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
|
|
6
|
-
import * as
|
|
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
|
|
12
|
+
import fs3 from "fs";
|
|
13
13
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
14
|
-
import * as
|
|
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.
|
|
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
|
-
"
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1252
|
-
// feature_area is not contained by
|
|
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
|
-
|
|
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
|
-
"
|
|
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", "
|
|
3228
|
-
|
|
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
|
-
"
|
|
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: ["
|
|
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
|
-
|
|
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", "
|
|
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
|
-
["
|
|
17538
|
-
"Group
|
|
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", "
|
|
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: "
|
|
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
|
-
"
|
|
23527
|
+
"roadmap_categorised_by_roadmap_theme",
|
|
23254
23528
|
"roadmap_schedules_release",
|
|
23255
23529
|
"release_documented_in_changelog",
|
|
23256
|
-
"
|
|
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.
|
|
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
|
-
|
|
26651
|
-
|
|
26652
|
-
|
|
26653
|
-
|
|
26654
|
-
|
|
26655
|
-
|
|
26656
|
-
|
|
26657
|
-
|
|
26658
|
-
|
|
26659
|
-
|
|
26660
|
-
|
|
26661
|
-
|
|
26662
|
-
|
|
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
|
|
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 (!
|
|
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 !==
|
|
28449
|
+
if (edgesOverride && edgesOverride.length !== path8.length) {
|
|
27859
28450
|
return textError(
|
|
27860
|
-
`edges_override length (${edgesOverride.length}) must match path length (${
|
|
28451
|
+
`edges_override length (${edgesOverride.length}) must match path length (${path8.length})`
|
|
27861
28452
|
);
|
|
27862
28453
|
}
|
|
27863
|
-
const result = executeTrace(ctx.store, anchor,
|
|
28454
|
+
const result = executeTrace(ctx.store, anchor, path8, edgesOverride);
|
|
27864
28455
|
const payload = {
|
|
27865
28456
|
params: {
|
|
27866
28457
|
anchor,
|
|
27867
|
-
path:
|
|
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
|
|
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 =
|
|
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
|
|
28717
|
-
import { join as join5, resolve as
|
|
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 [
|
|
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 =
|
|
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
|
|
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 =
|
|
29400
|
+
const deployedBody = readFileSync3(deployedPath, "utf8");
|
|
28810
29401
|
deployedFrontmatter = parseFrontmatter(deployedBody);
|
|
28811
29402
|
deployedFirstHeading = firstHeading(deployedBody);
|
|
28812
29403
|
if (sourceExists) {
|
|
28813
|
-
const sourceBody =
|
|
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 =
|
|
30563
|
-
const pkgPath =
|
|
30564
|
-
const raw =
|
|
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
|
-
|
|
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
|
|
31268
|
+
if (explicitFile) return path7.resolve(explicitFile);
|
|
30624
31269
|
const cwd = process.cwd();
|
|
30625
|
-
const workspacePath =
|
|
31270
|
+
const workspacePath = path7.join(cwd, ".upg", "workspace.json");
|
|
30626
31271
|
try {
|
|
30627
|
-
const raw = await
|
|
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 =
|
|
30631
|
-
await
|
|
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 =
|
|
31287
|
+
const upgDir = path7.join(cwd, ".upg");
|
|
30643
31288
|
try {
|
|
30644
|
-
const dirEntries = await
|
|
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 =
|
|
31294
|
+
let title = path7.basename(file, ".upg");
|
|
30650
31295
|
try {
|
|
30651
|
-
const raw = await
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
30724
|
-
await
|
|
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
|
|
31375
|
+
await fs4.access(resolvedPath);
|
|
30731
31376
|
} catch {
|
|
30732
|
-
const title = values.title ??
|
|
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
|
|
30748
|
-
await
|
|
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
|
|
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();
|