@unified-product-graph/core 0.9.0 → 0.9.2

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,44 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ---
9
9
 
10
- ## [0.9.0] - Unreleased
10
+ ## [0.9.2] - 2026-06-09
11
+
12
+ **Journey-model disambiguation (UPG-663).** The four Experience-Design journey types (`user_journey`, `journey_phase`, `journey_step`, `journey_action`) had a coherent concept but broken wiring: there was no deterministic answer to "what are the steps of this journey?". A `journey_phase` is now a temporal band that **spans** steps, not a container that **owns** them, mirroring the marketing precedent `customer_journey_stage_spans_journey_step`. Steps stay owned by `user_journey` (the stable 0.1.0 spine), so each step has exactly one containment parent and the journey renders one canonical step list. Additive plus one edge rename (dual-read via `UPG_EDGE_MIGRATIONS['0.9.2']`). Decision: `2026-06-09-journey-model-disambiguation.md`.
13
+
14
+ ### Added
15
+ - `journey_action_surfaces_need` and `journey_action_realised_by_feature` edges: `journey_action` previously had zero outbound edges despite carrying `pain_score` / `opportunity_score` "to drive opportunity discovery". Opportunity discovery now routes through `need` (which reaches `opportunity` via `opportunity_addresses_need`), mirroring `journey_step_reveals_need` one level deeper.
16
+ - `JourneyStepProperties.step_order` and `JourneyActionProperties.action_order`: scalar ordering, completing a single `*_order` convention across phase / step / action (matching the existing `phase_order` and `customer_journey_stage.stage_order`). `journey_step_precedes_journey_step` remains the explicit-chain option for branching journeys.
17
+ - `journey-phases-without-canonical-steps` curated anti-pattern (high severity): fires when phases span steps but no journey owns them via `user_journey_contains_journey_step`, so `validate_graph` surfaces a journey with no canonical step spine.
18
+
19
+ ### Changed
20
+ - **`journey_phase_has_step` → `journey_phase_spans_journey_step`** (paired, via `UPG_EDGE_MIGRATIONS['0.9.2']`): the phase-to-step edge becomes non-owning (verb `has_step` to `spans`, classification `hierarchy` to `cross-domain`). `journey_phase` is removed as a containment parent of `journey_step` in the hierarchy; `user_journey` keeps `journey_step` (owned spine) and `journey_phase` (band overlay).
21
+ - `properties/domains/ux-design.ts` JSDoc reconciled to the actual edge names (removed six phantom edges that the spec documented but never shipped).
22
+ - `presentation/labels.ts` alt-label collisions removed: `user_journey` drops `"customer journey"` (collided with `customer_journey_stage`); `journey_step` drops `"touchpoint"` and `"journey phase"` (distinct entity surfaces).
23
+ - `intelligence/domain-guides.ts` UX creation sequence reordered so `journey_step` precedes its child `journey_action`.
24
+
25
+ ### Notes
26
+ - `journey_phase` and `journey_action` remain `proposed` (the v0.2 Experience-Design extension over the stable 0.1.0 journey/step spine); not promoted to `stable` in this pass.
27
+
28
+ ---
29
+
30
+ ## [0.9.1] - 2026-06-09
31
+
32
+ **Cross-product read layer + an OKR-alignment edge + a workspace-resolution fix.** One core spec addition (the `contributes_to` cross-product edge), the read counterpart to 0.8.16's portfolio write tier (server tooling), and a `switch_product` bug fix. From batch-3 dogfooding (portfolio strategy reasoning across the full multi-product workspace). All additive; existing files stay valid.
33
+
34
+ ### Added
35
+ - `contributes_to` cross-product edge type in `UPG_CROSS_EDGE_TYPES` (union 7 → 8): the directional OKR rollup / alignment edge, subordinate → superior (product `objective` → company `objective`, product `key_result` → company `key_result`, product `outcome` → company `outcome`). Unlike the symmetric `shares_*` peer edges it is hierarchical, so a portfolio can answer "which company objective is this product serving?" and "which company KRs have no product driving them?". Additive; existing files stay valid. Decision: `2026-06-09-cross-product-okr-alignment-edge.md` (batch-3 #14).
36
+ - (MCP server, not core spec) `portfolio_query` + `portfolio_digest`: read node content and health digests ACROSS products in one call — the multi-product `query` / `get_graph_digest` — without `switch_product`. The read counterpart to the 0.8.16 portfolio write tier. Read-only and parallel-safe: the active product is read from the live store, every other product via a transient read-only load. Tool surface 106 → 108 (batch-3 #13).
37
+ - (SDK) `UPGFileStore.loadReadOnly()`: parse + normalise + index a `.upg` without starting a file watcher or taking a lock, for transient cross-product reads.
38
+
39
+ ### Fixed
40
+ - (MCP server) `switch_product` bare-name resolution now anchors to the workspace `.upg/` directory first and requires a regular file, so a bare product name (e.g. `sanity`) that collides with a sibling source directory (`sanity/`) loads `.upg/sanity.upg` instead of throwing `EISDIR` (batch-3 #12).
41
+
42
+ ### Deferred
43
+ - Cross-product **write** (writing nodes/edges into non-active products without `switch_product`) is tracked as batch-4 — the write counterpart to this read layer. Strategic linkage across products is already expressible today via `contributes_to` cross-edges (no switching required); only content propagation into product internals is outstanding.
44
+
45
+ ---
46
+
47
+ ## [0.9.0] - 2026-06-09
11
48
 
12
49
  **Breaking type rename (with migration) + framework-scoped scoring completion.** Two coordinated spec changes ship in this cut, both additive + deprecate (existing `.upg` files dual-read via `UPG_MIGRATIONS['0.9.0']`): the `theme` → `roadmap_theme` rename (UPG-660, completing the N6 four-way theme-family disambiguation) and the framework-scoped scoring migration for `solution` + `opportunity` (UPG-644, completing UPG-595). Decisions: `2026-06-09-theme-roadmap-theme-rename.md`, `2026-06-05-framework-scoped-scoring-completion.md`.
13
50
 
@@ -27,7 +64,7 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
27
64
  - `theme` (`ent_080`): retained in the type union and resolvable via the migration map for the 0.9.x dual-read window; `replacement: 'roadmap_theme'`. Loaders retarget old nodes and the four old edge keys automatically.
28
65
  - `solution`: `reach`, `impact`, `confidence`, `effort`, `rice_score` (apply `rice-scoring`). `timeline` is unaffected (intrinsic).
29
66
  - `opportunity`: `reach`, `frequency`, `pain`, `opportunity_score` (apply `opportunity-sizing`).
30
- - Native scoring fields are signalled deprecated via `@deprecated` JSDoc (flowing into the generated `UPG_PROPERTY_SCHEMA` descriptions) and stay dual-read for 0.9.x. Removal plus the `migrate_properties` lift onto `framework_exercise` includes-edges lands in 0.9.1, **after** consumers read from the edge (never before: a premature data move silently flattens sort/quadrant surfaces to zero).
67
+ - Native scoring fields are signalled deprecated via `@deprecated` JSDoc (flowing into the generated `UPG_PROPERTY_SCHEMA` descriptions) and stay dual-read for 0.9.x. Removal plus the `migrate_properties` lift onto `framework_exercise` includes-edges lands in a later 0.9.x cut (originally earmarked 0.9.1; 0.9.1 became the batch-3 read layer), **after** consumers read from the edge (never before: a premature data move silently flattens sort/quadrant surfaces to zero).
31
68
  - Follow-up (0.9.x): a machine-readable `PropertyDefinition.deprecated` field so the property-registry generator emits it and the public GraphQL generator can mark removed scalars `@deprecated` rather than dropping them silently.
32
69
 
33
70
  ---
package/dist/index.cjs CHANGED
@@ -1484,8 +1484,24 @@ var UPG_EDGE_CATALOG = {
1484
1484
  journey_step_realised_by_feature: { forward_verb: "realised_by", reverse_verb: "realises", classification: "cross-domain", source_type: "journey_step", target_type: "feature" },
1485
1485
  opportunity_improves_user_journey: { forward_verb: "improves", reverse_verb: "improved_by", classification: "cross-domain", source_type: "opportunity", target_type: "user_journey" },
1486
1486
  user_journey_passes_through_journey_phase: { forward_verb: "passes_through", reverse_verb: "is_phase_of", classification: "hierarchy", source_type: "user_journey", target_type: "journey_phase" },
1487
- journey_phase_has_step: { forward_verb: "has_step", reverse_verb: "is_step_in", classification: "hierarchy", source_type: "journey_phase", target_type: "journey_step" },
1487
+ // (since v0.9.2, UPG-663) A journey_phase is a temporal BAND over the
1488
+ // journey's single step timeline, not a container that owns steps. Steps
1489
+ // belong to the journey via `user_journey_contains_journey_step` (the stable
1490
+ // 0.1.0 spine); a phase merely SPANS a range of them. Mirrors the marketing
1491
+ // precedent `customer_journey_stage_spans_journey_step`. Renamed from the
1492
+ // owning `journey_phase_has_step` (hierarchy) so a step has exactly one
1493
+ // containment parent. See UPG_EDGE_MIGRATIONS['0.9.2'].
1494
+ journey_phase_spans_journey_step: { forward_verb: "spans", reverse_verb: "spanned_by", classification: "cross-domain", source_type: "journey_phase", target_type: "journey_step" },
1488
1495
  journey_step_has_action: { forward_verb: "has_action", reverse_verb: "is_action_in", classification: "hierarchy", source_type: "journey_step", target_type: "journey_action" },
1496
+ // (since v0.9.2, UPG-663) journey_action outbound edges. Fixes the
1497
+ // discovery dead-end (D2). The finest blueprint layer carries pain_score /
1498
+ // opportunity_score "to drive opportunity discovery" but previously had zero
1499
+ // outbound edges. Opportunity discovery routes through `need` (mirroring
1500
+ // `journey_step_reveals_need`), which already reaches `opportunity` via the
1501
+ // user chain; the action does not link an opportunity directly. The feature
1502
+ // edge mirrors `journey_step_realised_by_feature` one level deeper.
1503
+ journey_action_surfaces_need: { forward_verb: "surfaces", reverse_verb: "surfaced_in", classification: "cross-domain", source_type: "journey_action", target_type: "need" },
1504
+ journey_action_realised_by_feature: { forward_verb: "realised_by", reverse_verb: "realises", classification: "cross-domain", source_type: "journey_action", target_type: "feature" },
1489
1505
  // 2.5 UI System Domain
1490
1506
  product_systematised_in_design_system: { forward_verb: "systematised_in", reverse_verb: "systematises", classification: "hierarchy", source_type: "product", target_type: "design_system" },
1491
1507
  product_built_with_design_component: { forward_verb: "built_with", reverse_verb: "built_for", classification: "hierarchy", source_type: "product", target_type: "design_component" },
@@ -2951,7 +2967,8 @@ var UPG_CROSS_EDGE_TYPES = [
2951
2967
  "depends_on_product",
2952
2968
  "cannibalises",
2953
2969
  "succeeds",
2954
- "hosts"
2970
+ "hosts",
2971
+ "contributes_to"
2955
2972
  ];
2956
2973
 
2957
2974
  // src/registry/entity-type-resolution.ts
@@ -3242,8 +3259,13 @@ var UPG_VALID_CHILDREN = {
3242
3259
  // parallel to `observation: ['quote']`. Research-synthesis canon.
3243
3260
  // Pairs with `insight_evidenced_by_quote` in the edge catalog.
3244
3261
  // ── Design hierarchy ────────────────────────────────────────────────────────
3262
+ // (UPG-663, v0.9.2) A user_journey owns its steps (the stable 0.1.0 spine)
3263
+ // and carries phases as a non-owning band overlay. A journey_phase is NO
3264
+ // LONGER a containment parent of journey_step: it SPANS steps via the
3265
+ // `journey_phase_spans_journey_step` edge (mirroring the marketing
3266
+ // `customer_journey_stage_spans_journey_step` precedent), so each step has
3267
+ // exactly one containment parent and the journey has one canonical step list.
3245
3268
  user_journey: ["journey_step", "journey_phase"],
3246
- journey_phase: ["journey_step"],
3247
3269
  journey_step: ["journey_action"],
3248
3270
  // v0.5.8 (UPG-528 Part 2d): insights own their evidencing quotes;
3249
3271
  // parallel to `observation: ['quote']`. Research-synthesis canon
@@ -7796,6 +7818,20 @@ function getSplitMigrations(fromVersion, toVersion) {
7796
7818
  return result;
7797
7819
  }
7798
7820
  var UPG_EDGE_MIGRATIONS = {
7821
+ "0.9.2": [
7822
+ // (since v0.9.2, UPG-663) Journey-model disambiguation. A journey_phase
7823
+ // is a temporal BAND over the journey's single step timeline, not a
7824
+ // container that owns steps. The owning hierarchy edge
7825
+ // `journey_phase_has_step` is renamed to the non-owning
7826
+ // `journey_phase_spans_journey_step` (mirroring the marketing precedent
7827
+ // `customer_journey_stage_spans_journey_step`). Steps stay owned by the
7828
+ // journey via `user_journey_contains_journey_step` (the stable 0.1.0
7829
+ // spine), so each step keeps a single containment parent and the journey
7830
+ // renders one canonical step list. Endpoints are unchanged
7831
+ // (journey_phase → journey_step); only the edge key and its grammar
7832
+ // (verb has_step → spans, classification hierarchy → cross-domain) change.
7833
+ { kind: "rename", from: "journey_phase_has_step", to: "journey_phase_spans_journey_step", requires_source_type: "journey_phase", requires_target_type: "journey_step", reason: "Journey-model disambiguation (UPG-663): a phase spans steps, it does not own them. The phase to step edge becomes non-owning (verb has_step to spans), so the step keeps a single containment parent (the journey) and the journey has one canonical step list." }
7834
+ ],
7799
7835
  "0.9.0": [
7800
7836
  // (since v0.9.0, UPG-660) theme → roadmap_theme. The four canonical edges that
7801
7837
  // touch the roadmap theme are renamed to the roadmap_theme form. Endpoint guards
@@ -10294,6 +10330,7 @@ var UPG_PROPERTY_SCHEMA = {
10294
10330
  },
10295
10331
  // JourneyActionProperties: Discrete action at a journey step, classified by service layer.
10296
10332
  journey_action: {
10333
+ action_order: { type: "number", description: "Display order of this action within its step (0-indexed). The scalar ordering convention shared with `journey_phase.phase_order` and `journey_step.step_order` (UPG-663). Orders the service-blueprint rows within a single moment." },
10297
10334
  layer: { type: "string", enum: ["user", "frontstage", "backstage", "support"], description: "Service layer" },
10298
10335
  action_description: { type: "string", description: "Plain-language description. Primary content of the action." },
10299
10336
  channel: { type: "string", enum: ["in-app", "email", "web", "mobile", "phone", "in-person", "sms", "social", "other"], description: "Channel or surface. Keeps service-blueprint columns consistent across the journey." },
@@ -10325,7 +10362,7 @@ var UPG_PROPERTY_SCHEMA = {
10325
10362
  system: { type: "string", description: "Performing system or service" },
10326
10363
  notes: { type: "string", description: "Free-text notes, observations, or follow-up questions" }
10327
10364
  },
10328
- // JourneyPhaseProperties: Phase within a user journey. Groups journey steps into stages.
10365
+ // JourneyPhaseProperties: Phase within a user journey. A temporal BAND over the journey's step
10329
10366
  journey_phase: {
10330
10367
  phase_order: { type: "number", description: "Display order within the journey (0-indexed)" },
10331
10368
  label: { type: "string", description: 'Short human-readable name. @example "Discovery", "Onboarding", "Activation"' },
@@ -10336,8 +10373,9 @@ var UPG_PROPERTY_SCHEMA = {
10336
10373
  key_questions: { type: "string[]", description: "Open questions the user asks themselves. Fuel for design and content priorities." },
10337
10374
  timeframe: { type: "string", description: 'Typical time window. @example "first 30 seconds", "days 1\u20137", "onboarding week"' }
10338
10375
  },
10339
- // JourneyStepProperties: Single step within a user journey.
10376
+ // JourneyStepProperties: Single step within a user journey. A user-moment on the journey's single
10340
10377
  journey_step: {
10378
+ step_order: { type: "number", description: "Display order within the journey's step timeline (0-indexed). The scalar ordering convention shared with `journey_phase.phase_order` and `journey_action.action_order` (UPG-663). For branching journeys, the explicit `journey_step_precedes_journey_step` edge captures the chain." },
10341
10379
  touchpoint: { type: "string", description: "Interaction touchpoint" },
10342
10380
  channel: { type: "string", description: 'Channel (e.g. "web", "email", "in-store")' },
10343
10381
  emotion_score: {
@@ -18260,8 +18298,13 @@ var STANDARD_LABELS = {
18260
18298
  interview_guide: { alt_labels: ["discussion guide", "interview script", "moderator guide"] },
18261
18299
  survey_response: { alt_labels: ["survey answer", "questionnaire response"] },
18262
18300
  // Design layer
18263
- user_journey: { alt_labels: ["journey map", "customer journey", "experience map", "journey"] },
18264
- journey_step: { alt_labels: ["touchpoint", "journey stage", "journey phase"] },
18301
+ // (UPG-663) alt_labels must not collide with other entity types' canonical
18302
+ // names or alt_labels, or the string->type resolver is ambiguous.
18303
+ // Dropped 'customer journey' (collides with customer_journey_stage),
18304
+ // 'touchpoint' and 'journey phase' (both distinct entity surfaces: a
18305
+ // touchpoint is the journey_step.touchpoint property, a phase is journey_phase).
18306
+ user_journey: { alt_labels: ["journey map", "experience map", "journey"] },
18307
+ journey_step: { alt_labels: ["journey moment", "journey stage"] },
18265
18308
  design_component: { alt_labels: ["component", "ui component", "design element"] },
18266
18309
  design_token: { alt_labels: ["token", "style token", "css variable"] },
18267
18310
  wireframe: { alt_labels: ["wireflow", "lo-fi mockup", "skeleton"] },
@@ -20326,7 +20369,11 @@ var PRODUCT_SPEC_GUIDE = {
20326
20369
  var UX_DESIGN_GUIDE = {
20327
20370
  domain_id: "ux_design",
20328
20371
  anchor_entity: "user_journey",
20329
- creation_sequence: ["user_journey", "journey_phase", "journey_action", "journey_step", "screen", "screen_state", "user_flow", "wireframe", "prototype", "design_question", "design_concept"],
20372
+ // (UPG-663) creation order follows containment: a journey_action is a child
20373
+ // of a journey_step, so the step must precede the action. The journey is the
20374
+ // anchor; phases are the band overlay; steps are the timeline; actions
20375
+ // decompose a step into service-blueprint rows.
20376
+ creation_sequence: ["user_journey", "journey_phase", "journey_step", "journey_action", "screen", "screen_state", "user_flow", "wireframe", "prototype", "design_question", "design_concept"],
20330
20377
  patterns: [
20331
20378
  {
20332
20379
  name: "Journey to Screen Flow",
@@ -23623,6 +23670,40 @@ var UPG_ANTI_PATTERNS = [
23623
23670
  stages: ["concept", "validation", "build", "beta", "launch", "growth", "mature"],
23624
23671
  severity: "medium",
23625
23672
  source: { kind: "fundamental" }
23673
+ },
23674
+ // ── Experience-design layer ─────────────────────────────────────────────
23675
+ {
23676
+ id: "journey-phases-without-canonical-steps",
23677
+ name: "Journey phases without a step spine",
23678
+ description: 'The graph has journey phases spanning steps (`journey_phase_spans_journey_step`), but no journey owns its steps via `user_journey_contains_journey_step`. A phase is a band over a step timeline, not a container. When the timeline itself is missing there is no canonical answer to "what are the steps of this journey?". The phase overlay points at steps the journey does not own.',
23679
+ structured_condition: {
23680
+ operator: "and",
23681
+ checks: [
23682
+ {
23683
+ check: {
23684
+ type: "relationship",
23685
+ source_type: "journey_phase",
23686
+ edge_type: "journey_phase_spans_journey_step",
23687
+ target_type: "journey_step",
23688
+ comparison: "exists"
23689
+ }
23690
+ },
23691
+ {
23692
+ check: {
23693
+ type: "relationship",
23694
+ source_type: "user_journey",
23695
+ edge_type: "user_journey_contains_journey_step",
23696
+ target_type: "journey_step",
23697
+ comparison: "not_exists"
23698
+ }
23699
+ }
23700
+ ]
23701
+ },
23702
+ why_it_matters: "Steps owned by no journey render a different step list per consumer: the phase overlay sees them, a journey-direct walk does not. The journey has no deterministic step spine to traverse, score, or map to screens.",
23703
+ remediation: "Own every step under its journey with `user_journey_contains_journey_step`, then let phases span ranges of that single timeline via `journey_phase_spans_journey_step`. The phase is a non-owning band overlay, not the step container.",
23704
+ stages: ["concept", "validation", "build", "beta", "launch", "growth"],
23705
+ severity: "high",
23706
+ source: { kind: "fundamental" }
23626
23707
  }
23627
23708
  ];
23628
23709
  function getAntiPatternById(id) {
@@ -26145,7 +26226,7 @@ function isCanonical(text) {
26145
26226
  }
26146
26227
 
26147
26228
  // src/index.ts
26148
- var UPG_VERSION = "0.9.0";
26229
+ var UPG_VERSION = "0.9.2";
26149
26230
  var UPG_FORMAT_VERSION = "0.4.0";
26150
26231
  var MARKDOWN_FORMAT_VERSION = "0.1";
26151
26232
  var UPG_TYPES = getTypes();