@unified-product-graph/cloud-server 0.8.5 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +212 -10
- package/dist/index.js.map +1 -1
- package/dist/tools-manifest.json +97 -2
- package/migrations/005_edge_properties.sql +14 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -386,14 +386,14 @@ var UPGPgStore = class _UPGPgStore {
|
|
|
386
386
|
// ── Edges ──────────────────────────────────────────────────────────────────
|
|
387
387
|
async getAllEdges(productId) {
|
|
388
388
|
const { rows } = await this.pool.query(
|
|
389
|
-
`SELECT id, source, target, type FROM upg.edges WHERE product_id = $1`,
|
|
389
|
+
`SELECT id, source, target, type, properties FROM upg.edges WHERE product_id = $1`,
|
|
390
390
|
[productId]
|
|
391
391
|
);
|
|
392
392
|
return rows.map(rowToEdge);
|
|
393
393
|
}
|
|
394
394
|
async getEdgesForNode(nodeId2) {
|
|
395
395
|
const { rows } = await this.pool.query(
|
|
396
|
-
`SELECT id, source, target, type FROM upg.edges
|
|
396
|
+
`SELECT id, source, target, type, properties FROM upg.edges
|
|
397
397
|
WHERE source = $1 OR target = $1`,
|
|
398
398
|
[nodeId2]
|
|
399
399
|
);
|
|
@@ -405,9 +405,16 @@ var UPGPgStore = class _UPGPgStore {
|
|
|
405
405
|
try {
|
|
406
406
|
await client.query("BEGIN");
|
|
407
407
|
await client.query(
|
|
408
|
-
`INSERT INTO upg.edges (id, product_id, source, target, type)
|
|
409
|
-
VALUES ($1, $2, $3, $4, $5)`,
|
|
410
|
-
[
|
|
408
|
+
`INSERT INTO upg.edges (id, product_id, source, target, type, properties)
|
|
409
|
+
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
410
|
+
[
|
|
411
|
+
id,
|
|
412
|
+
productId,
|
|
413
|
+
edge.source,
|
|
414
|
+
edge.target,
|
|
415
|
+
edge.type,
|
|
416
|
+
edge.properties ? JSON.stringify(edge.properties) : null
|
|
417
|
+
]
|
|
411
418
|
);
|
|
412
419
|
await appendAudit(client, {
|
|
413
420
|
productId,
|
|
@@ -431,7 +438,7 @@ var UPGPgStore = class _UPGPgStore {
|
|
|
431
438
|
await client.query("BEGIN");
|
|
432
439
|
const { rows } = await client.query(
|
|
433
440
|
`DELETE FROM upg.edges WHERE id = $1
|
|
434
|
-
RETURNING id, product_id, source, target, type`,
|
|
441
|
+
RETURNING id, product_id, source, target, type, properties`,
|
|
435
442
|
[id]
|
|
436
443
|
);
|
|
437
444
|
if (rows.length === 0) {
|
|
@@ -455,6 +462,46 @@ var UPGPgStore = class _UPGPgStore {
|
|
|
455
462
|
client.release();
|
|
456
463
|
}
|
|
457
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* Set (or merge) the gated `properties` payload on a single edge (0.8.6
|
|
467
|
+
* framework-exercise parity). Merge is the default: the supplied keys are
|
|
468
|
+
* deep-merged at the top level via JSONB `||`; pass `{ merge: false }` to
|
|
469
|
+
* replace the payload wholesale. Mirrors the local SDK's `setEdgeProperties`.
|
|
470
|
+
*/
|
|
471
|
+
async setEdgeProperties(id, values, opts = {}) {
|
|
472
|
+
const merge = opts.merge !== false;
|
|
473
|
+
const client = await this.pool.connect();
|
|
474
|
+
try {
|
|
475
|
+
await client.query("BEGIN");
|
|
476
|
+
const sql = merge ? `UPDATE upg.edges
|
|
477
|
+
SET properties = COALESCE(properties, '{}'::jsonb) || $1::jsonb
|
|
478
|
+
WHERE id = $2
|
|
479
|
+
RETURNING id, product_id, source, target, type, properties` : `UPDATE upg.edges
|
|
480
|
+
SET properties = $1::jsonb
|
|
481
|
+
WHERE id = $2
|
|
482
|
+
RETURNING id, product_id, source, target, type, properties`;
|
|
483
|
+
const { rows } = await client.query(sql, [JSON.stringify(values), id]);
|
|
484
|
+
if (rows.length === 0) {
|
|
485
|
+
await client.query("ROLLBACK");
|
|
486
|
+
throw new Error(`Edge not found: ${id}`);
|
|
487
|
+
}
|
|
488
|
+
await appendAudit(client, {
|
|
489
|
+
productId: rows[0].product_id,
|
|
490
|
+
action: "update",
|
|
491
|
+
entityType: "edge",
|
|
492
|
+
entityId: id,
|
|
493
|
+
changes: values
|
|
494
|
+
});
|
|
495
|
+
await client.query("COMMIT");
|
|
496
|
+
this.emit(rows[0].product_id, "edge.updated", { id, properties: values });
|
|
497
|
+
return rowToEdge(rows[0]);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
await client.query("ROLLBACK");
|
|
500
|
+
throw err;
|
|
501
|
+
} finally {
|
|
502
|
+
client.release();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
458
505
|
// ── Edge utilities ─────────────────────────────────────
|
|
459
506
|
/**
|
|
460
507
|
* Flat enumeration of all edges for a product, optionally filtered by type.
|
|
@@ -861,7 +908,9 @@ function rowToNode(row) {
|
|
|
861
908
|
return node;
|
|
862
909
|
}
|
|
863
910
|
function rowToEdge(row) {
|
|
864
|
-
|
|
911
|
+
const edge = { id: row.id, source: row.source, target: row.target, type: row.type };
|
|
912
|
+
if (row.properties != null) edge.properties = row.properties;
|
|
913
|
+
return edge;
|
|
865
914
|
}
|
|
866
915
|
|
|
867
916
|
// src/lib/webhook-dispatcher.ts
|
|
@@ -1740,6 +1789,120 @@ var renameEdgeType = async (args, { store }) => {
|
|
|
1740
1789
|
}
|
|
1741
1790
|
};
|
|
1742
1791
|
|
|
1792
|
+
// src/tools/frameworks.ts
|
|
1793
|
+
import { UPG_FRAMEWORKS_BY_ID } from "@unified-product-graph/core";
|
|
1794
|
+
import {
|
|
1795
|
+
frameworkInputKeys,
|
|
1796
|
+
FRAMEWORK_EXERCISE_INCLUDES_EDGE
|
|
1797
|
+
} from "@unified-product-graph/sdk";
|
|
1798
|
+
var applyFramework = async (args, { store }) => {
|
|
1799
|
+
if (!args.product_id) return textError("Missing required parameter: product_id");
|
|
1800
|
+
const frameworkId = args.framework_id;
|
|
1801
|
+
if (!frameworkId) {
|
|
1802
|
+
return textError('Missing required parameter: framework_id (e.g. "moscow", "rice-scoring")');
|
|
1803
|
+
}
|
|
1804
|
+
const framework = UPG_FRAMEWORKS_BY_ID[frameworkId];
|
|
1805
|
+
if (!framework) {
|
|
1806
|
+
return textError(
|
|
1807
|
+
`Unknown framework: "${frameworkId}". Pass a framework id from the catalog (e.g. 'moscow', 'rice-scoring', 'kano-model').`
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
const productId = args.product_id;
|
|
1811
|
+
const warnings = [];
|
|
1812
|
+
try {
|
|
1813
|
+
const exercise = await store.addNode(productId, {
|
|
1814
|
+
id: nodeId(),
|
|
1815
|
+
type: "framework_exercise",
|
|
1816
|
+
title: args.title ?? `${framework.name} exercise`,
|
|
1817
|
+
status: args.status ?? "draft",
|
|
1818
|
+
properties: { framework_id: frameworkId }
|
|
1819
|
+
});
|
|
1820
|
+
const included = [];
|
|
1821
|
+
for (const entityId of args.entity_ids ?? []) {
|
|
1822
|
+
const target = await store.getNode(entityId);
|
|
1823
|
+
if (!target) {
|
|
1824
|
+
warnings.push(`Could not include ${entityId}: target not found`);
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
const edge = {
|
|
1828
|
+
id: edgeId(),
|
|
1829
|
+
source: exercise.id,
|
|
1830
|
+
target: entityId,
|
|
1831
|
+
type: FRAMEWORK_EXERCISE_INCLUDES_EDGE
|
|
1832
|
+
};
|
|
1833
|
+
try {
|
|
1834
|
+
await store.addEdge(productId, edge);
|
|
1835
|
+
included.push({ edge_id: edge.id, entity_id: entityId });
|
|
1836
|
+
} catch (err) {
|
|
1837
|
+
warnings.push(`Could not include ${entityId}: ${err.message}`);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
return text(
|
|
1841
|
+
JSON.stringify(
|
|
1842
|
+
{ exercise_id: exercise.id, exercise, included, warnings },
|
|
1843
|
+
null,
|
|
1844
|
+
2
|
|
1845
|
+
)
|
|
1846
|
+
);
|
|
1847
|
+
} catch (err) {
|
|
1848
|
+
return textError(err.message);
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
var scoreEntity = async (args, { store }) => {
|
|
1852
|
+
const exerciseId = args.exercise_id;
|
|
1853
|
+
const entityId = args.entity_id;
|
|
1854
|
+
const values = args.values;
|
|
1855
|
+
if (!exerciseId) return textError("Missing required parameter: exercise_id");
|
|
1856
|
+
if (!entityId) return textError("Missing required parameter: entity_id");
|
|
1857
|
+
if (!values || typeof values !== "object" || Array.isArray(values)) {
|
|
1858
|
+
return textError(
|
|
1859
|
+
'Missing required parameter: values (object of input \u2192 value, e.g. { "moscow": "must" })'
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
const exercise = await store.getNode(exerciseId);
|
|
1863
|
+
if (!exercise) return textError(`Exercise not found: ${exerciseId}`);
|
|
1864
|
+
if (exercise.type !== "framework_exercise") {
|
|
1865
|
+
return textError(`Node ${exerciseId} is a ${exercise.type}, not a framework_exercise.`);
|
|
1866
|
+
}
|
|
1867
|
+
const warnings = [];
|
|
1868
|
+
const frameworkId = exercise.properties?.framework_id;
|
|
1869
|
+
const framework = frameworkId ? UPG_FRAMEWORKS_BY_ID[frameworkId] : void 0;
|
|
1870
|
+
if (framework) {
|
|
1871
|
+
const known = new Set(frameworkInputKeys(framework));
|
|
1872
|
+
if (known.size > 0) {
|
|
1873
|
+
const unknown = Object.keys(values).filter((k) => !known.has(k));
|
|
1874
|
+
if (unknown.length > 0) {
|
|
1875
|
+
warnings.push(
|
|
1876
|
+
`Value key(s) not declared by ${frameworkId}: ${unknown.join(", ")}. Stored anyway (permissive).`
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
try {
|
|
1882
|
+
const edges = await store.getEdgesForNode(exerciseId);
|
|
1883
|
+
const existing = edges.find(
|
|
1884
|
+
(e) => e.type === FRAMEWORK_EXERCISE_INCLUDES_EDGE && e.source === exerciseId && e.target === entityId
|
|
1885
|
+
);
|
|
1886
|
+
if (existing) {
|
|
1887
|
+
const edge2 = await store.setEdgeProperties(existing.id, values, { merge: !args.replace });
|
|
1888
|
+
return text(JSON.stringify({ edge: edge2, warnings }, null, 2));
|
|
1889
|
+
}
|
|
1890
|
+
const target = await store.getNode(entityId);
|
|
1891
|
+
if (!target) return textError(`Entity not found: ${entityId}`);
|
|
1892
|
+
const edge = {
|
|
1893
|
+
id: edgeId(),
|
|
1894
|
+
source: exerciseId,
|
|
1895
|
+
target: entityId,
|
|
1896
|
+
type: FRAMEWORK_EXERCISE_INCLUDES_EDGE,
|
|
1897
|
+
properties: values
|
|
1898
|
+
};
|
|
1899
|
+
await store.addEdge(exercise.product_id, edge);
|
|
1900
|
+
return text(JSON.stringify({ edge, warnings }, null, 2));
|
|
1901
|
+
} catch (err) {
|
|
1902
|
+
return textError(err.message);
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
|
|
1743
1906
|
// src/tools/areas.ts
|
|
1744
1907
|
var listProductAreas = async (args, { store }) => {
|
|
1745
1908
|
if (!args.product_id) return textError(`Missing required parameter: product_id`);
|
|
@@ -1916,7 +2079,7 @@ import {
|
|
|
1916
2079
|
UPG_DOMAIN_GUIDES,
|
|
1917
2080
|
UPG_DOMAINS,
|
|
1918
2081
|
UPG_FRAMEWORKS,
|
|
1919
|
-
UPG_FRAMEWORKS_BY_ID,
|
|
2082
|
+
UPG_FRAMEWORKS_BY_ID as UPG_FRAMEWORKS_BY_ID2,
|
|
1920
2083
|
UPG_EDGE_CATALOG as UPG_EDGE_CATALOG2,
|
|
1921
2084
|
UPG_REGIONS,
|
|
1922
2085
|
UPG_REGION_MAP,
|
|
@@ -2061,7 +2224,7 @@ var prioritise = (args) => {
|
|
|
2061
2224
|
'Missing required parameter: framework_id (e.g. "rice-scoring", "ice-scoring", "kano-model")'
|
|
2062
2225
|
);
|
|
2063
2226
|
}
|
|
2064
|
-
const framework =
|
|
2227
|
+
const framework = UPG_FRAMEWORKS_BY_ID2[frameworkId];
|
|
2065
2228
|
return approachEnvelope("prioritise", candidates, {
|
|
2066
2229
|
params: { candidates, framework_id: frameworkId },
|
|
2067
2230
|
framework_resolved: framework ? { id: framework.id, name: framework.name, category: framework.category } : null,
|
|
@@ -2158,7 +2321,7 @@ var listFrameworks = (args) => {
|
|
|
2158
2321
|
var getFramework = (args) => {
|
|
2159
2322
|
const id = args.id;
|
|
2160
2323
|
if (!id) return textError("Missing required parameter: id");
|
|
2161
|
-
const framework =
|
|
2324
|
+
const framework = UPG_FRAMEWORKS_BY_ID2[id];
|
|
2162
2325
|
if (!framework) return textError(`Unknown framework id: ${id}`);
|
|
2163
2326
|
return text(JSON.stringify(framework, null, 2));
|
|
2164
2327
|
};
|
|
@@ -4940,6 +5103,42 @@ var TOOL_DEFINITIONS = [
|
|
|
4940
5103
|
"product_id"
|
|
4941
5104
|
]
|
|
4942
5105
|
}
|
|
5106
|
+
},
|
|
5107
|
+
{
|
|
5108
|
+
"name": "apply_framework",
|
|
5109
|
+
"description": "Apply a framework (MoSCoW, RICE, Kano, ...) to a set of entities in a product: creates a framework_exercise node and an `includes` edge to each entity. The per-entity result is recorded on the edge via score_entity, never on the entity node, so the same entity can sit in many exercises and any entity type can be scored. Returns { exercise_id, exercise, included, warnings }.",
|
|
5110
|
+
"inputSchema": {
|
|
5111
|
+
"type": "object",
|
|
5112
|
+
"properties": {
|
|
5113
|
+
"product_id": { "type": "string", "description": "Required. Product the exercise belongs to." },
|
|
5114
|
+
"framework_id": { "type": "string", "description": 'Required. UPGFramework.id (e.g. "moscow", "rice-scoring").' },
|
|
5115
|
+
"title": { "type": "string", "description": 'Human label for the exercise (default "<Framework> exercise").' },
|
|
5116
|
+
"entity_ids": { "type": "array", "items": { "type": "string" }, "description": "Entities to pull into the exercise (any type)." },
|
|
5117
|
+
"status": { "type": "string", "description": "Lifecycle phase: draft | active | archived (default draft)." }
|
|
5118
|
+
},
|
|
5119
|
+
"required": [
|
|
5120
|
+
"product_id",
|
|
5121
|
+
"framework_id"
|
|
5122
|
+
]
|
|
5123
|
+
}
|
|
5124
|
+
},
|
|
5125
|
+
{
|
|
5126
|
+
"name": "score_entity",
|
|
5127
|
+
"description": "Record a framework's result for one entity on the exercise's includes edge (a MoSCoW bucket, a RICE score, a canvas slot). Auto-includes the entity if not already in scope. Merges into existing edge properties unless replace is set. The product is resolved from the exercise node. Returns { edge, warnings }.",
|
|
5128
|
+
"inputSchema": {
|
|
5129
|
+
"type": "object",
|
|
5130
|
+
"properties": {
|
|
5131
|
+
"exercise_id": { "type": "string", "description": "Required. The framework_exercise id." },
|
|
5132
|
+
"entity_id": { "type": "string", "description": "Required. The entity being scored." },
|
|
5133
|
+
"values": { "type": "object", "description": 'Required. The result as { input: value }, e.g. { "moscow": "must" } or { "reach": 800, "impact": 3 }.' },
|
|
5134
|
+
"replace": { "type": "boolean", "description": "Replace the edge properties instead of merging (default false)." }
|
|
5135
|
+
},
|
|
5136
|
+
"required": [
|
|
5137
|
+
"exercise_id",
|
|
5138
|
+
"entity_id",
|
|
5139
|
+
"values"
|
|
5140
|
+
]
|
|
5141
|
+
}
|
|
4943
5142
|
}
|
|
4944
5143
|
];
|
|
4945
5144
|
var HANDLERS = {
|
|
@@ -4963,6 +5162,9 @@ var HANDLERS = {
|
|
|
4963
5162
|
delete_edge: deleteEdge,
|
|
4964
5163
|
export_edges: exportEdges,
|
|
4965
5164
|
rename_edge_type: renameEdgeType,
|
|
5165
|
+
// ── Framework exercises (0.8.6 cloud parity) ────────────────────
|
|
5166
|
+
apply_framework: applyFramework,
|
|
5167
|
+
score_entity: scoreEntity,
|
|
4966
5168
|
list_product_areas: listProductAreas,
|
|
4967
5169
|
get_area_graph: getAreaGraph,
|
|
4968
5170
|
create_area: createArea,
|