@unified-product-graph/cloud-server 0.8.5 → 0.8.7

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/CHANGELOG.md CHANGED
@@ -4,6 +4,10 @@ All notable changes to `@unified-product-graph/cloud-server` are documented in t
4
4
 
5
5
  This package co-versions with `@unified-product-graph/core` and `@unified-product-graph/mcp-server`. One version line covers the spec and both reference implementations.
6
6
 
7
+ ## 0.8.7 · 2026-06-03 · Consolidation + QA train
8
+
9
+ Co-version with the `@unified-product-graph/*` 0.8.7 release train (framework-surface consolidation + `scoring_lens`, CLI hardening, cross-surface QA, led by `core`/`cli`/`mcp-server`; folds the 0.8.6 train). No cloud-server surface change; co-versioned for a clean install matrix.
10
+
7
11
  ## 0.8.5 · 2026-06-02 · Field-report fast-follow
8
12
 
9
13
  Co-version with the `@unified-product-graph/*` 0.8.5 fast-follow (skill_audit source resolution + CLI/docs consistency + npx-cache fix, led by `cli`/`mcp-server`). No cloud-server surface change; co-versioned for a clean install matrix.
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
- [id, productId, edge.source, edge.target, edge.type]
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
- return { id: row.id, source: row.source, target: row.target, type: row.type };
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 = UPG_FRAMEWORKS_BY_ID[frameworkId];
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 = UPG_FRAMEWORKS_BY_ID[id];
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,