@unified-product-graph/core 0.17.1 → 0.17.3

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.cjs CHANGED
@@ -68,6 +68,8 @@ __export(index_exports, {
68
68
  UPG_CONTAINMENT_FREE_TYPES: () => UPG_CONTAINMENT_FREE_TYPES,
69
69
  UPG_COUNT_BENCHMARKS: () => UPG_COUNT_BENCHMARKS,
70
70
  UPG_CROSS_EDGE_TYPES: () => UPG_CROSS_EDGE_TYPES,
71
+ UPG_CROSS_ELIGIBLE_CATALOG_EDGE_TYPES: () => UPG_CROSS_ELIGIBLE_CATALOG_EDGE_TYPES,
72
+ UPG_CROSS_ONLY_EDGE_TYPES: () => UPG_CROSS_ONLY_EDGE_TYPES,
71
73
  UPG_DEPRECATED_TYPES: () => UPG_DEPRECATED_TYPES,
72
74
  UPG_DOMAINS: () => UPG_DOMAINS,
73
75
  UPG_DOMAIN_ACTIVATION: () => UPG_DOMAIN_ACTIVATION,
@@ -198,6 +200,7 @@ __export(index_exports, {
198
200
  isCanonical: () => isCanonical,
199
201
  isCanonicalProductStage: () => isCanonicalProductStage,
200
202
  isContainmentFreeType: () => isContainmentFreeType,
203
+ isCrossProductEligible: () => isCrossProductEligible,
201
204
  isDeprecatedType: () => isDeprecatedType,
202
205
  isDomainGuideStep: () => isDomainGuideStep,
203
206
  isEntitySequenceStep: () => isEntitySequenceStep,
@@ -1273,7 +1276,7 @@ var UPG_EDGE_CATALOG = {
1273
1276
  persona_aspires_to_desired_outcome: { forward_verb: "aspires_to", reverse_verb: "aspirational_for", classification: "hierarchy", source_type: "persona", target_type: "desired_outcome" },
1274
1277
  // Delegation (0.11.6): a persona delegates work to another. The common case is human -> agent
1275
1278
  // (Content Ops Lead -> Content Agent), but it also expresses human -> human (editor -> reviewer).
1276
- persona_delegates_to_persona: { forward_verb: "delegates_to", reverse_verb: "delegated_by", classification: "semantic", source_type: "persona", target_type: "persona" },
1279
+ persona_delegates_to_persona: { forward_verb: "delegates_to", reverse_verb: "delegated_by", classification: "semantic", source_type: "persona", target_type: "persona", cross_product_eligible: true },
1277
1280
  persona_incurs_switching_cost: { forward_verb: "incurs", reverse_verb: "incurred_by", classification: "hierarchy", source_type: "persona", target_type: "switching_cost" },
1278
1281
  job_surfaces_need: { forward_verb: "surfaces", reverse_verb: "surfaces_from", classification: "causal", source_type: "job", target_type: "need" },
1279
1282
  job_motivates_desired_outcome: { forward_verb: "motivates", reverse_verb: "motivated_by", classification: "causal", source_type: "job", target_type: "desired_outcome" },
@@ -1391,15 +1394,15 @@ var UPG_EDGE_CATALOG = {
1391
1394
  // Dual-registered as a cross-edge (UPG_CROSS_EDGE_TYPES) so it also spans our
1392
1395
  // product graph and a watched competitor-intelligence graph. Distinct from
1393
1396
  // `competitor_feature_inspires_feature` (ideation lineage, not parity).
1394
- feature_rivals_competitor_feature: { forward_verb: "rivals", reverse_verb: "is_rivalled_by", classification: "cross-domain", source_type: "feature", target_type: "competitor_feature", carries_properties: true },
1397
+ feature_rivals_competitor_feature: { forward_verb: "rivals", reverse_verb: "is_rivalled_by", classification: "cross-domain", source_type: "feature", target_type: "competitor_feature", carries_properties: true, cross_product_eligible: true },
1395
1398
  // 0.10.0 (spec issue #41): competitor_signal is a dated competitor move (feature
1396
1399
  // launch / pricing change / acquisition / partnership / market entry) emitted by a
1397
1400
  // competitor and mapped onto our portfolio. `emits` is within the watched graph;
1398
1401
  // `maps_to_feature` / `surfaces_opportunity` cross from the watched signal into our
1399
1402
  // product graph (dual-registered as cross-edges in UPG_CROSS_EDGE_TYPES).
1400
1403
  competitor_emits_competitor_signal: { forward_verb: "emits", reverse_verb: "emitted_by", classification: "hierarchy", source_type: "competitor", target_type: "competitor_signal" },
1401
- competitor_signal_maps_to_feature: { forward_verb: "maps_to", reverse_verb: "targeted_by_signal", classification: "cross-domain", source_type: "competitor_signal", target_type: "feature" },
1402
- competitor_signal_surfaces_opportunity: { forward_verb: "surfaces", reverse_verb: "prompted_by_signal", classification: "cross-domain", source_type: "competitor_signal", target_type: "opportunity" },
1404
+ competitor_signal_maps_to_feature: { forward_verb: "maps_to", reverse_verb: "targeted_by_signal", classification: "cross-domain", source_type: "competitor_signal", target_type: "feature", cross_product_eligible: true },
1405
+ competitor_signal_surfaces_opportunity: { forward_verb: "surfaces", reverse_verb: "prompted_by_signal", classification: "cross-domain", source_type: "competitor_signal", target_type: "opportunity", cross_product_eligible: true },
1403
1406
  competitive_analysis_analyses_competitor: { forward_verb: "analyses", reverse_verb: "analysed_in", classification: "hierarchy", source_type: "competitive_analysis", target_type: "competitor" },
1404
1407
  competitive_analysis_identifies_market_trend: { forward_verb: "identifies", reverse_verb: "identified_in", classification: "hierarchy", source_type: "competitive_analysis", target_type: "market_trend" },
1405
1408
  competitive_analysis_scopes_market_segment: { forward_verb: "scopes", reverse_verb: "scoped_in", classification: "hierarchy", source_type: "competitive_analysis", target_type: "market_segment" },
@@ -1432,13 +1435,13 @@ var UPG_EDGE_CATALOG = {
1432
1435
  // competitor can be classified directly against a `registry/{classification_value}`
1433
1436
  // canonical, eliminating the per-graph taxonomy node. Within-graph (the catalogue
1434
1437
  // case) here; cross-product against the registry canonical via create_cross_product_edge.
1435
- competitor_classified_as_classification_value: { forward_verb: "classified_as", reverse_verb: "classification_of", classification: "semantic", source_type: "competitor", target_type: "classification_value", carries_properties: true, property_schema: CLASSIFICATION_EDGE_PROPERTY_SCHEMA },
1438
+ competitor_classified_as_classification_value: { forward_verb: "classified_as", reverse_verb: "classification_of", classification: "semantic", source_type: "competitor", target_type: "classification_value", carries_properties: true, property_schema: CLASSIFICATION_EDGE_PROPERTY_SCHEMA, cross_product_eligible: true },
1436
1439
  // 0.10.3: the polymorphic, type-agnostic sibling. ANY node (a feature, a
1437
1440
  // product, a market_segment) classified against a classification_value, so
1438
1441
  // classification is not welded to the competitor type. Also dual-registered as
1439
1442
  // a cross-edge for the registry-canonical case. source_type:'node' (wildcard);
1440
1443
  // listed in UPG_POLYMORPHIC_EDGE_KEYS.
1441
- node_classified_as_classification_value: { forward_verb: "classified_as", reverse_verb: "classification_of", classification: "semantic", source_type: "node", target_type: "classification_value", carries_properties: true, property_schema: CLASSIFICATION_EDGE_PROPERTY_SCHEMA },
1444
+ node_classified_as_classification_value: { forward_verb: "classified_as", reverse_verb: "classification_of", classification: "semantic", source_type: "node", target_type: "classification_value", carries_properties: true, property_schema: CLASSIFICATION_EDGE_PROPERTY_SCHEMA, cross_product_eligible: true },
1442
1445
  persona_anti_fit_for_classification_value: { forward_verb: "is_anti_fit_for", reverse_verb: "should_not_be_picked_by", classification: "semantic", source_type: "persona", target_type: "classification_value" },
1443
1446
  persona_anti_fit_for_product: { forward_verb: "is_anti_fit_for", reverse_verb: "should_not_be_picked_by", classification: "semantic", source_type: "persona", target_type: "product" },
1444
1447
  persona_anti_fit_for_competitor: { forward_verb: "is_anti_fit_for", reverse_verb: "should_not_be_picked_by", classification: "semantic", source_type: "persona", target_type: "competitor" },
@@ -1491,28 +1494,28 @@ var UPG_EDGE_CATALOG = {
1491
1494
  feedback_program_has_beta_program: { forward_verb: "has", reverse_verb: "part_of", classification: "hierarchy", source_type: "feedback_program", target_type: "beta_program" },
1492
1495
  // ── Part 2: Strategic & Execution Spine (Ring 2) ───────────────────────────
1493
1496
  // 2.1 Strategic Domain
1494
- product_pursues_outcome: { forward_verb: "pursues", reverse_verb: "pursued_by", classification: "hierarchy", source_type: "product", target_type: "outcome" },
1495
- product_targets_objective: { forward_verb: "targets", reverse_verb: "targeted_by", classification: "hierarchy", source_type: "product", target_type: "objective" },
1496
- product_guided_by_vision: { forward_verb: "guided_by", reverse_verb: "guides", classification: "hierarchy", source_type: "product", target_type: "vision" },
1497
+ product_pursues_outcome: { forward_verb: "pursues", reverse_verb: "pursued_by", classification: "hierarchy", source_type: "product", target_type: "outcome", cross_product_eligible: true },
1498
+ product_targets_objective: { forward_verb: "targets", reverse_verb: "targeted_by", classification: "hierarchy", source_type: "product", target_type: "objective", cross_product_eligible: true },
1499
+ product_guided_by_vision: { forward_verb: "guided_by", reverse_verb: "guides", classification: "hierarchy", source_type: "product", target_type: "vision", cross_product_eligible: true },
1497
1500
  // product→{mission,strategic_theme,strategic_pillar,initiative,capability,value_stream,assumption}
1498
1501
  // are semantic "how the product relates to the strategic cascade", not containment.
1499
1502
  // The containment chain is product → vision → mission → strategic_pillar → … so these
1500
1503
  // targets are reachable via the vision/mission hierarchy already.
1501
- product_fulfils_mission: { forward_verb: "fulfils", reverse_verb: "fulfilled_by", classification: "semantic", source_type: "product", target_type: "mission" },
1502
- product_organises_around_strategic_theme: { forward_verb: "organises_around", reverse_verb: "organises", classification: "semantic", source_type: "product", target_type: "strategic_theme" },
1503
- product_stands_on_strategic_pillar: { forward_verb: "stands_on", reverse_verb: "supports", classification: "semantic", source_type: "product", target_type: "strategic_pillar" },
1504
- product_invests_in_initiative: { forward_verb: "invests_in", reverse_verb: "investment_of", classification: "semantic", source_type: "product", target_type: "initiative" },
1504
+ product_fulfils_mission: { forward_verb: "fulfils", reverse_verb: "fulfilled_by", classification: "semantic", source_type: "product", target_type: "mission", cross_product_eligible: true },
1505
+ product_organises_around_strategic_theme: { forward_verb: "organises_around", reverse_verb: "organises", classification: "semantic", source_type: "product", target_type: "strategic_theme", cross_product_eligible: true },
1506
+ product_stands_on_strategic_pillar: { forward_verb: "stands_on", reverse_verb: "supports", classification: "semantic", source_type: "product", target_type: "strategic_pillar", cross_product_eligible: true },
1507
+ product_invests_in_initiative: { forward_verb: "invests_in", reverse_verb: "investment_of", classification: "semantic", source_type: "product", target_type: "initiative", cross_product_eligible: true },
1505
1508
  product_develops_capability: { forward_verb: "develops", reverse_verb: "developed_by", classification: "semantic", source_type: "product", target_type: "capability" },
1506
1509
  product_delivers_through_value_stream: { forward_verb: "delivers_through", reverse_verb: "delivers_for", classification: "semantic", source_type: "product", target_type: "value_stream" },
1507
1510
  product_holds_assumption: { forward_verb: "holds", reverse_verb: "held_by", classification: "semantic", source_type: "product", target_type: "assumption" },
1508
- product_measures_with_metric: { forward_verb: "measures_with", reverse_verb: "measures", classification: "hierarchy", source_type: "product", target_type: "metric" },
1509
- outcome_measured_by_metric: { forward_verb: "measured_by", reverse_verb: "measures", classification: "hierarchy", source_type: "outcome", target_type: "metric" },
1511
+ product_measures_with_metric: { forward_verb: "measures_with", reverse_verb: "measures", classification: "hierarchy", source_type: "product", target_type: "metric", cross_product_eligible: true },
1512
+ outcome_measured_by_metric: { forward_verb: "measured_by", reverse_verb: "measures", classification: "hierarchy", source_type: "outcome", target_type: "metric", cross_product_eligible: true },
1510
1513
  // (UPG-677) outcome_tracked_by_metric retired — near-synonym of
1511
1514
  // outcome_measured_by_metric (the kept canonical key). Captain's lean =
1512
1515
  // collapse. See UPG_EDGE_MIGRATIONS['0.9.9'].
1513
- objective_achieved_through_key_result: { forward_verb: "achieved_through", reverse_verb: "achieves", classification: "hierarchy", source_type: "objective", target_type: "key_result" },
1514
- objective_measured_by_metric: { forward_verb: "measured_by", reverse_verb: "measures", classification: "hierarchy", source_type: "objective", target_type: "metric" },
1515
- key_result_quantified_by_metric: { forward_verb: "quantified_by", reverse_verb: "quantifies", classification: "hierarchy", source_type: "key_result", target_type: "metric" },
1516
+ objective_achieved_through_key_result: { forward_verb: "achieved_through", reverse_verb: "achieves", classification: "hierarchy", source_type: "objective", target_type: "key_result", cross_product_eligible: true },
1517
+ objective_measured_by_metric: { forward_verb: "measured_by", reverse_verb: "measures", classification: "hierarchy", source_type: "objective", target_type: "metric", cross_product_eligible: true },
1518
+ key_result_quantified_by_metric: { forward_verb: "quantified_by", reverse_verb: "quantifies", classification: "hierarchy", source_type: "key_result", target_type: "metric", cross_product_eligible: true },
1516
1519
  // (UPG-677) key_result_tracked_by_metric retired — near-synonym of
1517
1520
  // key_result_quantified_by_metric (the kept canonical key). Captain's lean =
1518
1521
  // collapse. See UPG_EDGE_MIGRATIONS['0.9.9'].
@@ -1522,7 +1525,7 @@ var UPG_EDGE_CATALOG = {
1522
1525
  strategic_pillar_enables_capability: { forward_verb: "enables", reverse_verb: "enabled_by", classification: "hierarchy", source_type: "strategic_pillar", target_type: "capability" },
1523
1526
  strategic_pillar_delivers_value_stream: { forward_verb: "delivers", reverse_verb: "delivered_by", classification: "hierarchy", source_type: "strategic_pillar", target_type: "value_stream" },
1524
1527
  strategic_pillar_decided_via_decision: { forward_verb: "decided_via", reverse_verb: "decided_for", classification: "hierarchy", source_type: "strategic_pillar", target_type: "decision" },
1525
- strategic_theme_pursues_initiative: { forward_verb: "pursues", reverse_verb: "pursued_under", classification: "hierarchy", source_type: "strategic_theme", target_type: "initiative" },
1528
+ strategic_theme_pursues_initiative: { forward_verb: "pursues", reverse_verb: "pursued_under", classification: "hierarchy", source_type: "strategic_theme", target_type: "initiative", cross_product_eligible: true },
1526
1529
  // v0.9.0 (UPG-660): the soft bridge from the annual strategy focus area to the
1527
1530
  // roadmap grouping that realises it. Semantic, NOT hierarchy: strategic_theme and
1528
1531
  // roadmap_theme sit on different spines (strategy cascade vs roadmap cascade), so
@@ -1550,11 +1553,11 @@ var UPG_EDGE_CATALOG = {
1550
1553
  // surfaces the upward-rollup read. Mirrors `strategic_pillar_organises_
1551
1554
  // strategic_theme` (pillar → theme), completing the strategic cascade:
1552
1555
  // strategic_pillar → strategic_theme → objective → key_result.
1553
- strategic_theme_delivers_outcome: { forward_verb: "delivers", reverse_verb: "delivered_by", classification: "causal", source_type: "strategic_theme", target_type: "outcome" },
1554
- strategic_theme_measured_by_key_result: { forward_verb: "measured_by", reverse_verb: "measures", classification: "causal", source_type: "strategic_theme", target_type: "key_result" },
1555
- strategic_theme_contains_objective: { forward_verb: "contains", reverse_verb: "rolls_up_to", classification: "hierarchy", source_type: "strategic_theme", target_type: "objective" },
1556
+ strategic_theme_delivers_outcome: { forward_verb: "delivers", reverse_verb: "delivered_by", classification: "causal", source_type: "strategic_theme", target_type: "outcome", cross_product_eligible: true },
1557
+ strategic_theme_measured_by_key_result: { forward_verb: "measured_by", reverse_verb: "measures", classification: "causal", source_type: "strategic_theme", target_type: "key_result", cross_product_eligible: true },
1558
+ strategic_theme_contains_objective: { forward_verb: "contains", reverse_verb: "rolls_up_to", classification: "hierarchy", source_type: "strategic_theme", target_type: "objective", cross_product_eligible: true },
1556
1559
  initiative_assumes_assumption: { forward_verb: "assumes", reverse_verb: "assumed_by", classification: "hierarchy", source_type: "initiative", target_type: "assumption" },
1557
- initiative_drives_outcome: { forward_verb: "drives", reverse_verb: "driven_by", classification: "cross-domain", source_type: "initiative", target_type: "outcome" },
1560
+ initiative_drives_outcome: { forward_verb: "drives", reverse_verb: "driven_by", classification: "cross-domain", source_type: "initiative", target_type: "outcome", cross_product_eligible: true },
1558
1561
  capability_enables_value_stream: { forward_verb: "enables", reverse_verb: "enabled_by", classification: "cross-domain", source_type: "capability", target_type: "value_stream" },
1559
1562
  // v0.5.2 (UPG-528): three capability-anchored edges that complete the
1560
1563
  // Wardley-style value chain: need → capability → capability → feature.
@@ -1692,7 +1695,7 @@ var UPG_EDGE_CATALOG = {
1692
1695
  // brand_identity. Dual-registered: also a cross-edge for the common case where the
1693
1696
  // brand is a registry singleton in another graph (the ratified pattern: brand is a
1694
1697
  // singleton expressed, not instance_of-d); this catalog entry is the within-graph case.
1695
- product_expresses_brand_identity: { forward_verb: "expresses", reverse_verb: "expressed_by", classification: "semantic", source_type: "product", target_type: "brand_identity" },
1698
+ product_expresses_brand_identity: { forward_verb: "expresses", reverse_verb: "expressed_by", classification: "semantic", source_type: "product", target_type: "brand_identity", cross_product_eligible: true },
1696
1699
  design_system_encompasses_user_journey: { forward_verb: "encompasses", reverse_verb: "encompassed_in", classification: "hierarchy", source_type: "design_system", target_type: "user_journey" },
1697
1700
  design_system_encompasses_user_flow: { forward_verb: "encompasses", reverse_verb: "encompassed_in", classification: "hierarchy", source_type: "design_system", target_type: "user_flow" },
1698
1701
  design_system_informed_by_insight: { forward_verb: "informed_by", reverse_verb: "informs", classification: "hierarchy", source_type: "design_system", target_type: "insight" },
@@ -1704,12 +1707,12 @@ var UPG_EDGE_CATALOG = {
1704
1707
  design_component_specified_by_interaction_spec: { forward_verb: "specified_by", reverse_verb: "specifies", classification: "hierarchy", source_type: "design_component", target_type: "interaction_spec" },
1705
1708
  design_component_composes_design_component: { forward_verb: "composes", reverse_verb: "composed_in", classification: "hierarchy", source_type: "design_component", target_type: "design_component" },
1706
1709
  prototype_annotated_with_annotation: { forward_verb: "annotated_with", reverse_verb: "annotates", classification: "hierarchy", source_type: "prototype", target_type: "annotation" },
1707
- screen_renders_design_component: { forward_verb: "renders", reverse_verb: "rendered_on", classification: "hierarchy", source_type: "screen", target_type: "design_component" },
1710
+ screen_renders_design_component: { forward_verb: "renders", reverse_verb: "rendered_on", classification: "hierarchy", source_type: "screen", target_type: "design_component", cross_product_eligible: true },
1708
1711
  // Marketing surface to product (0.12.7, UPG-696/698). A marketing/landing screen
1709
1712
  // markets a product. Dual-registered: also a cross-edge (UPG_CROSS_EDGE_TYPES) for
1710
1713
  // the common case where the product lives in another graph; this catalog entry is
1711
1714
  // the within-graph degenerate case (a product's own landing page).
1712
- screen_markets_product: { forward_verb: "markets", reverse_verb: "marketed_by", classification: "semantic", source_type: "screen", target_type: "product" },
1715
+ screen_markets_product: { forward_verb: "markets", reverse_verb: "marketed_by", classification: "semantic", source_type: "screen", target_type: "product", cross_product_eligible: true },
1713
1716
  // Connective cross-product references (0.13.1, Data's connective-layer brief).
1714
1717
  // Dual-registered (also in UPG_CROSS_EDGE_TYPES) for the common cross-graph case;
1715
1718
  // these catalog entries are the within-graph degenerate case.
@@ -1725,10 +1728,10 @@ var UPG_EDGE_CATALOG = {
1725
1728
  // system (the adoption semantic, mirroring product_implements_specification). Distinct
1726
1729
  // from the hierarchy `product_systematised_in_design_system` (structural containment):
1727
1730
  // same pattern as brand (branded_as = hierarchy, expresses = semantic).
1728
- screen_targets_competitor: { forward_verb: "targets", reverse_verb: "targeted_by", classification: "semantic", source_type: "screen", target_type: "competitor" },
1729
- feature_surfaces_product: { forward_verb: "surfaces", reverse_verb: "surfaced_by", classification: "semantic", source_type: "feature", target_type: "product" },
1730
- feature_uses_design_component: { forward_verb: "uses", reverse_verb: "used_by", classification: "semantic", source_type: "feature", target_type: "design_component" },
1731
- product_implements_design_system: { forward_verb: "implements", reverse_verb: "implemented_by", classification: "semantic", source_type: "product", target_type: "design_system" },
1731
+ screen_targets_competitor: { forward_verb: "targets", reverse_verb: "targeted_by", classification: "semantic", source_type: "screen", target_type: "competitor", cross_product_eligible: true },
1732
+ feature_surfaces_product: { forward_verb: "surfaces", reverse_verb: "surfaced_by", classification: "semantic", source_type: "feature", target_type: "product", cross_product_eligible: true },
1733
+ feature_uses_design_component: { forward_verb: "uses", reverse_verb: "used_by", classification: "semantic", source_type: "feature", target_type: "design_component", cross_product_eligible: true },
1734
+ product_implements_design_system: { forward_verb: "implements", reverse_verb: "implemented_by", classification: "semantic", source_type: "product", target_type: "design_system", cross_product_eligible: true },
1732
1735
  screen_navigates_to_screen: { forward_verb: "navigates_to", reverse_verb: "navigated_from", classification: "hierarchy", source_type: "screen", target_type: "screen" },
1733
1736
  screen_surfaces_feature: { forward_verb: "surfaces", reverse_verb: "surfaced_on", classification: "cross-domain", source_type: "screen", target_type: "feature" },
1734
1737
  screen_wireframed_as_wireframe: { forward_verb: "wireframed_as", reverse_verb: "wireframes", classification: "hierarchy", source_type: "screen", target_type: "wireframe" },
@@ -2312,6 +2315,16 @@ var UPG_EDGE_CATALOG = {
2312
2315
  // UPG_EDGE_MIGRATIONS['0.9.9'].
2313
2316
  product_organised_into_department: { forward_verb: "organised_into", reverse_verb: "organises", classification: "hierarchy", source_type: "product", target_type: "department" },
2314
2317
  department_contains_team: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "department", target_type: "team" },
2318
+ // Second-level team nesting (0.17.2). department_contains_team covers the first
2319
+ // level (department -> team); this covers a team nested inside another team in
2320
+ // the same department (a sub-team or squad under a parent team). Modelled
2321
+ // parent -> child like feature_area_contains_feature_area, so the org map builds
2322
+ // correctly in get_tree (which reads source as the parent); the reverse verb
2323
+ // belongs_to gives the upward "this team is part of that team" read. The
2324
+ // same-department expectation is advisory (a write-time warning), never a hard
2325
+ // block; cross-department reporting lines belong to person_reports_to_person at
2326
+ // the individual level.
2327
+ team_contains_team: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "team", target_type: "team" },
2315
2328
  department_includes_stakeholder: { forward_verb: "includes", reverse_verb: "included_in", classification: "hierarchy", source_type: "department", target_type: "stakeholder" },
2316
2329
  // NOTE: `person` is a containment-free entity type (see
2317
2330
  // UPG_CONTAINMENT_FREE_TYPES in grammar/hierarchy.ts). It is referenced
@@ -2570,10 +2583,10 @@ var UPG_EDGE_CATALOG = {
2570
2583
  experiment_run_measured_by_metric: { forward_verb: "measured_by", reverse_verb: "measures", classification: "hierarchy", source_type: "experiment_run", target_type: "metric" },
2571
2584
  solution_measured_by_metric: { forward_verb: "measured_by", reverse_verb: "measures", classification: "hierarchy", source_type: "solution", target_type: "metric" },
2572
2585
  // ── Cross-domain: Ownership ──────────────────────────────────────────────────
2573
- node_owned_by_team: { forward_verb: "owned_by", reverse_verb: "owns", classification: "cross-domain", source_type: "node", target_type: "team" },
2586
+ node_owned_by_team: { forward_verb: "owned_by", reverse_verb: "owns", classification: "cross-domain", source_type: "node", target_type: "team", cross_product_eligible: true },
2574
2587
  node_owned_by_role: { forward_verb: "owned_by", reverse_verb: "owns", classification: "cross-domain", source_type: "node", target_type: "role" },
2575
2588
  node_owned_by_stakeholder: { forward_verb: "owned_by", reverse_verb: "owns", classification: "cross-domain", source_type: "node", target_type: "stakeholder" },
2576
- node_owned_by_department: { forward_verb: "owned_by", reverse_verb: "owns", classification: "cross-domain", source_type: "node", target_type: "department" },
2589
+ node_owned_by_department: { forward_verb: "owned_by", reverse_verb: "owns", classification: "cross-domain", source_type: "node", target_type: "department", cross_product_eligible: true },
2577
2590
  node_owned_by_person: { forward_verb: "owned_by", reverse_verb: "owns", classification: "cross-domain", source_type: "node", target_type: "person" },
2578
2591
  // ── Cross-domain: Architecture (DDD) ─────────────────────────────────────────
2579
2592
  // Service, domain_event, domain_entity, aggregate, read_model, api_contract,
@@ -3237,7 +3250,7 @@ var UPG_EDGE_CATALOG = {
3237
3250
  // not the outcome it exists to move; the strategic spine was severed at its
3238
3251
  // anchor (0 outbound to outcome). An objective ADVANCES an outcome. Intra-
3239
3252
  // domain (both in strategy) and directional → causal.
3240
- objective_advances_outcome: { forward_verb: "advances", reverse_verb: "advanced_by", classification: "causal", source_type: "objective", target_type: "outcome" },
3253
+ objective_advances_outcome: { forward_verb: "advances", reverse_verb: "advanced_by", classification: "causal", source_type: "objective", target_type: "outcome", cross_product_eligible: true },
3241
3254
  // ── F6: Priority 2 — scored / synthesis / verdict dead-end leaves (P-A) ─────
3242
3255
  // desired_outcome (Ulwick-scored: importance × satisfaction) had 0 outbound —
3243
3256
  // the scores could not flow anywhere. An under-served desired_outcome REVEALS
@@ -3320,9 +3333,16 @@ var UPG_EDGE_CATALOG = {
3320
3333
  specification_governed_by_organization: { forward_verb: "governed_by", reverse_verb: "governs", classification: "semantic", source_type: "specification", target_type: "organization" },
3321
3334
  // Operating-lifecycle cross-edges (0.11.6): a product's journey phase realises a canonical
3322
3335
  // operating stage (the cross-surface join key); a stage is measured by a rollup metric (loop close).
3323
- journey_phase_realises_operating_stage: { forward_verb: "realises", reverse_verb: "realised_by", classification: "cross-domain", source_type: "journey_phase", target_type: "operating_stage" },
3336
+ journey_phase_realises_operating_stage: { forward_verb: "realises", reverse_verb: "realised_by", classification: "cross-domain", source_type: "journey_phase", target_type: "operating_stage", cross_product_eligible: true },
3324
3337
  operating_stage_measured_by_metric: { forward_verb: "measured_by", reverse_verb: "measures", classification: "cross-domain", source_type: "operating_stage", target_type: "metric" }
3325
3338
  };
3339
+ var UPG_CROSS_ELIGIBLE_CATALOG_EDGE_TYPES = Object.keys(UPG_EDGE_CATALOG).filter(
3340
+ (k) => UPG_EDGE_CATALOG[k].cross_product_eligible === true
3341
+ );
3342
+ function isCrossProductEligible(type) {
3343
+ const def = UPG_EDGE_CATALOG[type];
3344
+ return def?.cross_product_eligible === true;
3345
+ }
3326
3346
  var UPG_POLYMORPHIC_EDGE_KEYS = [
3327
3347
  // Universal semantic verbs
3328
3348
  "node_informs_node",
@@ -3382,46 +3402,47 @@ function isLegacyProductStage(stage) {
3382
3402
  }
3383
3403
 
3384
3404
  // src/shapes/document.ts
3385
- var UPG_CROSS_EDGE_TYPES = [
3405
+ var UPG_CROSS_ONLY_EDGE_TYPES = [
3406
+ // Peer overlap: "these two products share / overlap on this thing." Symmetric,
3407
+ // no canonical instance_of identity. shares_job / shares_need are the job/need
3408
+ // siblings of the persona/competitor/metric peers (connective layer, 0.13.1).
3386
3409
  "shares_persona",
3387
3410
  "shares_competitor",
3388
3411
  "shares_metric",
3412
+ "shares_job",
3413
+ "shares_need",
3414
+ // Product-to-product relationships.
3389
3415
  "depends_on_product",
3416
+ // runtime dependency (built-on / dogfood), not containment
3390
3417
  "cannibalises",
3391
3418
  "succeeds",
3392
3419
  "hosts",
3420
+ // host runs the hosted product inside itself (container -> contained)
3421
+ // OKR / measurement rollup primitives: the cascade ACROSS products (subordinate
3422
+ // -> superior). `contributes_to` rolls a product strategy entity up to a higher-
3423
+ // level one; `rolls_up_to` feeds a product metric into a higher-level north-star.
3393
3424
  "contributes_to",
3425
+ "rolls_up_to",
3426
+ // Canonical instance + area-to-audience: the target lives in the portfolio
3427
+ // `registry` section (`registry/{node_id}`). `instance_of` is same-type
3428
+ // canonical-to-instance; the area edges are minted via `link_area_to_audience`.
3394
3429
  "instance_of",
3395
3430
  "area_serves_persona",
3396
3431
  "area_targets_market_segment",
3397
- "rolls_up_to",
3432
+ // Foundations (0.9.12): a product-graph entity links to a canonical specification
3433
+ // or primitive in the registry (`registry/{node_id}` target, same shape as
3434
+ // instance_of). Registry-internal spec-to-spec links are catalog edges, not these.
3398
3435
  "product_implements_specification",
3399
3436
  "product_exposes_specification",
3400
3437
  "feature_conforms_to_specification",
3401
3438
  "api_contract_speaks_specification",
3402
3439
  "product_exposes_primitive",
3403
3440
  "feature_manipulates_primitive",
3404
- "primitive_stored_as_data_type",
3405
- "feature_rivals_competitor_feature",
3406
- "competitor_signal_maps_to_feature",
3407
- "competitor_signal_surfaces_opportunity",
3408
- "competitor_classified_as_classification_value",
3409
- "node_classified_as_classification_value",
3410
- "journey_phase_realises_operating_stage",
3411
- "screen_markets_product",
3412
- "screen_renders_design_component",
3413
- "product_expresses_brand_identity",
3414
- // Connective cross-product layer (0.13.1) — see UPGCrossEdgeType above.
3415
- "shares_job",
3416
- "shares_need",
3417
- "persona_delegates_to_persona",
3418
- "screen_targets_competitor",
3419
- "feature_surfaces_product",
3420
- "feature_uses_design_component",
3421
- "product_implements_design_system",
3422
- // Org-link (0.17.0): dual-registered ownership edges (see UPGCrossEdgeType above).
3423
- "node_owned_by_team",
3424
- "node_owned_by_department"
3441
+ "primitive_stored_as_data_type"
3442
+ ];
3443
+ var UPG_CROSS_EDGE_TYPES = [
3444
+ ...UPG_CROSS_ONLY_EDGE_TYPES,
3445
+ ...UPG_CROSS_ELIGIBLE_CATALOG_EDGE_TYPES
3425
3446
  ];
3426
3447
  var REGISTRY_PRODUCT_ID = "registry";
3427
3448
  var UPG_PORTFOLIO_KINDS = ["owned", "watched", "strategic", "internal", "gtm"];
@@ -3875,7 +3896,11 @@ var UPG_VALID_CHILDREN = {
3875
3896
  "skill",
3876
3897
  "ceremony",
3877
3898
  "capacity_plan",
3878
- "decision"
3899
+ "decision",
3900
+ // Self-nesting (0.17.2): a parent team contains its sub-teams / squads, one
3901
+ // level below department_contains_team. Backs team_contains_team. Mirrors
3902
+ // feature_area_contains_feature_area (same-type child listed last).
3903
+ "team"
3879
3904
  ],
3880
3905
  // ── Data & Analytics hierarchy ───────────────────────────────────────────────
3881
3906
  data_source: ["metric", "data_pipeline", "data_lineage", "event_schema"],
@@ -10868,6 +10893,7 @@ var UPG_PROPERTY_SCHEMA = {
10868
10893
  // ConstraintProperties: Constraint: a named limitation or boundary on product creation.
10869
10894
  constraint: {
10870
10895
  constraint_kind: { type: "string", enum: ["resource", "technical", "regulatory", "temporal", "compliance", "other"], description: "Limitation category" },
10896
+ constraint_origin: { type: "string", enum: ["internal", "external"], description: "Provenance: internal (a self-imposed principle or operating tenet) or external (a limit, requirement, or ceiling imposed from outside)." },
10871
10897
  constraint_status: { type: "string", enum: ["binding", "advisory", "lifted"], description: "Whether binding, advisory, or lifted" },
10872
10898
  rule_strength: { type: "string", enum: ["must", "must_not", "exception", "warning", "guideline"], description: "Enforcement strictness. Reuses the governance/guideline rule vocabulary." },
10873
10899
  source: { type: "string", description: "Free-text origin: policy document, regulation, stakeholder, technical doc." },
@@ -21258,6 +21284,30 @@ var UPG_TREE_PATTERNS = [
21258
21284
  metric: [opt("metric"), opt("outcome")]
21259
21285
  },
21260
21286
  natural_depth: 3
21287
+ },
21288
+ {
21289
+ id: "org",
21290
+ label: "Org chart",
21291
+ description: "Departments and the teams they contain, plus the sub-teams nested within those teams. The people-structure org chart, walked by containment (department_contains_team, then team_contains_team). Distinct from product_area, which is the product classification axis, not the org.",
21292
+ region: "operations_quality",
21293
+ gap_policy: "all-optional",
21294
+ anchor_type: "department",
21295
+ // A graph with no department roots at the team (team -> sub-team), reported
21296
+ // anchor_resolved_from: department. A sub-team also renders as a bare team
21297
+ // root when its parent department is not modelled.
21298
+ fallback_anchors: ["team"],
21299
+ child_map: {
21300
+ // department -> team (department_contains_team) is the first level; team ->
21301
+ // team (team_contains_team, 0.17.2) is the second, so a sub-team nested one
21302
+ // level deeper than department_contains_team renders under its parent team.
21303
+ // Self-nesting: the assembler's `seen` set terminates a team -> team cycle
21304
+ // for free (a team met again becomes a shared reference).
21305
+ department: [opt("team")],
21306
+ team: [opt("team")]
21307
+ },
21308
+ // Default to 3 tiers (department -> team -> sub-team); `depth` extends into
21309
+ // deeper team_contains_team nesting.
21310
+ natural_depth: 3
21261
21311
  }
21262
21312
  ];
21263
21313
  var UPG_TREE_PATTERNS_BY_ID = Object.fromEntries(
@@ -23047,6 +23097,12 @@ var STRATEGY_GUIDE = {
23047
23097
  description: "Vision grounds mission, product pursues outcomes and targets objectives, objectives achieved through key results measured by metrics",
23048
23098
  entity_types: ["vision", "mission", "outcome", "objective", "key_result", "metric"],
23049
23099
  edge_chain: ["product_guided_by_vision", "product_fulfils_mission", "vision_realised_through_mission", "product_pursues_outcome", "product_targets_objective", "objective_achieved_through_key_result", "outcome_measured_by_metric"]
23100
+ },
23101
+ {
23102
+ name: "Rendering the company spine",
23103
+ description: "The company OKR / strategy / north-star spine already renders with get_tree, no new pattern needed: switch to the org_rollup member (the company rollup graph) and run the okr, strategy, or north_star tree pattern there. In a portfolio the spine can span files, a rollup theme laddering into product-graph objectives through the cross-product strategic_theme_contains_objective edge; get_tree walks the active member, so render the rollup for the whole-company view and a product for its slice.",
23104
+ entity_types: ["strategic_theme", "objective", "key_result", "metric"],
23105
+ edge_chain: ["strategic_theme_contains_objective", "objective_achieved_through_key_result", "key_result_quantified_by_metric"]
23050
23106
  }
23051
23107
  ],
23052
23108
  required_bridges: [
@@ -23057,7 +23113,8 @@ var STRATEGY_GUIDE = {
23057
23113
  anti_patterns: [
23058
23114
  { description: "Outcomes without metrics. Measurement is the signal an outcome happened." },
23059
23115
  { description: "Objectives without key results: an objective without measurement is a wish" },
23060
- { description: "Too many strategic themes: focus beats breadth, aim for 2-4 active themes" }
23116
+ { description: "Too many strategic themes: focus beats breadth, aim for 2-4 active themes" },
23117
+ { description: "Constraints without an origin: a qualitative guardrail lives on a constraint, so set constraint_origin to say where it comes from. internal marks a self-imposed principle or operating tenet the team commits to; external marks a limit, requirement, or ceiling imposed from outside (a regulation, a budget cap, a platform bound)." }
23061
23118
  ]
23062
23119
  };
23063
23120
  var PRODUCT_SPEC_GUIDE = {
@@ -23608,6 +23665,12 @@ var TEAM_ORG_GUIDE = {
23608
23665
  description: "Teams are staffed with roles, target OKRs, reflect in retrospectives, and are blocked by dependencies",
23609
23666
  entity_types: ["team", "role", "team_okr", "retrospective", "dependency"],
23610
23667
  edge_chain: ["team_staffed_with_role", "team_targets_team_okr", "team_reflects_in_retrospective", "dependency_blocks_team"]
23668
+ },
23669
+ {
23670
+ name: "Org Nesting",
23671
+ description: "A department contains its teams; a parent team contains its sub-teams or squads one level deeper. Keep a sub-team in the same department as its parent team. Cross-department reporting lines are modelled at the individual level with person_reports_to_person, not by nesting a team under a parent in another department.",
23672
+ entity_types: ["department", "team"],
23673
+ edge_chain: ["department_contains_team", "team_contains_team"]
23611
23674
  }
23612
23675
  ],
23613
23676
  required_bridges: [
@@ -23618,6 +23681,7 @@ var TEAM_ORG_GUIDE = {
23618
23681
  { description: "Teams without OKRs: every team needs clear goals" },
23619
23682
  { description: "Dependencies without both teams linked: a dependency must connect blocker and blocked" },
23620
23683
  { description: "Retrospectives without action items: reflection without action is just venting" },
23684
+ { description: "Nesting a sub-team under a parent team in a different department: a team_contains_team nesting should stay within one department (both teams sharing a department_contains_team parent). A cross-department nesting is almost always a modelling error; model a cross-department reporting line at the individual level with person_reports_to_person instead." },
23621
23685
  { description: "Double-modelling the org as product_area and team_org: a product_area is the classification axis ON products (which area a product belongs to, the axis used to group products); a team_org department/team is the people structure (the org chart, who reports to whom). Use product_area to classify products and department/team for the org chart, and relate them through the team that owns an area rather than restating one as the other." }
23622
23686
  ]
23623
23687
  };
@@ -26340,7 +26404,7 @@ var UPG_ANTI_PATTERNS = [
26340
26404
  {
26341
26405
  id: "competitors-missing-past-validation",
26342
26406
  name: "Competitor catalogue empty past validation",
26343
- description: "The graph has zero competitor entities at validation stage or later. Past concept, the absence of named competitors usually means alternatives haven't been thought through, not that none exist.",
26407
+ description: "The graph has zero competitor entities at build stage or later. Past validation, the absence of named competitors usually means alternatives haven't been thought through, not that none exist. A graph still at concept or validation is exempt: cataloguing the field can wait until you commit to building.",
26344
26408
  structured_condition: {
26345
26409
  check: {
26346
26410
  type: "benchmark",
@@ -26350,14 +26414,18 @@ var UPG_ANTI_PATTERNS = [
26350
26414
  },
26351
26415
  why_it_matters: "Without competitors in the graph, positioning, differentiation, and switching-cost analysis lack referents. The team is reasoning in a vacuum.",
26352
26416
  remediation: "Catalogue the 3\u20135 closest alternatives users would pick today. Use `/upg-compete` to author.",
26353
- stages: ["validation", "build", "beta", "launch", "growth", "mature"],
26417
+ // Fires from build onward, matching the name (past the validation stage). A
26418
+ // concept or validation graph is still framing the problem; holding it to a
26419
+ // competitor benchmark there is the false alarm this anti-pattern caused on
26420
+ // legitimately-young graphs.
26421
+ stages: ["build", "beta", "launch", "growth", "mature"],
26354
26422
  severity: "medium",
26355
26423
  source: { kind: "industry_practice", category: "product_strategy" }
26356
26424
  },
26357
26425
  {
26358
26426
  id: "persona-count-below-stage-benchmark",
26359
26427
  name: "Persona count below stage benchmark",
26360
- description: "The graph has fewer personas than the stage-appropriate benchmark expects. Persona under-coverage signals the team has not segmented its audience.",
26428
+ description: "The graph has fewer personas than the stage-appropriate benchmark expects, from beta onward. Persona under-coverage at that point signals the team has not segmented its audience. Concept through build are exempt: the sound move there is to start with one beachhead persona and expand as you find fit.",
26361
26429
  structured_condition: {
26362
26430
  check: {
26363
26431
  type: "benchmark",
@@ -26365,9 +26433,13 @@ var UPG_ANTI_PATTERNS = [
26365
26433
  comparison: "below_min"
26366
26434
  }
26367
26435
  },
26368
- why_it_matters: 'A graph with one persona past validation is usually carrying an unexamined "everyone is the same user" assumption.',
26436
+ why_it_matters: "A graph still on a single persona by beta is usually carrying an unexamined assumption that every user is the same.",
26369
26437
  remediation: "Add personas representing the next 1\u20132 most distinct user segments. Use `/upg-new-persona`.",
26370
- stages: ["validation", "build", "beta", "launch", "growth", "mature"],
26438
+ // Fires from beta onward. The persona benchmark expects 2+ from validation,
26439
+ // but the canonical beachhead move is to start with one persona and expand as
26440
+ // fit is found, so holding a build-stage graph to a multi-persona bar is a
26441
+ // false alarm. By beta an unsegmented audience is a real signal.
26442
+ stages: ["beta", "launch", "growth", "mature"],
26371
26443
  severity: "medium",
26372
26444
  source: { kind: "practitioner", attribution: "Alan Cooper, The Inmates Are Running the Asylum" }
26373
26445
  },
@@ -26392,7 +26464,7 @@ var UPG_ANTI_PATTERNS = [
26392
26464
  {
26393
26465
  id: "single-domain-graph",
26394
26466
  name: "Single-domain graph",
26395
- description: "The graph has more than five entities but they all live in a single UPG domain. A real product spans multiple domains; a single-domain graph is usually a deep notebook in one corner with the rest of the picture missing.",
26467
+ description: "The graph has more than five entities but they all live in a single UPG domain, at beta stage or later. A real product spans multiple domains by then; a single-domain graph that far along is usually a deep notebook in one corner with the rest of the picture missing. Concept through build are exempt: a graph legitimately starts deep in one domain and broadens as it matures.",
26396
26468
  structured_condition: {
26397
26469
  operator: "and",
26398
26470
  checks: [
@@ -26402,7 +26474,11 @@ var UPG_ANTI_PATTERNS = [
26402
26474
  },
26403
26475
  why_it_matters: 'Single-domain coverage prevents cross-domain reasoning. The graph cannot answer questions like "which feature serves which persona?" or "which experiment validates which hypothesis?".',
26404
26476
  remediation: "Identify the next adjacent domain (usually `user`, `validation`, or `product_spec`) and add 2\u20133 anchor entities to bridge.",
26405
- stages: ["validation", "build", "beta", "launch"],
26477
+ // Fires from beta onward. Early on a graph is legitimately deep in one domain
26478
+ // (a discovery notebook, a thin internal tool); only once it reaches beta is
26479
+ // single-domain coverage a smell rather than a normal starting shape. Extends
26480
+ // through growth and mature, where a single-domain graph is most telling.
26481
+ stages: ["beta", "launch", "growth", "mature"],
26406
26482
  severity: "medium",
26407
26483
  source: { kind: "fundamental" }
26408
26484
  },
@@ -29800,7 +29876,7 @@ function isCanonical(text) {
29800
29876
  }
29801
29877
 
29802
29878
  // src/index.ts
29803
- var UPG_VERSION = "0.17.1";
29879
+ var UPG_VERSION = "0.17.3";
29804
29880
  var UPG_FORMAT_VERSION = "0.4.0";
29805
29881
  var MARKDOWN_FORMAT_VERSION = "0.1";
29806
29882
  var UPG_TYPES = getTypes();
@@ -29903,6 +29979,8 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
29903
29979
  UPG_CONTAINMENT_FREE_TYPES,
29904
29980
  UPG_COUNT_BENCHMARKS,
29905
29981
  UPG_CROSS_EDGE_TYPES,
29982
+ UPG_CROSS_ELIGIBLE_CATALOG_EDGE_TYPES,
29983
+ UPG_CROSS_ONLY_EDGE_TYPES,
29906
29984
  UPG_DEPRECATED_TYPES,
29907
29985
  UPG_DOMAINS,
29908
29986
  UPG_DOMAIN_ACTIVATION,
@@ -30033,6 +30111,7 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
30033
30111
  isCanonical,
30034
30112
  isCanonicalProductStage,
30035
30113
  isContainmentFreeType,
30114
+ isCrossProductEligible,
30036
30115
  isDeprecatedType,
30037
30116
  isDomainGuideStep,
30038
30117
  isEntitySequenceStep,