@unified-product-graph/core 0.17.0 → 0.17.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/dist/index.cjs CHANGED
@@ -98,6 +98,7 @@ __export(index_exports, {
98
98
  UPG_MIGRATIONS: () => UPG_MIGRATIONS,
99
99
  UPG_PLAYBOOKS: () => UPG_PLAYBOOKS,
100
100
  UPG_POLYMORPHIC_EDGE_KEYS: () => UPG_POLYMORPHIC_EDGE_KEYS,
101
+ UPG_PORTFOLIO_KINDS: () => UPG_PORTFOLIO_KINDS,
101
102
  UPG_PRODUCT_STAGES: () => UPG_PRODUCT_STAGES,
102
103
  UPG_PRODUCT_STAGE_COERCION_MAP: () => UPG_PRODUCT_STAGE_COERCION_MAP,
103
104
  UPG_PROPERTY_MIGRATIONS: () => UPG_PROPERTY_MIGRATIONS,
@@ -2311,6 +2312,16 @@ var UPG_EDGE_CATALOG = {
2311
2312
  // UPG_EDGE_MIGRATIONS['0.9.9'].
2312
2313
  product_organised_into_department: { forward_verb: "organised_into", reverse_verb: "organises", classification: "hierarchy", source_type: "product", target_type: "department" },
2313
2314
  department_contains_team: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "department", target_type: "team" },
2315
+ // Second-level team nesting (0.17.2). department_contains_team covers the first
2316
+ // level (department -> team); this covers a team nested inside another team in
2317
+ // the same department (a sub-team or squad under a parent team). Modelled
2318
+ // parent -> child like feature_area_contains_feature_area, so the org map builds
2319
+ // correctly in get_tree (which reads source as the parent); the reverse verb
2320
+ // belongs_to gives the upward "this team is part of that team" read. The
2321
+ // same-department expectation is advisory (a write-time warning), never a hard
2322
+ // block; cross-department reporting lines belong to person_reports_to_person at
2323
+ // the individual level.
2324
+ team_contains_team: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "team", target_type: "team" },
2314
2325
  department_includes_stakeholder: { forward_verb: "includes", reverse_verb: "included_in", classification: "hierarchy", source_type: "department", target_type: "stakeholder" },
2315
2326
  // NOTE: `person` is a containment-free entity type (see
2316
2327
  // UPG_CONTAINMENT_FREE_TYPES in grammar/hierarchy.ts). It is referenced
@@ -3420,9 +3431,16 @@ var UPG_CROSS_EDGE_TYPES = [
3420
3431
  "product_implements_design_system",
3421
3432
  // Org-link (0.17.0): dual-registered ownership edges (see UPGCrossEdgeType above).
3422
3433
  "node_owned_by_team",
3423
- "node_owned_by_department"
3434
+ "node_owned_by_department",
3435
+ // Cross-graph strategy & measurement (0.17.2): dual-registered OKR/measurement
3436
+ // hierarchy edges so the strategy spine can span files (see UPGCrossEdgeType above).
3437
+ "strategic_theme_contains_objective",
3438
+ "objective_achieved_through_key_result",
3439
+ "key_result_quantified_by_metric",
3440
+ "objective_measured_by_metric"
3424
3441
  ];
3425
3442
  var REGISTRY_PRODUCT_ID = "registry";
3443
+ var UPG_PORTFOLIO_KINDS = ["owned", "watched", "strategic", "internal", "gtm"];
3426
3444
 
3427
3445
  // src/registry/entity-type-resolution.ts
3428
3446
  var TYPES = getTypes();
@@ -3873,7 +3891,11 @@ var UPG_VALID_CHILDREN = {
3873
3891
  "skill",
3874
3892
  "ceremony",
3875
3893
  "capacity_plan",
3876
- "decision"
3894
+ "decision",
3895
+ // Self-nesting (0.17.2): a parent team contains its sub-teams / squads, one
3896
+ // level below department_contains_team. Backs team_contains_team. Mirrors
3897
+ // feature_area_contains_feature_area (same-type child listed last).
3898
+ "team"
3877
3899
  ],
3878
3900
  // ── Data & Analytics hierarchy ───────────────────────────────────────────────
3879
3901
  data_source: ["metric", "data_pipeline", "data_lineage", "event_schema"],
@@ -10866,6 +10888,7 @@ var UPG_PROPERTY_SCHEMA = {
10866
10888
  // ConstraintProperties: Constraint: a named limitation or boundary on product creation.
10867
10889
  constraint: {
10868
10890
  constraint_kind: { type: "string", enum: ["resource", "technical", "regulatory", "temporal", "compliance", "other"], description: "Limitation category" },
10891
+ 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)." },
10869
10892
  constraint_status: { type: "string", enum: ["binding", "advisory", "lifted"], description: "Whether binding, advisory, or lifted" },
10870
10893
  rule_strength: { type: "string", enum: ["must", "must_not", "exception", "warning", "guideline"], description: "Enforcement strictness. Reuses the governance/guideline rule vocabulary." },
10871
10894
  source: { type: "string", description: "Free-text origin: policy document, regulation, stakeholder, technical doc." },
@@ -23055,7 +23078,8 @@ var STRATEGY_GUIDE = {
23055
23078
  anti_patterns: [
23056
23079
  { description: "Outcomes without metrics. Measurement is the signal an outcome happened." },
23057
23080
  { description: "Objectives without key results: an objective without measurement is a wish" },
23058
- { description: "Too many strategic themes: focus beats breadth, aim for 2-4 active themes" }
23081
+ { description: "Too many strategic themes: focus beats breadth, aim for 2-4 active themes" },
23082
+ { 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)." }
23059
23083
  ]
23060
23084
  };
23061
23085
  var PRODUCT_SPEC_GUIDE = {
@@ -23606,6 +23630,12 @@ var TEAM_ORG_GUIDE = {
23606
23630
  description: "Teams are staffed with roles, target OKRs, reflect in retrospectives, and are blocked by dependencies",
23607
23631
  entity_types: ["team", "role", "team_okr", "retrospective", "dependency"],
23608
23632
  edge_chain: ["team_staffed_with_role", "team_targets_team_okr", "team_reflects_in_retrospective", "dependency_blocks_team"]
23633
+ },
23634
+ {
23635
+ name: "Org Nesting",
23636
+ 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.",
23637
+ entity_types: ["department", "team"],
23638
+ edge_chain: ["department_contains_team", "team_contains_team"]
23609
23639
  }
23610
23640
  ],
23611
23641
  required_bridges: [
@@ -23615,7 +23645,9 @@ var TEAM_ORG_GUIDE = {
23615
23645
  anti_patterns: [
23616
23646
  { description: "Teams without OKRs: every team needs clear goals" },
23617
23647
  { description: "Dependencies without both teams linked: a dependency must connect blocker and blocked" },
23618
- { description: "Retrospectives without action items: reflection without action is just venting" }
23648
+ { description: "Retrospectives without action items: reflection without action is just venting" },
23649
+ { 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." },
23650
+ { 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." }
23619
23651
  ]
23620
23652
  };
23621
23653
  var PROGRAM_MGMT_GUIDE = {
@@ -23724,7 +23756,8 @@ var PORTFOLIO_GUIDE = {
23724
23756
  ],
23725
23757
  anti_patterns: [
23726
23758
  { description: "Products without a product area: every product should be classified" },
23727
- { description: "Portfolios without strategic alignment: portfolio decisions should reflect strategy" }
23759
+ { description: "Portfolios without strategic alignment: portfolio decisions should reflect strategy" },
23760
+ { description: "Confusing product_area with the org chart: product_area is the classification axis on products (how products are grouped), not the people structure. Model who reports to whom with team_org department/team; relate the two through the team that owns an area." }
23728
23761
  ]
23729
23762
  };
23730
23763
  var WORKSPACE_GUIDE = {
@@ -26336,7 +26369,7 @@ var UPG_ANTI_PATTERNS = [
26336
26369
  {
26337
26370
  id: "competitors-missing-past-validation",
26338
26371
  name: "Competitor catalogue empty past validation",
26339
- 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.",
26372
+ 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.",
26340
26373
  structured_condition: {
26341
26374
  check: {
26342
26375
  type: "benchmark",
@@ -26346,14 +26379,18 @@ var UPG_ANTI_PATTERNS = [
26346
26379
  },
26347
26380
  why_it_matters: "Without competitors in the graph, positioning, differentiation, and switching-cost analysis lack referents. The team is reasoning in a vacuum.",
26348
26381
  remediation: "Catalogue the 3\u20135 closest alternatives users would pick today. Use `/upg-compete` to author.",
26349
- stages: ["validation", "build", "beta", "launch", "growth", "mature"],
26382
+ // Fires from build onward, matching the name (past the validation stage). A
26383
+ // concept or validation graph is still framing the problem; holding it to a
26384
+ // competitor benchmark there is the false alarm this anti-pattern caused on
26385
+ // legitimately-young graphs.
26386
+ stages: ["build", "beta", "launch", "growth", "mature"],
26350
26387
  severity: "medium",
26351
26388
  source: { kind: "industry_practice", category: "product_strategy" }
26352
26389
  },
26353
26390
  {
26354
26391
  id: "persona-count-below-stage-benchmark",
26355
26392
  name: "Persona count below stage benchmark",
26356
- description: "The graph has fewer personas than the stage-appropriate benchmark expects. Persona under-coverage signals the team has not segmented its audience.",
26393
+ 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.",
26357
26394
  structured_condition: {
26358
26395
  check: {
26359
26396
  type: "benchmark",
@@ -26361,9 +26398,13 @@ var UPG_ANTI_PATTERNS = [
26361
26398
  comparison: "below_min"
26362
26399
  }
26363
26400
  },
26364
- why_it_matters: 'A graph with one persona past validation is usually carrying an unexamined "everyone is the same user" assumption.',
26401
+ why_it_matters: "A graph still on a single persona by beta is usually carrying an unexamined assumption that every user is the same.",
26365
26402
  remediation: "Add personas representing the next 1\u20132 most distinct user segments. Use `/upg-new-persona`.",
26366
- stages: ["validation", "build", "beta", "launch", "growth", "mature"],
26403
+ // Fires from beta onward. The persona benchmark expects 2+ from validation,
26404
+ // but the canonical beachhead move is to start with one persona and expand as
26405
+ // fit is found, so holding a build-stage graph to a multi-persona bar is a
26406
+ // false alarm. By beta an unsegmented audience is a real signal.
26407
+ stages: ["beta", "launch", "growth", "mature"],
26367
26408
  severity: "medium",
26368
26409
  source: { kind: "practitioner", attribution: "Alan Cooper, The Inmates Are Running the Asylum" }
26369
26410
  },
@@ -26388,7 +26429,7 @@ var UPG_ANTI_PATTERNS = [
26388
26429
  {
26389
26430
  id: "single-domain-graph",
26390
26431
  name: "Single-domain graph",
26391
- 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.",
26432
+ 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.",
26392
26433
  structured_condition: {
26393
26434
  operator: "and",
26394
26435
  checks: [
@@ -26398,7 +26439,11 @@ var UPG_ANTI_PATTERNS = [
26398
26439
  },
26399
26440
  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?".',
26400
26441
  remediation: "Identify the next adjacent domain (usually `user`, `validation`, or `product_spec`) and add 2\u20133 anchor entities to bridge.",
26401
- stages: ["validation", "build", "beta", "launch"],
26442
+ // Fires from beta onward. Early on a graph is legitimately deep in one domain
26443
+ // (a discovery notebook, a thin internal tool); only once it reaches beta is
26444
+ // single-domain coverage a smell rather than a normal starting shape. Extends
26445
+ // through growth and mature, where a single-domain graph is most telling.
26446
+ stages: ["beta", "launch", "growth", "mature"],
26402
26447
  severity: "medium",
26403
26448
  source: { kind: "fundamental" }
26404
26449
  },
@@ -29796,7 +29841,7 @@ function isCanonical(text) {
29796
29841
  }
29797
29842
 
29798
29843
  // src/index.ts
29799
- var UPG_VERSION = "0.17.0";
29844
+ var UPG_VERSION = "0.17.2";
29800
29845
  var UPG_FORMAT_VERSION = "0.4.0";
29801
29846
  var MARKDOWN_FORMAT_VERSION = "0.1";
29802
29847
  var UPG_TYPES = getTypes();
@@ -29929,6 +29974,7 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
29929
29974
  UPG_MIGRATIONS,
29930
29975
  UPG_PLAYBOOKS,
29931
29976
  UPG_POLYMORPHIC_EDGE_KEYS,
29977
+ UPG_PORTFOLIO_KINDS,
29932
29978
  UPG_PRODUCT_STAGES,
29933
29979
  UPG_PRODUCT_STAGE_COERCION_MAP,
29934
29980
  UPG_PROPERTY_MIGRATIONS,