@unified-product-graph/core 0.9.4 → 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,7 +7,34 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ---
9
9
 
10
- ## [0.9.4] - 2026-06-09
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
+
29
+ ## [0.9.5] - 2026-06-10
30
+
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`.
32
+
33
+ ### Added
34
+ - `PersonaProperties.audience_role`: optional enum `'buyer' | 'user' | 'champion' | 'influencer' | 'partner'` — the decision-making-unit (buying-committee) split. A portfolio must separate the economic **buyer** (who signs) from the practitioner **user** (who uses); they are distinct personas with distinct jobs, and flattening them loses the most important B2B distinction. Closed set so roles compare across products. Backward-compatible: optional, existing personas unaffected. No entity / edge / tool count change.
35
+
36
+ ### Notes
37
+ - This unblocks the registry initiative's stopgap migration (each canonical persona gets tagged with `audience_role`). The registry core — an `instance_of` cross-edge + portfolio-tier canonical hosting + drift detection — is designed (Phases 2–4) and sequenced for later cuts.
11
38
 
12
39
  **Cross-product structure clone + pre-commit preview + target-profile coverage (batch-4, part 2).** No core spec change — one new local MCP tool plus two opt-in parameters. Decision: `2026-06-09-cross-product-structure-clone.md`.
13
40
 
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();
@@ -10877,7 +10880,8 @@ var UPG_PROPERTY_SCHEMA = {
10877
10880
  experience_level: { type: "string", enum: ["beginner", "intermediate", "advanced", "varies"], description: "How experienced this persona is in their domain" },
10878
10881
  motivation: { type: "string", description: 'Primary motivation or driving need @example "Making confident, evidence-based decisions"' },
10879
10882
  tech_comfort: { type: "string", enum: ["low", "medium", "high", "expert", "other"], description: "Tech comfort. Closed set so personas across products compare on the same axis. Free-text colour belongs in `context` or `motivation`." },
10880
- domain_expertise: { type: "string", description: 'Industry or domain knowledge this persona brings @example "10+ years SaaS experience", "New to healthcare IT"' }
10883
+ domain_expertise: { type: "string", description: 'Industry or domain knowledge this persona brings @example "10+ years SaaS experience", "New to healthcare IT"' },
10884
+ audience_role: { type: "string", enum: ["buyer", "user", "champion", "influencer", "partner"], description: 'Role in the buying / adoption decision (the decision-making-unit split): who signs (buyer) vs who uses (user) vs who advocates internally (champion) vs who shapes the choice (influencer) vs who delivers / implements (partner). A portfolio must separate the economic buyer from the practitioner user; they are distinct personas with distinct jobs. Closed set so roles compare across products. @example "buyer"' }
10881
10885
  },
10882
10886
  // PipelineSalesProperties: Sales pipeline.
10883
10887
  pipeline_sales: {
@@ -26104,7 +26108,16 @@ function portfolioBody(doc) {
26104
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"] })),
26105
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"] })),
26106
26110
  products,
26107
- 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
+ } : {}
26108
26121
  };
26109
26122
  }
26110
26123
  function computeBodyChecksum(doc) {
@@ -26204,6 +26217,13 @@ function normalizeDocument(obj) {
26204
26217
  })),
26205
26218
  cross_edges: raw.cross_edges ?? []
26206
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
+ }
26207
26227
  return out2;
26208
26228
  }
26209
26229
  const out = {
@@ -26226,7 +26246,7 @@ function isCanonical(text) {
26226
26246
  }
26227
26247
 
26228
26248
  // src/index.ts
26229
- var UPG_VERSION = "0.9.4";
26249
+ var UPG_VERSION = "0.9.6";
26230
26250
  var UPG_FORMAT_VERSION = "0.4.0";
26231
26251
  var MARKDOWN_FORMAT_VERSION = "0.1";
26232
26252
  var UPG_TYPES = getTypes();
@@ -26306,6 +26326,7 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
26306
26326
  PRODUCT_DELIVERY_PLAYBOOK,
26307
26327
  PROPERTY_SCALE_MAP,
26308
26328
  REFLECT_MODES,
26329
+ REGISTRY_PRODUCT_ID,
26309
26330
  STRATEGY_OUTCOMES_PLAYBOOK,
26310
26331
  UPG_ACTIVE_TYPES,
26311
26332
  UPG_ANTI_PATTERNS,