@unified-product-graph/core 0.16.2 → 0.17.1

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.js CHANGED
@@ -3174,9 +3174,13 @@ var UPG_CROSS_EDGE_TYPES = [
3174
3174
  "screen_targets_competitor",
3175
3175
  "feature_surfaces_product",
3176
3176
  "feature_uses_design_component",
3177
- "product_implements_design_system"
3177
+ "product_implements_design_system",
3178
+ // Org-link (0.17.0): dual-registered ownership edges (see UPGCrossEdgeType above).
3179
+ "node_owned_by_team",
3180
+ "node_owned_by_department"
3178
3181
  ];
3179
3182
  var REGISTRY_PRODUCT_ID = "registry";
3183
+ var UPG_PORTFOLIO_KINDS = ["owned", "watched", "strategic", "internal", "gtm"];
3180
3184
 
3181
3185
  // src/registry/entity-type-resolution.ts
3182
3186
  var TYPES = getTypes();
@@ -23369,7 +23373,8 @@ var TEAM_ORG_GUIDE = {
23369
23373
  anti_patterns: [
23370
23374
  { description: "Teams without OKRs: every team needs clear goals" },
23371
23375
  { description: "Dependencies without both teams linked: a dependency must connect blocker and blocked" },
23372
- { description: "Retrospectives without action items: reflection without action is just venting" }
23376
+ { description: "Retrospectives without action items: reflection without action is just venting" },
23377
+ { 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." }
23373
23378
  ]
23374
23379
  };
23375
23380
  var PROGRAM_MGMT_GUIDE = {
@@ -23478,7 +23483,8 @@ var PORTFOLIO_GUIDE = {
23478
23483
  ],
23479
23484
  anti_patterns: [
23480
23485
  { description: "Products without a product area: every product should be classified" },
23481
- { description: "Portfolios without strategic alignment: portfolio decisions should reflect strategy" }
23486
+ { description: "Portfolios without strategic alignment: portfolio decisions should reflect strategy" },
23487
+ { 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." }
23482
23488
  ]
23483
23489
  };
23484
23490
  var WORKSPACE_GUIDE = {
@@ -26295,6 +26301,54 @@ var UPG_ANTI_PATTERNS = [
26295
26301
  severity: "medium",
26296
26302
  source: { kind: "practitioner", attribution: "Marty Cagan, Inspired (product discovery)" }
26297
26303
  },
26304
+ // ── Operating-function layer (0.17.0): member_kind: operating_function ─────
26305
+ // Carry concern 'operating' (UPG_ANTI_PATTERN_CONCERNS) and are evaluated ONLY
26306
+ // for operating_function graphs — a function a team operates (revenue / success
26307
+ // / finance / people / marketing), not a product it ships. The product-spine
26308
+ // patterns above are not evaluated for that kind (category errors); these assert
26309
+ // the function spine instead.
26310
+ {
26311
+ id: "operating-function-without-north-star",
26312
+ since: "0.17.0",
26313
+ name: "Operating function without a north-star metric",
26314
+ description: "An operating function graph has real content but no north-star metric to operate toward. A function (sales, finance, people, marketing) is run against one headline number it moves and that rolls up to the company tree. With no north-star metric the function has direction but nothing to steer by.",
26315
+ structured_condition: {
26316
+ operator: "and",
26317
+ checks: [
26318
+ { check: { type: "total_entity_count", comparison: "gt", threshold: 3 } },
26319
+ { check: { type: "entity_count", entity_type: "metric", filter: { property: "designation", value: "north_star" }, comparison: "zero" } }
26320
+ ]
26321
+ },
26322
+ why_it_matters: "A function with no north-star metric cannot be steered, prioritised, or rolled into the company metric tree. Operating work becomes activity without a measured target.",
26323
+ remediation: "Add the one `metric` the function operates toward and mark it `designation: north_star`, then wire it to the company metric tree via `metric_decomposes_into_metric`.",
26324
+ stages: ["concept", "validation", "build", "beta", "launch", "growth", "mature", "maintenance", "sunset"],
26325
+ severity: "high",
26326
+ source: { kind: "fundamental" }
26327
+ },
26328
+ {
26329
+ id: "operating-function-without-operating-content",
26330
+ since: "0.17.0",
26331
+ name: "Operating function without operating content",
26332
+ description: "An operating function graph has more than a few entities but none in any operating domain (sales, go-to-market, customer success, growth, marketing, business model, pricing). It carries direction and people but no operating substance: the work the function actually runs.",
26333
+ structured_condition: {
26334
+ operator: "and",
26335
+ checks: [
26336
+ { check: { type: "total_entity_count", comparison: "gt", threshold: 3 } },
26337
+ { check: { type: "domain_population", domain_id: "sales", comparison: "zero" } },
26338
+ { check: { type: "domain_population", domain_id: "go_to_market", comparison: "zero" } },
26339
+ { check: { type: "domain_population", domain_id: "customer_success", comparison: "zero" } },
26340
+ { check: { type: "domain_population", domain_id: "growth", comparison: "zero" } },
26341
+ { check: { type: "domain_population", domain_id: "marketing", comparison: "zero" } },
26342
+ { check: { type: "domain_population", domain_id: "business_model", comparison: "zero" } },
26343
+ { check: { type: "domain_population", domain_id: "pricing", comparison: "zero" } }
26344
+ ]
26345
+ },
26346
+ why_it_matters: "A function graph that is all strategy and org chart with no operating domain is a stub: it states intent but models none of the work the function runs, so it answers no operating questions.",
26347
+ remediation: "Populate at least one operating domain appropriate to the function (e.g. Field \u2192 `sales` / `go_to_market` / `customer_success`; Finance \u2192 `business_model` / `pricing`). Start from the matching template via `/upg-new-from-template`.",
26348
+ stages: ["concept", "validation", "build", "beta", "launch", "growth", "mature", "maintenance", "sunset"],
26349
+ severity: "medium",
26350
+ source: { kind: "fundamental" }
26351
+ },
26298
26352
  // ── Foundations layer (0.9.13): portfolio-scoped, registry-aware ──────────
26299
26353
  // These read the shared registry + cross-product edges, so they are evaluated
26300
26354
  // by portfolio_validate, not the single-graph evaluator. They carry no
@@ -26335,6 +26389,23 @@ var UPG_ANTI_PATTERNS = [
26335
26389
  stages: ["build", "beta", "launch", "growth", "mature"],
26336
26390
  severity: "low",
26337
26391
  source: { kind: "fundamental" }
26392
+ },
26393
+ // ── Operating-function org link (0.17.0): portfolio-scoped ─────────────────
26394
+ // Cross-product detector (in portfolio_validate): an operating_function graph
26395
+ // should reference the org unit it operates under, which lives once in the
26396
+ // rollup's team_org map. Carries no structured_condition — the org link is a
26397
+ // cross-product edge in portfolio.upg, not a single-graph shape.
26398
+ {
26399
+ id: "operating-function-without-org-link",
26400
+ since: "0.17.0",
26401
+ scope: "portfolio",
26402
+ name: "Operating function without an org link",
26403
+ description: "An operating_function graph does not reference the org unit it operates under. The department/team hierarchy lives once in the rollup (org_rollup) as team_org entities; a function should point its spine at the department or team that owns it (node_owned_by_department / node_owned_by_team), so the operating layer hangs off the canonical org map rather than re-stating it.",
26404
+ why_it_matters: "A function with no org link floats free of the org chart: its work cannot be rolled up by department, and the single source of truth for who owns what is bypassed.",
26405
+ remediation: "Add a cross-product node_owned_by_department (or node_owned_by_team) edge from the function spine to its department/team in the rollup. Mint the org unit once in the org_rollup graph; functions reference it.",
26406
+ stages: ["concept", "validation", "build", "beta", "launch", "growth", "mature", "maintenance", "sunset"],
26407
+ severity: "medium",
26408
+ source: { kind: "fundamental" }
26338
26409
  }
26339
26410
  ];
26340
26411
  function getAntiPatternById(id) {
@@ -26347,6 +26418,45 @@ function getAntiPatternsBySeverity(severity) {
26347
26418
  return UPG_ANTI_PATTERNS.filter((ap) => ap.severity === severity);
26348
26419
  }
26349
26420
 
26421
+ // src/intelligence/validation-profiles.ts
26422
+ var UPG_ANTI_PATTERN_CONCERNS = {
26423
+ // universal — kind-independent graph hygiene + strategy discipline
26424
+ "objective-without-key-results": "universal",
26425
+ "single-domain-graph": "universal",
26426
+ "orphan-loose-thoughts": "universal",
26427
+ // operating — only evaluated for operating_function graphs
26428
+ "operating-function-without-north-star": "operating",
26429
+ "operating-function-without-operating-content": "operating"
26430
+ // everything else defaults to 'product_spine'
26431
+ };
26432
+ function concernFor(antiPatternId) {
26433
+ return UPG_ANTI_PATTERN_CONCERNS[antiPatternId] ?? "product_spine";
26434
+ }
26435
+ var UPG_VALIDATION_PROFILES = {
26436
+ product: { evaluate_concerns: ["product_spine", "universal"], gating_concerns: ["product_spine", "universal"] },
26437
+ watched: { evaluate_concerns: ["product_spine", "universal"], gating_concerns: [] },
26438
+ org_rollup: { evaluate_concerns: ["product_spine", "universal"], gating_concerns: ["universal"] },
26439
+ operating_function: { evaluate_concerns: ["universal", "operating"], gating_concerns: ["universal", "operating"] }
26440
+ };
26441
+ function validationProfileFor(kind) {
26442
+ return UPG_VALIDATION_PROFILES[kind ?? "product"] ?? UPG_VALIDATION_PROFILES.product;
26443
+ }
26444
+ function concernEvaluatedFor(kind, concern) {
26445
+ return validationProfileFor(kind).evaluate_concerns.includes(concern);
26446
+ }
26447
+ function concernGatesFor(kind, concern) {
26448
+ return validationProfileFor(kind).gating_concerns.includes(concern);
26449
+ }
26450
+ var COVERAGE_ANTI_PATTERNS = /* @__PURE__ */ new Set([
26451
+ "single-domain-graph",
26452
+ "persona-count-below-stage-benchmark",
26453
+ "competitors-missing-past-validation"
26454
+ ]);
26455
+ var THIN_GRAPH_THRESHOLD = 8;
26456
+ function isThinCoverageAdvisory(antiPatternId, totalEntityCount) {
26457
+ return COVERAGE_ANTI_PATTERNS.has(antiPatternId) && totalEntityCount < THIN_GRAPH_THRESHOLD;
26458
+ }
26459
+
26350
26460
  // src/intelligence/evaluator.ts
26351
26461
  var SEVERITY_ORDER = {
26352
26462
  high: 0,
@@ -26364,8 +26474,10 @@ function evaluateAntiPatterns(inputs, options) {
26364
26474
  if (inputs.productStage && !ap.stages.includes(inputs.productStage)) {
26365
26475
  continue;
26366
26476
  }
26477
+ const concern = concernFor(ap.id);
26478
+ if (!concernEvaluatedFor(inputs.memberKind, concern)) continue;
26367
26479
  if (evaluateCondition(ap.structured_condition, inputs)) {
26368
- fires.push(buildViolation(ap));
26480
+ fires.push(buildViolation(ap, concern));
26369
26481
  }
26370
26482
  }
26371
26483
  fires.sort((a, b) => {
@@ -26434,11 +26546,12 @@ function compareNumber(value, comparison, threshold) {
26434
26546
  }
26435
26547
  }
26436
26548
  function evaluateEntityCount(check, inputs) {
26437
- const statusFilter = check.filter && typeof check.filter.status === "string" ? check.filter.status : void 0;
26549
+ const filter = check.filter;
26438
26550
  let count = 0;
26439
- if (statusFilter) {
26440
- const byStatus = inputs.countsByTypeAndStatus?.[check.entity_type];
26441
- count = byStatus?.[statusFilter] ?? 0;
26551
+ if (filter && typeof filter.property === "string" && typeof filter.value === "string") {
26552
+ count = inputs.countsByTypeAndProperty?.[check.entity_type]?.[filter.property]?.[filter.value] ?? 0;
26553
+ } else if (filter && typeof filter.status === "string") {
26554
+ count = inputs.countsByTypeAndStatus?.[check.entity_type]?.[filter.status] ?? 0;
26442
26555
  } else {
26443
26556
  count = inputs.countsByType[check.entity_type] ?? 0;
26444
26557
  }
@@ -26505,11 +26618,12 @@ function evaluateDomainPopulation(check, inputs) {
26505
26618
  function evaluateOrphanCount(check, inputs) {
26506
26619
  return compareNumber(inputs.orphanCount, check.comparison, check.threshold);
26507
26620
  }
26508
- function buildViolation(ap) {
26621
+ function buildViolation(ap, concern) {
26509
26622
  return {
26510
26623
  anti_pattern_id: ap.id,
26511
26624
  name: ap.name,
26512
26625
  severity: ap.severity,
26626
+ concern,
26513
26627
  target_entities: collectTargetEntities(ap.structured_condition),
26514
26628
  description: ap.description,
26515
26629
  why_it_matters: ap.why_it_matters,
@@ -29337,7 +29451,7 @@ function serializeSingleWithHeader(doc, opts) {
29337
29451
  const summary = deriveSummary(product.description);
29338
29452
  if (summary) header.summary = summary;
29339
29453
  header.counts = { nodes: doc.nodes?.length ?? 0, edges: doc.edges?.length ?? 0 };
29340
- if (doc.member_kind === "org_rollup" || doc.member_kind === "watched") {
29454
+ if (doc.member_kind === "org_rollup" || doc.member_kind === "watched" || doc.member_kind === "operating_function") {
29341
29455
  header.member_kind = doc.member_kind;
29342
29456
  }
29343
29457
  header.provenance = buildProvenance(doc, opts);
@@ -29363,10 +29477,12 @@ function serializePortfolioWithHeader(doc, opts) {
29363
29477
  const memberKindOf = (p) => p.member_kind ?? "product";
29364
29478
  const watchedCount = members.filter((p) => memberKindOf(p) === "watched").length;
29365
29479
  const rollupCount = members.filter((p) => memberKindOf(p) === "org_rollup").length;
29480
+ const operatingFunctionCount = members.filter((p) => memberKindOf(p) === "operating_function").length;
29366
29481
  header.counts = {
29367
29482
  products: members.filter((p) => memberKindOf(p) === "product").length,
29368
29483
  ...watchedCount > 0 ? { watched_products: watchedCount } : {},
29369
29484
  ...rollupCount > 0 ? { org_rollups: rollupCount } : {},
29485
+ ...operatingFunctionCount > 0 ? { operating_functions: operatingFunctionCount } : {},
29370
29486
  product_areas: doc.product_areas?.length ?? 0,
29371
29487
  portfolios: doc.portfolios?.length ?? 0,
29372
29488
  cross_edges: doc.cross_edges?.length ?? 0
@@ -29425,7 +29541,9 @@ function normalizeDocument(obj) {
29425
29541
  edges: raw.edges ?? []
29426
29542
  };
29427
29543
  const memberKind = header?.member_kind ?? raw.member_kind;
29428
- if (memberKind === "org_rollup" || memberKind === "watched") out.member_kind = memberKind;
29544
+ if (memberKind === "org_rollup" || memberKind === "watched" || memberKind === "operating_function") {
29545
+ out.member_kind = memberKind;
29546
+ }
29429
29547
  if (_integrity) out._integrity = _integrity;
29430
29548
  return out;
29431
29549
  }
@@ -29438,7 +29556,7 @@ function isCanonical(text) {
29438
29556
  }
29439
29557
 
29440
29558
  // src/index.ts
29441
- var UPG_VERSION = "0.16.2";
29559
+ var UPG_VERSION = "0.17.1";
29442
29560
  var UPG_FORMAT_VERSION = "0.4.0";
29443
29561
  var MARKDOWN_FORMAT_VERSION = "0.1";
29444
29562
  var UPG_TYPES = getTypes();
@@ -29508,6 +29626,7 @@ export {
29508
29626
  BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK,
29509
29627
  CLASSIFICATION_EDGE_PROPERTY_SCHEMA,
29510
29628
  CONTENT_DEPTH_WARNING_RULES,
29629
+ COVERAGE_ANTI_PATTERNS,
29511
29630
  DEFAULT_ENTITY_EMOJI,
29512
29631
  DEFERRED_PROPOSED_BY_CONTRACT,
29513
29632
  DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK,
@@ -29527,8 +29646,10 @@ export {
29527
29646
  REFLECT_MODES,
29528
29647
  REGISTRY_PRODUCT_ID,
29529
29648
  STRATEGY_OUTCOMES_PLAYBOOK,
29649
+ THIN_GRAPH_THRESHOLD,
29530
29650
  UPG_ACTIVE_TYPES,
29531
29651
  UPG_ANTI_PATTERNS,
29652
+ UPG_ANTI_PATTERN_CONCERNS,
29532
29653
  UPG_APPROACHES,
29533
29654
  UPG_APPROACHES_BY_ID,
29534
29655
  UPG_AREA_TAXONOMY,
@@ -29567,6 +29688,7 @@ export {
29567
29688
  UPG_MIGRATIONS,
29568
29689
  UPG_PLAYBOOKS,
29569
29690
  UPG_POLYMORPHIC_EDGE_KEYS,
29691
+ UPG_PORTFOLIO_KINDS,
29570
29692
  UPG_PRODUCT_STAGES,
29571
29693
  UPG_PRODUCT_STAGE_COERCION_MAP,
29572
29694
  UPG_PROPERTY_MIGRATIONS,
@@ -29589,6 +29711,7 @@ export {
29589
29711
  UPG_TYPE_LABELS,
29590
29712
  UPG_TYPE_LABELS_MAP,
29591
29713
  UPG_TYPE_NAMES,
29714
+ UPG_VALIDATION_PROFILES,
29592
29715
  UPG_VALID_CHILDREN,
29593
29716
  UPG_VERSION,
29594
29717
  UPG_WILDCARD_ENDPOINT,
@@ -29601,6 +29724,9 @@ export {
29601
29724
  collectSlugsForType,
29602
29725
  compareVersions,
29603
29726
  computeBodyChecksum,
29727
+ concernEvaluatedFor,
29728
+ concernFor,
29729
+ concernGatesFor,
29604
29730
  describeTreePattern,
29605
29731
  edgeCarriesProperties,
29606
29732
  evaluateAntiPatterns,
@@ -29675,6 +29801,7 @@ export {
29675
29801
  isRegisteredPolymorphicEdge,
29676
29802
  isRuntimeStateShapedProperty,
29677
29803
  isSubSequenceStep,
29804
+ isThinCoverageAdvisory,
29678
29805
  isUPGDocument,
29679
29806
  listPropertiesByModifier,
29680
29807
  listStatusMigrations,
@@ -29700,6 +29827,7 @@ export {
29700
29827
  validateProductStageStrict,
29701
29828
  validateUPGDocument,
29702
29829
  validateUPGFramework,
29830
+ validationProfileFor,
29703
29831
  walkMigrationChainToCanonical
29704
29832
  };
29705
29833
  //# sourceMappingURL=index.js.map