@unified-product-graph/core 0.17.1 → 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
@@ -2312,6 +2312,16 @@ var UPG_EDGE_CATALOG = {
2312
2312
  // UPG_EDGE_MIGRATIONS['0.9.9'].
2313
2313
  product_organised_into_department: { forward_verb: "organised_into", reverse_verb: "organises", classification: "hierarchy", source_type: "product", target_type: "department" },
2314
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" },
2315
2325
  department_includes_stakeholder: { forward_verb: "includes", reverse_verb: "included_in", classification: "hierarchy", source_type: "department", target_type: "stakeholder" },
2316
2326
  // NOTE: `person` is a containment-free entity type (see
2317
2327
  // UPG_CONTAINMENT_FREE_TYPES in grammar/hierarchy.ts). It is referenced
@@ -3421,7 +3431,13 @@ var UPG_CROSS_EDGE_TYPES = [
3421
3431
  "product_implements_design_system",
3422
3432
  // Org-link (0.17.0): dual-registered ownership edges (see UPGCrossEdgeType above).
3423
3433
  "node_owned_by_team",
3424
- "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"
3425
3441
  ];
3426
3442
  var REGISTRY_PRODUCT_ID = "registry";
3427
3443
  var UPG_PORTFOLIO_KINDS = ["owned", "watched", "strategic", "internal", "gtm"];
@@ -3875,7 +3891,11 @@ var UPG_VALID_CHILDREN = {
3875
3891
  "skill",
3876
3892
  "ceremony",
3877
3893
  "capacity_plan",
3878
- "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"
3879
3899
  ],
3880
3900
  // ── Data & Analytics hierarchy ───────────────────────────────────────────────
3881
3901
  data_source: ["metric", "data_pipeline", "data_lineage", "event_schema"],
@@ -10868,6 +10888,7 @@ var UPG_PROPERTY_SCHEMA = {
10868
10888
  // ConstraintProperties: Constraint: a named limitation or boundary on product creation.
10869
10889
  constraint: {
10870
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)." },
10871
10892
  constraint_status: { type: "string", enum: ["binding", "advisory", "lifted"], description: "Whether binding, advisory, or lifted" },
10872
10893
  rule_strength: { type: "string", enum: ["must", "must_not", "exception", "warning", "guideline"], description: "Enforcement strictness. Reuses the governance/guideline rule vocabulary." },
10873
10894
  source: { type: "string", description: "Free-text origin: policy document, regulation, stakeholder, technical doc." },
@@ -23057,7 +23078,8 @@ var STRATEGY_GUIDE = {
23057
23078
  anti_patterns: [
23058
23079
  { description: "Outcomes without metrics. Measurement is the signal an outcome happened." },
23059
23080
  { 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" }
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)." }
23061
23083
  ]
23062
23084
  };
23063
23085
  var PRODUCT_SPEC_GUIDE = {
@@ -23608,6 +23630,12 @@ var TEAM_ORG_GUIDE = {
23608
23630
  description: "Teams are staffed with roles, target OKRs, reflect in retrospectives, and are blocked by dependencies",
23609
23631
  entity_types: ["team", "role", "team_okr", "retrospective", "dependency"],
23610
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"]
23611
23639
  }
23612
23640
  ],
23613
23641
  required_bridges: [
@@ -23618,6 +23646,7 @@ var TEAM_ORG_GUIDE = {
23618
23646
  { description: "Teams without OKRs: every team needs clear goals" },
23619
23647
  { description: "Dependencies without both teams linked: a dependency must connect blocker and blocked" },
23620
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." },
23621
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." }
23622
23651
  ]
23623
23652
  };
@@ -26340,7 +26369,7 @@ var UPG_ANTI_PATTERNS = [
26340
26369
  {
26341
26370
  id: "competitors-missing-past-validation",
26342
26371
  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.",
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.",
26344
26373
  structured_condition: {
26345
26374
  check: {
26346
26375
  type: "benchmark",
@@ -26350,14 +26379,18 @@ var UPG_ANTI_PATTERNS = [
26350
26379
  },
26351
26380
  why_it_matters: "Without competitors in the graph, positioning, differentiation, and switching-cost analysis lack referents. The team is reasoning in a vacuum.",
26352
26381
  remediation: "Catalogue the 3\u20135 closest alternatives users would pick today. Use `/upg-compete` to author.",
26353
- 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"],
26354
26387
  severity: "medium",
26355
26388
  source: { kind: "industry_practice", category: "product_strategy" }
26356
26389
  },
26357
26390
  {
26358
26391
  id: "persona-count-below-stage-benchmark",
26359
26392
  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.",
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.",
26361
26394
  structured_condition: {
26362
26395
  check: {
26363
26396
  type: "benchmark",
@@ -26365,9 +26398,13 @@ var UPG_ANTI_PATTERNS = [
26365
26398
  comparison: "below_min"
26366
26399
  }
26367
26400
  },
26368
- 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.",
26369
26402
  remediation: "Add personas representing the next 1\u20132 most distinct user segments. Use `/upg-new-persona`.",
26370
- 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"],
26371
26408
  severity: "medium",
26372
26409
  source: { kind: "practitioner", attribution: "Alan Cooper, The Inmates Are Running the Asylum" }
26373
26410
  },
@@ -26392,7 +26429,7 @@ var UPG_ANTI_PATTERNS = [
26392
26429
  {
26393
26430
  id: "single-domain-graph",
26394
26431
  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.",
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.",
26396
26433
  structured_condition: {
26397
26434
  operator: "and",
26398
26435
  checks: [
@@ -26402,7 +26439,11 @@ var UPG_ANTI_PATTERNS = [
26402
26439
  },
26403
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?".',
26404
26441
  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"],
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"],
26406
26447
  severity: "medium",
26407
26448
  source: { kind: "fundamental" }
26408
26449
  },
@@ -29800,7 +29841,7 @@ function isCanonical(text) {
29800
29841
  }
29801
29842
 
29802
29843
  // src/index.ts
29803
- var UPG_VERSION = "0.17.1";
29844
+ var UPG_VERSION = "0.17.2";
29804
29845
  var UPG_FORMAT_VERSION = "0.4.0";
29805
29846
  var MARKDOWN_FORMAT_VERSION = "0.1";
29806
29847
  var UPG_TYPES = getTypes();