@unified-product-graph/mcp-server 0.8.14 → 0.8.16
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/README.md +2 -2
- package/TOOLS.md +280 -8
- package/dist/index.js +455 -12
- package/dist/index.js.map +1 -1
- package/dist/tools-manifest.json +469 -24
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -2733,7 +2733,8 @@ var UPG_CROSS_EDGE_TYPES = [
|
|
|
2733
2733
|
"shares_metric",
|
|
2734
2734
|
"depends_on_product",
|
|
2735
2735
|
"cannibalises",
|
|
2736
|
-
"succeeds"
|
|
2736
|
+
"succeeds",
|
|
2737
|
+
"hosts"
|
|
2737
2738
|
];
|
|
2738
2739
|
var TYPES = getTypes();
|
|
2739
2740
|
var TYPES_SET = new Set(TYPES);
|
|
@@ -10131,7 +10132,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
10131
10132
|
// ProductAreaProperties: ProductArea entity.
|
|
10132
10133
|
product_area: {
|
|
10133
10134
|
strategic_priority: { type: "string", enum: ["urgent", "high", "medium", "low", "none"], description: "Strategic priority assigned to this area" },
|
|
10134
|
-
description: { type: "string", description: "Narrative description of what this area covers" }
|
|
10135
|
+
description: { type: "string", description: "Narrative description of what this area covers" },
|
|
10136
|
+
owner: { type: "string", description: "Person or team that owns this area" }
|
|
10135
10137
|
},
|
|
10136
10138
|
// ProgramProperties: Program.
|
|
10137
10139
|
program: {
|
|
@@ -24371,7 +24373,7 @@ function serializePortfolioWithHeader(doc, opts) {
|
|
|
24371
24373
|
header.integrity = { algorithm: INTEGRITY_ALGORITHM, body: computeBodyChecksum(doc) };
|
|
24372
24374
|
return JSON.stringify({ $upg: header, ...body }, null, 2) + "\n";
|
|
24373
24375
|
}
|
|
24374
|
-
var UPG_VERSION = "0.8.
|
|
24376
|
+
var UPG_VERSION = "0.8.16";
|
|
24375
24377
|
var MARKDOWN_FORMAT_VERSION = "0.1";
|
|
24376
24378
|
var UPG_TYPES = getTypes();
|
|
24377
24379
|
var UPG_TYPES_SET = new Set(UPG_TYPES);
|
|
@@ -26255,6 +26257,11 @@ import * as path2 from "path";
|
|
|
26255
26257
|
import {
|
|
26256
26258
|
writePortfolioScopedNode as writePortfolioScopedNode2,
|
|
26257
26259
|
openPortfolioStoreIfExists,
|
|
26260
|
+
assignProductToArea,
|
|
26261
|
+
updateProductArea,
|
|
26262
|
+
removeProductFromArea,
|
|
26263
|
+
deleteArea,
|
|
26264
|
+
moveProductToArea,
|
|
26258
26265
|
PortfolioRoutingError as PortfolioRoutingError2
|
|
26259
26266
|
} from "@unified-product-graph/sdk";
|
|
26260
26267
|
var listProductAreas = async (_args, _ctx) => {
|
|
@@ -26269,11 +26276,24 @@ var listProductAreas = async (_args, _ctx) => {
|
|
|
26269
26276
|
if (area.description) row.description = area.description;
|
|
26270
26277
|
if (area.parent_area_id !== void 0) row.parent_area_id = area.parent_area_id;
|
|
26271
26278
|
if (area.strategic_priority) row.strategic_priority = area.strategic_priority;
|
|
26279
|
+
if (area.owner) row.owner = area.owner;
|
|
26272
26280
|
if (area.products) row.products = area.products;
|
|
26273
26281
|
return row;
|
|
26274
26282
|
});
|
|
26275
26283
|
return text(JSON.stringify({ areas: result, total: result.length }, null, 2));
|
|
26276
26284
|
};
|
|
26285
|
+
var assignProductToAreaTool = async (args, _ctx) => {
|
|
26286
|
+
const productId = args.product_id;
|
|
26287
|
+
const areaId = args.area_id;
|
|
26288
|
+
if (!productId) return textError("Missing required parameter: product_id");
|
|
26289
|
+
if (!areaId) return textError("Missing required parameter: area_id");
|
|
26290
|
+
try {
|
|
26291
|
+
const result = await assignProductToArea(process.cwd(), { product_id: productId, area_id: areaId });
|
|
26292
|
+
return text(JSON.stringify(result, null, 2));
|
|
26293
|
+
} catch (err) {
|
|
26294
|
+
return textError(err.message);
|
|
26295
|
+
}
|
|
26296
|
+
};
|
|
26277
26297
|
var getAreaGraph = (args, ctx) => {
|
|
26278
26298
|
const { store } = ctx;
|
|
26279
26299
|
const areaId = args.area_id;
|
|
@@ -26428,6 +26448,7 @@ var createArea = async (args, _ctx) => {
|
|
|
26428
26448
|
const properties = {};
|
|
26429
26449
|
if (args.strategic_priority) properties.strategic_priority = args.strategic_priority;
|
|
26430
26450
|
if (args.parent_area_id) properties.parent_area_id = args.parent_area_id;
|
|
26451
|
+
if (args.owner) properties.owner = args.owner;
|
|
26431
26452
|
try {
|
|
26432
26453
|
const result = await writePortfolioScopedNode2(process.cwd(), {
|
|
26433
26454
|
type: "product_area",
|
|
@@ -26463,6 +26484,73 @@ var getChanges = (args, ctx) => {
|
|
|
26463
26484
|
)
|
|
26464
26485
|
);
|
|
26465
26486
|
};
|
|
26487
|
+
var updateAreaTool = async (args, _ctx) => {
|
|
26488
|
+
const areaId = args.area_id;
|
|
26489
|
+
if (!areaId) return textError("Missing required parameter: area_id");
|
|
26490
|
+
const hasField = args.title !== void 0 || args.description !== void 0 || args.strategic_priority !== void 0 || args.owner !== void 0 || "parent_area_id" in args;
|
|
26491
|
+
if (!hasField) {
|
|
26492
|
+
return textError(
|
|
26493
|
+
"Nothing to update: pass at least one of: title, description, strategic_priority, owner, parent_area_id."
|
|
26494
|
+
);
|
|
26495
|
+
}
|
|
26496
|
+
try {
|
|
26497
|
+
const result = await updateProductArea(process.cwd(), areaId, {
|
|
26498
|
+
title: args.title,
|
|
26499
|
+
description: args.description,
|
|
26500
|
+
strategic_priority: args.strategic_priority,
|
|
26501
|
+
owner: args.owner,
|
|
26502
|
+
// Tri-state: present (incl. null) re-parents/un-nests; absent leaves unchanged.
|
|
26503
|
+
..."parent_area_id" in args ? { parent_area_id: args.parent_area_id ?? null } : {}
|
|
26504
|
+
});
|
|
26505
|
+
return text(
|
|
26506
|
+
JSON.stringify({ message: `Updated area (${result.updated.join(", ")})`, ...result }, null, 2)
|
|
26507
|
+
);
|
|
26508
|
+
} catch (err) {
|
|
26509
|
+
if (err instanceof PortfolioRoutingError2) return textError(err.message);
|
|
26510
|
+
return textError(err.message);
|
|
26511
|
+
}
|
|
26512
|
+
};
|
|
26513
|
+
var removeProductFromAreaTool = async (args, _ctx) => {
|
|
26514
|
+
const productId = args.product_id;
|
|
26515
|
+
const areaId = args.area_id;
|
|
26516
|
+
if (!productId) return textError("Missing required parameter: product_id");
|
|
26517
|
+
if (!areaId) return textError("Missing required parameter: area_id");
|
|
26518
|
+
try {
|
|
26519
|
+
const result = await removeProductFromArea(process.cwd(), { product_id: productId, area_id: areaId });
|
|
26520
|
+
return text(JSON.stringify(result, null, 2));
|
|
26521
|
+
} catch (err) {
|
|
26522
|
+
if (err instanceof PortfolioRoutingError2) return textError(err.message);
|
|
26523
|
+
return textError(err.message);
|
|
26524
|
+
}
|
|
26525
|
+
};
|
|
26526
|
+
var deleteAreaTool = async (args, _ctx) => {
|
|
26527
|
+
const areaId = args.area_id;
|
|
26528
|
+
if (!areaId) return textError("Missing required parameter: area_id");
|
|
26529
|
+
try {
|
|
26530
|
+
const result = await deleteArea(process.cwd(), areaId, { force: args.force });
|
|
26531
|
+
return text(JSON.stringify({ message: `Deleted area ${areaId}`, ...result }, null, 2));
|
|
26532
|
+
} catch (err) {
|
|
26533
|
+
if (err instanceof PortfolioRoutingError2) return textError(err.message);
|
|
26534
|
+
return textError(err.message);
|
|
26535
|
+
}
|
|
26536
|
+
};
|
|
26537
|
+
var moveProductToAreaTool = async (args, _ctx) => {
|
|
26538
|
+
const productId = args.product_id;
|
|
26539
|
+
const toAreaId = args.to_area_id;
|
|
26540
|
+
if (!productId) return textError("Missing required parameter: product_id");
|
|
26541
|
+
if (!toAreaId) return textError("Missing required parameter: to_area_id");
|
|
26542
|
+
try {
|
|
26543
|
+
const result = await moveProductToArea(process.cwd(), {
|
|
26544
|
+
product_id: productId,
|
|
26545
|
+
to_area_id: toAreaId,
|
|
26546
|
+
from_area_id: args.from_area_id
|
|
26547
|
+
});
|
|
26548
|
+
return text(JSON.stringify(result, null, 2));
|
|
26549
|
+
} catch (err) {
|
|
26550
|
+
if (err instanceof PortfolioRoutingError2) return textError(err.message);
|
|
26551
|
+
return textError(err.message);
|
|
26552
|
+
}
|
|
26553
|
+
};
|
|
26466
26554
|
|
|
26467
26555
|
// src/tools/workspace.ts
|
|
26468
26556
|
import * as fs from "fs";
|
|
@@ -26474,10 +26562,14 @@ import {
|
|
|
26474
26562
|
resolvePortfolioPath,
|
|
26475
26563
|
openPortfolioStoreIfExists as openPortfolioStoreIfExists2,
|
|
26476
26564
|
registerProductOnPortfolio,
|
|
26477
|
-
findProductFileById
|
|
26565
|
+
findProductFileById,
|
|
26566
|
+
attachProductToPortfolio,
|
|
26567
|
+
detachProductFromPortfolio,
|
|
26568
|
+
deleteCrossProductEdge
|
|
26478
26569
|
} from "@unified-product-graph/sdk";
|
|
26479
26570
|
import {
|
|
26480
26571
|
createProduct,
|
|
26572
|
+
updateProduct,
|
|
26481
26573
|
initWorkspace,
|
|
26482
26574
|
InvalidProductNameError,
|
|
26483
26575
|
InvalidProductStageError,
|
|
@@ -26487,6 +26579,25 @@ import {
|
|
|
26487
26579
|
var listLocalProducts = (_args, _ctx) => {
|
|
26488
26580
|
const cwd = process.cwd();
|
|
26489
26581
|
const products = [];
|
|
26582
|
+
const membership = /* @__PURE__ */ new Map();
|
|
26583
|
+
try {
|
|
26584
|
+
const pdoc = JSON.parse(fs.readFileSync(path3.join(cwd, ".upg", "portfolio.upg"), "utf-8"));
|
|
26585
|
+
for (const area of pdoc.product_areas ?? []) {
|
|
26586
|
+
for (const pid of area.products ?? []) {
|
|
26587
|
+
const m = membership.get(pid) ?? { areas: [], portfolios: [] };
|
|
26588
|
+
m.areas.push(area.title ?? area.id);
|
|
26589
|
+
membership.set(pid, m);
|
|
26590
|
+
}
|
|
26591
|
+
}
|
|
26592
|
+
for (const pf of pdoc.portfolios ?? []) {
|
|
26593
|
+
for (const pid of pf.products ?? []) {
|
|
26594
|
+
const m = membership.get(pid) ?? { areas: [], portfolios: [] };
|
|
26595
|
+
m.portfolios.push(pf.title ?? pf.id);
|
|
26596
|
+
membership.set(pid, m);
|
|
26597
|
+
}
|
|
26598
|
+
}
|
|
26599
|
+
} catch {
|
|
26600
|
+
}
|
|
26490
26601
|
const candidates = [];
|
|
26491
26602
|
const topEntries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
26492
26603
|
for (const entry of topEntries) {
|
|
@@ -26511,13 +26622,19 @@ var listLocalProducts = (_args, _ctx) => {
|
|
|
26511
26622
|
try {
|
|
26512
26623
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
26513
26624
|
const doc = JSON.parse(raw);
|
|
26625
|
+
if (!doc.product) continue;
|
|
26514
26626
|
const coerced = coerceProductStage(doc.product?.stage);
|
|
26627
|
+
const pid = doc.product?.id ?? null;
|
|
26628
|
+
const m = pid ? membership.get(pid) : void 0;
|
|
26515
26629
|
products.push({
|
|
26630
|
+
id: pid,
|
|
26516
26631
|
file: path3.relative(cwd, filePath),
|
|
26517
26632
|
title: doc.product?.title ?? "(untitled)",
|
|
26518
26633
|
stage: coerced.canonical ?? null,
|
|
26519
26634
|
nodes: Array.isArray(doc.nodes) ? doc.nodes.length : 0,
|
|
26520
|
-
edges: Array.isArray(doc.edges) ? doc.edges.length : 0
|
|
26635
|
+
edges: Array.isArray(doc.edges) ? doc.edges.length : 0,
|
|
26636
|
+
...m && m.areas.length > 0 ? { areas: m.areas } : {},
|
|
26637
|
+
...m && m.portfolios.length > 0 ? { portfolios: m.portfolios } : {}
|
|
26521
26638
|
});
|
|
26522
26639
|
} catch {
|
|
26523
26640
|
}
|
|
@@ -26645,7 +26762,8 @@ var createProductTool = async (args, ctx) => {
|
|
|
26645
26762
|
slug: args.slug,
|
|
26646
26763
|
description: args.description,
|
|
26647
26764
|
stage: args.stage,
|
|
26648
|
-
portfolio_id: args.portfolio_id
|
|
26765
|
+
portfolio_id: args.portfolio_id,
|
|
26766
|
+
area_id: args.area_id
|
|
26649
26767
|
});
|
|
26650
26768
|
return text(
|
|
26651
26769
|
JSON.stringify({ message: `Created product: ${result.title}`, ...result }, null, 2)
|
|
@@ -26658,6 +26776,31 @@ var createProductTool = async (args, ctx) => {
|
|
|
26658
26776
|
return textError(`create_product failed: ${err.message}`);
|
|
26659
26777
|
}
|
|
26660
26778
|
};
|
|
26779
|
+
var updateProductTool = async (args, ctx) => {
|
|
26780
|
+
const { store } = ctx;
|
|
26781
|
+
try {
|
|
26782
|
+
const result = updateProduct({
|
|
26783
|
+
store,
|
|
26784
|
+
stage: args.stage,
|
|
26785
|
+
title: args.title,
|
|
26786
|
+
description: args.description,
|
|
26787
|
+
health_status: args.health_status,
|
|
26788
|
+
url: args.url
|
|
26789
|
+
});
|
|
26790
|
+
if (result.updated.length === 0) {
|
|
26791
|
+
return textError(
|
|
26792
|
+
"Nothing to update: pass at least one of: stage, title, description, health_status, url."
|
|
26793
|
+
);
|
|
26794
|
+
}
|
|
26795
|
+
await store.flush();
|
|
26796
|
+
return text(
|
|
26797
|
+
JSON.stringify({ message: `Updated product (${result.updated.join(", ")})`, ...result }, null, 2)
|
|
26798
|
+
);
|
|
26799
|
+
} catch (err) {
|
|
26800
|
+
if (err instanceof InvalidProductStageError) return textError(err.message);
|
|
26801
|
+
return textError(`update_product failed: ${err.message}`);
|
|
26802
|
+
}
|
|
26803
|
+
};
|
|
26661
26804
|
var listPortfolios = async (_args, _ctx) => {
|
|
26662
26805
|
const portfolioStore = await openPortfolioStoreIfExists2(process.cwd());
|
|
26663
26806
|
if (!portfolioStore) {
|
|
@@ -26867,6 +27010,141 @@ var migrateCrossEdges = async (args, ctx) => {
|
|
|
26867
27010
|
)
|
|
26868
27011
|
);
|
|
26869
27012
|
};
|
|
27013
|
+
var attachProductToPortfolioTool = async (args, _ctx) => {
|
|
27014
|
+
const productId = args.product_id;
|
|
27015
|
+
const portfolioId = args.portfolio_id;
|
|
27016
|
+
if (!productId) return textError("Missing required parameter: product_id");
|
|
27017
|
+
if (!portfolioId) return textError("Missing required parameter: portfolio_id");
|
|
27018
|
+
try {
|
|
27019
|
+
const result = await attachProductToPortfolio(process.cwd(), {
|
|
27020
|
+
product_id: productId,
|
|
27021
|
+
portfolio_id: portfolioId
|
|
27022
|
+
});
|
|
27023
|
+
return text(JSON.stringify(result, null, 2));
|
|
27024
|
+
} catch (err) {
|
|
27025
|
+
return textError(err.message);
|
|
27026
|
+
}
|
|
27027
|
+
};
|
|
27028
|
+
var detachProductFromPortfolioTool = async (args, _ctx) => {
|
|
27029
|
+
const productId = args.product_id;
|
|
27030
|
+
const portfolioId = args.portfolio_id;
|
|
27031
|
+
if (!productId) return textError("Missing required parameter: product_id");
|
|
27032
|
+
if (!portfolioId) return textError("Missing required parameter: portfolio_id");
|
|
27033
|
+
try {
|
|
27034
|
+
const result = await detachProductFromPortfolio(process.cwd(), {
|
|
27035
|
+
product_id: productId,
|
|
27036
|
+
portfolio_id: portfolioId
|
|
27037
|
+
});
|
|
27038
|
+
return text(JSON.stringify(result, null, 2));
|
|
27039
|
+
} catch (err) {
|
|
27040
|
+
return textError(err.message);
|
|
27041
|
+
}
|
|
27042
|
+
};
|
|
27043
|
+
var deleteCrossProductEdgeTool = async (args, _ctx) => {
|
|
27044
|
+
const edgeIdArg = args.edge_id;
|
|
27045
|
+
if (!edgeIdArg) return textError("Missing required parameter: edge_id");
|
|
27046
|
+
try {
|
|
27047
|
+
const result = await deleteCrossProductEdge(process.cwd(), edgeIdArg);
|
|
27048
|
+
return text(JSON.stringify(result, null, 2));
|
|
27049
|
+
} catch (err) {
|
|
27050
|
+
return textError(err.message);
|
|
27051
|
+
}
|
|
27052
|
+
};
|
|
27053
|
+
var batchCreateCrossProductEdges = async (args, _ctx) => {
|
|
27054
|
+
const edgesArg = args.edges;
|
|
27055
|
+
if (!Array.isArray(edgesArg) || edgesArg.length === 0) {
|
|
27056
|
+
return textError("Missing required parameter: edges (a non-empty array).");
|
|
27057
|
+
}
|
|
27058
|
+
if (edgesArg.length > 50) {
|
|
27059
|
+
return textError(`Too many edges: ${edgesArg.length}. Max 50 per batch_create_cross_product_edges call.`);
|
|
27060
|
+
}
|
|
27061
|
+
const cwd = process.cwd();
|
|
27062
|
+
const portfolioPath = resolvePortfolioPath(cwd);
|
|
27063
|
+
if (!portfolioPath) {
|
|
27064
|
+
return textError("No workspace found. Run `init_workspace` first to enable portfolio cross-product edges.");
|
|
27065
|
+
}
|
|
27066
|
+
const autoCreatePortfolio = args.auto_create_portfolio ?? false;
|
|
27067
|
+
const portfolioExisted = fs.existsSync(portfolioPath);
|
|
27068
|
+
if (!portfolioExisted && !autoCreatePortfolio) {
|
|
27069
|
+
return textError(
|
|
27070
|
+
'No portfolio document found at .upg/portfolio.upg. Cross-product edges express portfolio-level relationships and should be anchored to a portfolio that contains the products. Create a portfolio first (`create_node({type: "portfolio", title: "..."})`), or pass `auto_create_portfolio: true`.'
|
|
27071
|
+
);
|
|
27072
|
+
}
|
|
27073
|
+
const prepared = [];
|
|
27074
|
+
for (let i = 0; i < edgesArg.length; i++) {
|
|
27075
|
+
const e = edgesArg[i];
|
|
27076
|
+
const sourceIdArg = e.source_id;
|
|
27077
|
+
const targetIdArg = e.target_id;
|
|
27078
|
+
const edgeTypeArg = e.type;
|
|
27079
|
+
const sourceProductId = e.source_product_id;
|
|
27080
|
+
const targetProductId = e.target_product_id;
|
|
27081
|
+
if (!sourceIdArg) return textError(`edges[${i}]: missing source_id`);
|
|
27082
|
+
if (!targetIdArg) return textError(`edges[${i}]: missing target_id`);
|
|
27083
|
+
if (!edgeTypeArg) return textError(`edges[${i}]: missing type`);
|
|
27084
|
+
if (!UPG_CROSS_EDGE_TYPES.includes(edgeTypeArg)) {
|
|
27085
|
+
return textError(`edges[${i}]: invalid cross-product edge type "${edgeTypeArg}". Valid types: ${UPG_CROSS_EDGE_TYPES.join(", ")}`);
|
|
27086
|
+
}
|
|
27087
|
+
let qualifiedSource;
|
|
27088
|
+
if (sourceIdArg.includes("/")) qualifiedSource = sourceIdArg;
|
|
27089
|
+
else if (sourceProductId) qualifiedSource = `${sourceProductId}/${sourceIdArg}`;
|
|
27090
|
+
else return textError(`edges[${i}]: source_id "${sourceIdArg}" is a bare node id. Supply source_product_id or a qualified {product_id}/{node_id}.`);
|
|
27091
|
+
let qualifiedTarget;
|
|
27092
|
+
if (targetIdArg.includes("/")) qualifiedTarget = targetIdArg;
|
|
27093
|
+
else if (targetProductId) qualifiedTarget = `${targetProductId}/${targetIdArg}`;
|
|
27094
|
+
else return textError(`edges[${i}]: target_id "${targetIdArg}" is a bare node id. Supply target_product_id or a qualified {product_id}/{node_id}.`);
|
|
27095
|
+
prepared.push({
|
|
27096
|
+
id: edgeId3(),
|
|
27097
|
+
source: qualifiedSource,
|
|
27098
|
+
target: qualifiedTarget,
|
|
27099
|
+
type: edgeTypeArg,
|
|
27100
|
+
source_product_id: sourceProductId ?? qualifiedSource.split("/")[0],
|
|
27101
|
+
target_product_id: targetProductId ?? qualifiedTarget.split("/")[0]
|
|
27102
|
+
});
|
|
27103
|
+
}
|
|
27104
|
+
const portfolioStore = new UPGPortfolioStore();
|
|
27105
|
+
try {
|
|
27106
|
+
await portfolioStore.loadOrInit(portfolioPath);
|
|
27107
|
+
} catch (err) {
|
|
27108
|
+
return textError(`Failed to load portfolio document: ${err.message}`);
|
|
27109
|
+
}
|
|
27110
|
+
const registeredProducts = [];
|
|
27111
|
+
const portfolioDoc = portfolioStore.getDocument();
|
|
27112
|
+
if (portfolioDoc) {
|
|
27113
|
+
const productIds = /* @__PURE__ */ new Set();
|
|
27114
|
+
for (const e of prepared) {
|
|
27115
|
+
if (e.source_product_id) productIds.add(e.source_product_id);
|
|
27116
|
+
if (e.target_product_id) productIds.add(e.target_product_id);
|
|
27117
|
+
}
|
|
27118
|
+
for (const pid of productIds) {
|
|
27119
|
+
const lookup = findProductFileById(cwd, pid);
|
|
27120
|
+
const wasNew = registerProductOnPortfolio(portfolioDoc, {
|
|
27121
|
+
id: pid,
|
|
27122
|
+
...lookup ? { file_path: lookup.file_path, title: lookup.title } : {}
|
|
27123
|
+
});
|
|
27124
|
+
if (wasNew) registeredProducts.push({ id: pid, ...lookup ? { file_path: lookup.file_path, title: lookup.title } : {} });
|
|
27125
|
+
}
|
|
27126
|
+
if (registeredProducts.length > 0) portfolioStore.markDirty();
|
|
27127
|
+
}
|
|
27128
|
+
try {
|
|
27129
|
+
for (const e of prepared) portfolioStore.addCrossEdge(e);
|
|
27130
|
+
await portfolioStore.flush();
|
|
27131
|
+
} catch (err) {
|
|
27132
|
+
return textError(`Failed to write cross-product edges: ${err.message}`);
|
|
27133
|
+
}
|
|
27134
|
+
return text(
|
|
27135
|
+
JSON.stringify(
|
|
27136
|
+
{
|
|
27137
|
+
message: `Created ${prepared.length} cross-product edge(s)`,
|
|
27138
|
+
created: prepared,
|
|
27139
|
+
count: prepared.length,
|
|
27140
|
+
portfolio_file: path3.relative(cwd, portfolioPath),
|
|
27141
|
+
...registeredProducts.length > 0 ? { registered_products: registeredProducts } : {}
|
|
27142
|
+
},
|
|
27143
|
+
null,
|
|
27144
|
+
2
|
|
27145
|
+
)
|
|
27146
|
+
);
|
|
27147
|
+
};
|
|
26870
27148
|
|
|
26871
27149
|
// src/tools/schema.ts
|
|
26872
27150
|
var getEntitySchema = (args, _ctx) => {
|
|
@@ -29138,12 +29416,30 @@ var TOOL_DEFINITIONS = [
|
|
|
29138
29416
|
},
|
|
29139
29417
|
portfolio_id: {
|
|
29140
29418
|
type: "string",
|
|
29141
|
-
description: "Optional portfolio
|
|
29419
|
+
description: "Optional portfolio id (resolved against portfolio.upg) to place the new product under. A portfolio id that resolves only in the active graph still attaches via an in-graph edge (DEPRECATED; prefer attach_product_to_portfolio)."
|
|
29420
|
+
},
|
|
29421
|
+
area_id: {
|
|
29422
|
+
type: "string",
|
|
29423
|
+
description: "Optional product_area id (resolved against portfolio.upg) to place the new product under."
|
|
29142
29424
|
}
|
|
29143
29425
|
},
|
|
29144
29426
|
required: ["name"]
|
|
29145
29427
|
}
|
|
29146
29428
|
},
|
|
29429
|
+
{
|
|
29430
|
+
name: "update_product",
|
|
29431
|
+
description: "Update the product header (`$upg.product`): stage, title, description, health_status, url. The supported way to advance a product's lifecycle stage; it writes the value get_graph_digest reads, without hand-editing the .upg file.",
|
|
29432
|
+
inputSchema: {
|
|
29433
|
+
type: "object",
|
|
29434
|
+
properties: {
|
|
29435
|
+
stage: { type: "string", description: "Product lifecycle stage (canonical UPGProductStage)." },
|
|
29436
|
+
title: { type: "string", description: "Product display title." },
|
|
29437
|
+
description: { type: "string", description: "Product description." },
|
|
29438
|
+
health_status: { type: "string", description: "Product health (free-form, e.g. on_track / at_risk)." },
|
|
29439
|
+
url: { type: "string", description: "Product URL." }
|
|
29440
|
+
}
|
|
29441
|
+
}
|
|
29442
|
+
},
|
|
29147
29443
|
{
|
|
29148
29444
|
name: "migrate_type",
|
|
29149
29445
|
description: "Migrate every entity of one type to another, applying defaults from `UPG_MIGRATIONS`. Three passes commit as one write: (1) node rename, (2) edges through `UPG_EDGE_MIGRATIONS` (catalog-aware renames, direction flips, drops; endpoint guards check post-migration types; uncatalogued edges surface as `unmapped_legacy_edges`), (3) every node through `UPG_PROPERTY_MIGRATIONS` (top-level renames, lifts, drops, self-referential cleanup). Type-specific property rules see the post-rename type.",
|
|
@@ -29475,7 +29771,7 @@ var TOOL_DEFINITIONS = [
|
|
|
29475
29771
|
},
|
|
29476
29772
|
{
|
|
29477
29773
|
name: "list_cross_edge_types",
|
|
29478
|
-
description: "List the canonical cross-product edge types from `UPG_CROSS_EDGE_TYPES`: `shares_persona`, `shares_competitor`, `shares_metric`, `depends_on_product`, `cannibalises`, `succeeds`. Portfolio-level relationships across products. Distinct from the within-product `UPG_EDGE_CATALOG`.",
|
|
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`.",
|
|
29479
29775
|
inputSchema: { type: "object", properties: {} }
|
|
29480
29776
|
},
|
|
29481
29777
|
{
|
|
@@ -29840,14 +30136,110 @@ var TOOL_DEFINITIONS = [
|
|
|
29840
30136
|
},
|
|
29841
30137
|
strategic_priority: {
|
|
29842
30138
|
type: "string",
|
|
29843
|
-
enum: ["
|
|
29844
|
-
description: "Strategic priority of this area"
|
|
30139
|
+
enum: ["urgent", "high", "medium", "low", "none"],
|
|
30140
|
+
description: "Strategic priority of this area (canonical Priority scale)"
|
|
29845
30141
|
},
|
|
29846
30142
|
owner: { type: "string", description: "Person or team that owns this area" }
|
|
29847
30143
|
},
|
|
29848
30144
|
required: ["title"]
|
|
29849
30145
|
}
|
|
29850
30146
|
},
|
|
30147
|
+
{
|
|
30148
|
+
name: "assign_product_to_area",
|
|
30149
|
+
description: "Place an existing product under a product area (adds it to the area's `products[]` in `.upg/portfolio.upg`). Resolves the area against the portfolio document and auto-registers the product on the portfolio registry. Use after `create_product`, or pass `area_id` to `create_product` directly.",
|
|
30150
|
+
inputSchema: {
|
|
30151
|
+
type: "object",
|
|
30152
|
+
properties: {
|
|
30153
|
+
product_id: { type: "string", description: "Product id (from create_product / list_local_products)" },
|
|
30154
|
+
area_id: { type: "string", description: "Product area id (from list_product_areas)" }
|
|
30155
|
+
},
|
|
30156
|
+
required: ["product_id", "area_id"]
|
|
30157
|
+
}
|
|
30158
|
+
},
|
|
30159
|
+
{
|
|
30160
|
+
name: "update_area",
|
|
30161
|
+
description: "Edit a product area in `.upg/portfolio.upg` (title, description, strategic_priority, owner) and/or re-parent it via `parent_area_id`. The mirror of `update_product` for the organisational axis. `parent_area_id` is tri-state: omit to leave unchanged, pass null to un-nest (top-level), or pass an area id to re-parent (rejected if it would create a cycle).",
|
|
30162
|
+
inputSchema: {
|
|
30163
|
+
type: "object",
|
|
30164
|
+
properties: {
|
|
30165
|
+
area_id: { type: "string", description: "Product area id to edit (from list_product_areas)" },
|
|
30166
|
+
title: { type: "string", description: "New area title" },
|
|
30167
|
+
description: { type: "string", description: "New area description" },
|
|
30168
|
+
strategic_priority: {
|
|
30169
|
+
type: "string",
|
|
30170
|
+
enum: ["urgent", "high", "medium", "low", "none"],
|
|
30171
|
+
description: "Strategic priority (canonical Priority scale)"
|
|
30172
|
+
},
|
|
30173
|
+
parent_area_id: {
|
|
30174
|
+
type: ["string", "null"],
|
|
30175
|
+
description: "Re-parent under this area id; null un-nests (top-level); omit to leave unchanged"
|
|
30176
|
+
},
|
|
30177
|
+
owner: { type: "string", description: "Person or team that owns this area" }
|
|
30178
|
+
},
|
|
30179
|
+
required: ["area_id"]
|
|
30180
|
+
}
|
|
30181
|
+
},
|
|
30182
|
+
{
|
|
30183
|
+
name: "remove_product_from_area",
|
|
30184
|
+
description: "Remove a product from a product area's `products[]` in `.upg/portfolio.upg` (the product stays registered on the portfolio and in any other container). The inverse of `assign_product_to_area`.",
|
|
30185
|
+
inputSchema: {
|
|
30186
|
+
type: "object",
|
|
30187
|
+
properties: {
|
|
30188
|
+
product_id: { type: "string", description: "Product id (from list_local_products)" },
|
|
30189
|
+
area_id: { type: "string", description: "Product area id (from list_product_areas)" }
|
|
30190
|
+
},
|
|
30191
|
+
required: ["product_id", "area_id"]
|
|
30192
|
+
}
|
|
30193
|
+
},
|
|
30194
|
+
{
|
|
30195
|
+
name: "delete_area",
|
|
30196
|
+
description: "Delete a product area from `.upg/portfolio.upg`. Guarded: refuses while the area still has products unless `force: true`. Child areas are un-nested (their parent link is cleared) so no parent reference dangles.",
|
|
30197
|
+
inputSchema: {
|
|
30198
|
+
type: "object",
|
|
30199
|
+
properties: {
|
|
30200
|
+
area_id: { type: "string", description: "Product area id to delete (from list_product_areas)" },
|
|
30201
|
+
force: { type: "boolean", description: "Delete even if the area still has products (default false)" }
|
|
30202
|
+
},
|
|
30203
|
+
required: ["area_id"]
|
|
30204
|
+
}
|
|
30205
|
+
},
|
|
30206
|
+
{
|
|
30207
|
+
name: "move_product_to_area",
|
|
30208
|
+
description: "Move a product to a different product area: remove it from `from_area_id` (or, when omitted, from every area it currently sits in) and add it to `to_area_id`. Convenience over remove_product_from_area + assign_product_to_area.",
|
|
30209
|
+
inputSchema: {
|
|
30210
|
+
type: "object",
|
|
30211
|
+
properties: {
|
|
30212
|
+
product_id: { type: "string", description: "Product id (from list_local_products)" },
|
|
30213
|
+
to_area_id: { type: "string", description: "Destination product area id (from list_product_areas)" },
|
|
30214
|
+
from_area_id: { type: "string", description: "Source area id to remove from; omit to remove from all areas" }
|
|
30215
|
+
},
|
|
30216
|
+
required: ["product_id", "to_area_id"]
|
|
30217
|
+
}
|
|
30218
|
+
},
|
|
30219
|
+
{
|
|
30220
|
+
name: "attach_product_to_portfolio",
|
|
30221
|
+
description: "Place an existing product under a portfolio (adds it to the portfolio's `products[]` in `.upg/portfolio.upg`). Resolves the portfolio against the portfolio document and auto-registers the product on the portfolio registry. Use after `create_product`, or pass `portfolio_id` to `create_product` directly.",
|
|
30222
|
+
inputSchema: {
|
|
30223
|
+
type: "object",
|
|
30224
|
+
properties: {
|
|
30225
|
+
product_id: { type: "string", description: "Product id (from create_product / list_local_products)" },
|
|
30226
|
+
portfolio_id: { type: "string", description: "Portfolio id (from list_portfolios)" }
|
|
30227
|
+
},
|
|
30228
|
+
required: ["product_id", "portfolio_id"]
|
|
30229
|
+
}
|
|
30230
|
+
},
|
|
30231
|
+
{
|
|
30232
|
+
name: "detach_product_from_portfolio",
|
|
30233
|
+
description: "Remove a product from a portfolio's `products[]` in `.upg/portfolio.upg` (the product stays registered and in any other container). The inverse of `attach_product_to_portfolio`.",
|
|
30234
|
+
inputSchema: {
|
|
30235
|
+
type: "object",
|
|
30236
|
+
properties: {
|
|
30237
|
+
product_id: { type: "string", description: "Product id (from list_local_products)" },
|
|
30238
|
+
portfolio_id: { type: "string", description: "Portfolio id (from list_portfolios)" }
|
|
30239
|
+
},
|
|
30240
|
+
required: ["product_id", "portfolio_id"]
|
|
30241
|
+
}
|
|
30242
|
+
},
|
|
29851
30243
|
{
|
|
29852
30244
|
name: "list_portfolios",
|
|
29853
30245
|
description: "List portfolios from the portfolio document (`.upg/portfolio.upg`). Portfolios represent the strategic axis (where we invest). Returns an empty list when no portfolio document exists yet.",
|
|
@@ -29860,7 +30252,7 @@ var TOOL_DEFINITIONS = [
|
|
|
29860
30252
|
},
|
|
29861
30253
|
{
|
|
29862
30254
|
name: "create_cross_product_edge",
|
|
29863
|
-
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
|
|
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).",
|
|
29864
30256
|
inputSchema: {
|
|
29865
30257
|
type: "object",
|
|
29866
30258
|
properties: {
|
|
@@ -29868,7 +30260,7 @@ var TOOL_DEFINITIONS = [
|
|
|
29868
30260
|
target_id: { type: "string", description: "Target node ID" },
|
|
29869
30261
|
type: {
|
|
29870
30262
|
type: "string",
|
|
29871
|
-
enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds"],
|
|
30263
|
+
enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds", "hosts"],
|
|
29872
30264
|
description: "Cross-product relationship type"
|
|
29873
30265
|
},
|
|
29874
30266
|
source_product_id: { type: "string", description: "Product ID of the source node" },
|
|
@@ -29877,6 +30269,47 @@ var TOOL_DEFINITIONS = [
|
|
|
29877
30269
|
required: ["source_id", "target_id", "type"]
|
|
29878
30270
|
}
|
|
29879
30271
|
},
|
|
30272
|
+
{
|
|
30273
|
+
name: "delete_cross_product_edge",
|
|
30274
|
+
description: "Delete a cross-product edge from `.upg/portfolio.upg` by id. The inverse of `create_cross_product_edge`. Returns `deleted: false` (not an error) when no edge with that id exists.",
|
|
30275
|
+
inputSchema: {
|
|
30276
|
+
type: "object",
|
|
30277
|
+
properties: {
|
|
30278
|
+
edge_id: { type: "string", description: "Cross-product edge id (from list_portfolio_cross_edges)" }
|
|
30279
|
+
},
|
|
30280
|
+
required: ["edge_id"]
|
|
30281
|
+
}
|
|
30282
|
+
},
|
|
30283
|
+
{
|
|
30284
|
+
name: "batch_create_cross_product_edges",
|
|
30285
|
+
description: "Create up to 50 cross-product edges in one atomic write (the portfolio-tier mirror of batch_create_edges). Every edge is validated and qualified before anything is written; if any is invalid the whole batch is rejected. Referenced products are auto-registered.",
|
|
30286
|
+
inputSchema: {
|
|
30287
|
+
type: "object",
|
|
30288
|
+
properties: {
|
|
30289
|
+
edges: {
|
|
30290
|
+
type: "array",
|
|
30291
|
+
description: "Cross-product edges to create (max 50). Each: { source_id, target_id, type, source_product_id?, target_product_id? }.",
|
|
30292
|
+
items: {
|
|
30293
|
+
type: "object",
|
|
30294
|
+
properties: {
|
|
30295
|
+
source_id: { type: "string", description: "Source node ID (bare or qualified {product_id}/{node_id})" },
|
|
30296
|
+
target_id: { type: "string", description: "Target node ID (bare or qualified {product_id}/{node_id})" },
|
|
30297
|
+
type: {
|
|
30298
|
+
type: "string",
|
|
30299
|
+
enum: ["shares_persona", "shares_competitor", "shares_metric", "depends_on_product", "cannibalises", "succeeds", "hosts"],
|
|
30300
|
+
description: "Cross-product relationship type"
|
|
30301
|
+
},
|
|
30302
|
+
source_product_id: { type: "string", description: "Product ID of the source node (qualifies a bare source_id)" },
|
|
30303
|
+
target_product_id: { type: "string", description: "Product ID of the target node (qualifies a bare target_id)" }
|
|
30304
|
+
},
|
|
30305
|
+
required: ["source_id", "target_id", "type"]
|
|
30306
|
+
}
|
|
30307
|
+
},
|
|
30308
|
+
auto_create_portfolio: { type: "boolean", description: "Create an empty portfolio document if none exists (default false)" }
|
|
30309
|
+
},
|
|
30310
|
+
required: ["edges"]
|
|
30311
|
+
}
|
|
30312
|
+
},
|
|
29880
30313
|
{
|
|
29881
30314
|
name: "list_portfolio_cross_edges",
|
|
29882
30315
|
description: "List all cross-product edges stored in the portfolio document (`.upg/portfolio.upg`). Empty list when the portfolio document is absent.",
|
|
@@ -29999,6 +30432,7 @@ var HANDLERS = {
|
|
|
29999
30432
|
get_workspace_info: getWorkspaceInfo,
|
|
30000
30433
|
init_workspace: initWorkspaceTool,
|
|
30001
30434
|
create_product: createProductTool,
|
|
30435
|
+
update_product: updateProductTool,
|
|
30002
30436
|
migrate_type: migrateType,
|
|
30003
30437
|
migrate_properties: migrateProperties,
|
|
30004
30438
|
migrate_status: migrateStatus,
|
|
@@ -30054,9 +30488,18 @@ var HANDLERS = {
|
|
|
30054
30488
|
skill_audit: skillAudit,
|
|
30055
30489
|
get_area_context: getAreaContext,
|
|
30056
30490
|
create_area: createArea,
|
|
30491
|
+
assign_product_to_area: assignProductToAreaTool,
|
|
30492
|
+
update_area: updateAreaTool,
|
|
30493
|
+
remove_product_from_area: removeProductFromAreaTool,
|
|
30494
|
+
delete_area: deleteAreaTool,
|
|
30495
|
+
move_product_to_area: moveProductToAreaTool,
|
|
30057
30496
|
list_portfolios: listPortfolios,
|
|
30058
30497
|
get_organization: getOrganization,
|
|
30059
30498
|
create_cross_product_edge: createCrossProductEdge,
|
|
30499
|
+
delete_cross_product_edge: deleteCrossProductEdgeTool,
|
|
30500
|
+
batch_create_cross_product_edges: batchCreateCrossProductEdges,
|
|
30501
|
+
attach_product_to_portfolio: attachProductToPortfolioTool,
|
|
30502
|
+
detach_product_from_portfolio: detachProductFromPortfolioTool,
|
|
30060
30503
|
list_portfolio_cross_edges: listPortfolioCrossEdges,
|
|
30061
30504
|
migrate_cross_edges: migrateCrossEdges,
|
|
30062
30505
|
get_sync_state: getSyncState,
|