@unified-product-graph/core 0.9.5 → 0.9.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/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.9.6] - 2026-06-10
11
+
12
+ **Canonical shared-entity registry + drift detection (canonical-registry initiative, Phases 2–3).** The core of the registry: a portfolio can now define a shared entity ONCE and have every product's local copy link to it as a canonical instance, with drift surfaced automatically. Decision: `2026-06-10-canonical-shared-entity-registry.md`.
13
+
14
+ ### Added
15
+ - **`instance_of` cross-edge** (9th member of `UPG_CROSS_EDGE_TYPES`): a directed *product entity → canonical (registry) entity* relationship — "this product node is an instance of that shared, authoritative node." Endpoints must share a type (a `persona` instance_of a `persona`); the target is addressed as `registry/{node_id}`. Distinct from the symmetric `shares_*` peer edges and coexists with them (canonical-to-instance vs peer-equivalence; neither deprecates the other). Unlocks rollup ("every Developer instance and its per-surface jobs"), cross-instance diff, and drift detection.
16
+ - **`registry` section on the portfolio document** (`UPGPortfolioDocument.registry`, shape `UPGRegistry`): the shared-vocabulary tier that sits above products. Canonical entities are normal `UPGBaseNode`s (no new type, no flag — canonical-ness is conferred by living in the registry). Optional and additive: portfolios without a registry stay valid and byte-identical; an empty registry is omitted on serialise. `REGISTRY_PRODUCT_ID = 'registry'` is the reserved pseudo product-id used to qualify registry references; product creation rejects it.
17
+ - (SDK) `UPGPortfolioStore` registry accessors: `getRegistry` / `ensureRegistry` / `addRegistryNode` / `getRegistryNode` / `listRegistryNodes(type?)` / `removeRegistryNode`. The canonical serialiser round-trips the registry section (write + normalise).
18
+ - (MCP server) three local registry tools: **`define_canonical_entity`** (create a canonical node in the registry), **`register_instance`** (create an `instance_of` edge from a product node to a canonical, enforcing the same-type constraint and the `registry/` target), and **`list_registry`** (list canonical entities, optionally with their instances). Tool surface 110 → 113. Local-only (portfolio.upg is a workspace concept; no cloud analogue).
19
+ - (MCP server) **registry drift detection in `portfolio_validate`** (Phase 3): flags an `instance_of` edge whose target is missing from the registry, whose endpoints disagree on type, or whose instance title diverges from its canonical (off-canon rename). The payoff of the registry — a renamed-off-canon instance is now *detectable*, not silent.
20
+
21
+ ### Changed
22
+ - `create_cross_product_edge` and `batch_create_cross_product_edges` reject `instance_of`, directing callers to `register_instance` (single enforcement path for the registry's same-type + `registry/` target rules). `list_cross_edge_types` now reports nine types.
23
+
24
+ ### Notes
25
+ - Phase 4 (`primitive` / `standard` type for foundational shared tech) remains deferred pending a coverage pass against existing `dependency` / `capability`; it must not block the registry core. No entity / domain / edge-catalogue count change (cross-edges are a separate union from `UPG_EDGE_CATALOG`).
26
+
27
+ ---
28
+
10
29
  ## [0.9.5] - 2026-06-10
11
30
 
12
31
  **Persona `audience_role` (canonical-registry initiative, Phase 1).** A small, independent property change — the first cut of the canonical shared-entity registry initiative. Decision: `2026-06-10-canonical-shared-entity-registry.md`.
package/dist/index.cjs CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  PRODUCT_DELIVERY_PLAYBOOK: () => PRODUCT_DELIVERY_PLAYBOOK,
36
36
  PROPERTY_SCALE_MAP: () => PROPERTY_SCALE_MAP,
37
37
  REFLECT_MODES: () => REFLECT_MODES,
38
+ REGISTRY_PRODUCT_ID: () => REGISTRY_PRODUCT_ID,
38
39
  STRATEGY_OUTCOMES_PLAYBOOK: () => STRATEGY_OUTCOMES_PLAYBOOK,
39
40
  UPG_ACTIVE_TYPES: () => UPG_ACTIVE_TYPES,
40
41
  UPG_ANTI_PATTERNS: () => UPG_ANTI_PATTERNS,
@@ -2968,8 +2969,10 @@ var UPG_CROSS_EDGE_TYPES = [
2968
2969
  "cannibalises",
2969
2970
  "succeeds",
2970
2971
  "hosts",
2971
- "contributes_to"
2972
+ "contributes_to",
2973
+ "instance_of"
2972
2974
  ];
2975
+ var REGISTRY_PRODUCT_ID = "registry";
2973
2976
 
2974
2977
  // src/registry/entity-type-resolution.ts
2975
2978
  var TYPES = getTypes();
@@ -26105,7 +26108,16 @@ function portfolioBody(doc) {
26105
26108
  product_areas: [...doc.product_areas ?? []].sort((a, b) => byCodeUnit(a.id ?? "", b.id ?? "")).map((a) => orderedObject(a, ["id", "title", "description", "parent_area_id", "strategic_priority", "products"], { forceKeys: ["id", "title"] })),
26106
26109
  portfolios: [...doc.portfolios ?? []].sort((a, b) => byCodeUnit(a.id ?? "", b.id ?? "")).map((p) => orderedObject(p, ["id", "title", "description", "parent_portfolio_id", "hierarchy_model", "products"], { forceKeys: ["id", "title"] })),
26107
26110
  products,
26108
- cross_edges: sortEdges(doc.cross_edges ?? []).map(canonicalCrossEdge)
26111
+ cross_edges: sortEdges(doc.cross_edges ?? []).map(canonicalCrossEdge),
26112
+ // Registry tier (shared vocabulary). Emitted only when non-empty so existing
26113
+ // portfolio files without a registry stay byte-identical. Canonical entities
26114
+ // are normal nodes, serialised with the same node/edge canonical rules.
26115
+ ...doc.registry && doc.registry.nodes.length > 0 ? {
26116
+ registry: {
26117
+ nodes: sortNodes(doc.registry.nodes).map(canonicalNode),
26118
+ ...doc.registry.edges && doc.registry.edges.length > 0 ? { edges: sortEdges(doc.registry.edges).map(canonicalEdge) } : {}
26119
+ }
26120
+ } : {}
26109
26121
  };
26110
26122
  }
26111
26123
  function computeBodyChecksum(doc) {
@@ -26205,6 +26217,13 @@ function normalizeDocument(obj) {
26205
26217
  })),
26206
26218
  cross_edges: raw.cross_edges ?? []
26207
26219
  };
26220
+ const rawRegistry = raw.registry;
26221
+ if (rawRegistry && Array.isArray(rawRegistry.nodes)) {
26222
+ out2.registry = {
26223
+ nodes: rawRegistry.nodes.map((n) => repairNodeDrift(n)),
26224
+ ...Array.isArray(rawRegistry.edges) ? { edges: rawRegistry.edges } : {}
26225
+ };
26226
+ }
26208
26227
  return out2;
26209
26228
  }
26210
26229
  const out = {
@@ -26227,7 +26246,7 @@ function isCanonical(text) {
26227
26246
  }
26228
26247
 
26229
26248
  // src/index.ts
26230
- var UPG_VERSION = "0.9.5";
26249
+ var UPG_VERSION = "0.9.6";
26231
26250
  var UPG_FORMAT_VERSION = "0.4.0";
26232
26251
  var MARKDOWN_FORMAT_VERSION = "0.1";
26233
26252
  var UPG_TYPES = getTypes();
@@ -26307,6 +26326,7 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
26307
26326
  PRODUCT_DELIVERY_PLAYBOOK,
26308
26327
  PROPERTY_SCALE_MAP,
26309
26328
  REFLECT_MODES,
26329
+ REGISTRY_PRODUCT_ID,
26310
26330
  STRATEGY_OUTCOMES_PLAYBOOK,
26311
26331
  UPG_ACTIVE_TYPES,
26312
26332
  UPG_ANTI_PATTERNS,