@unified-product-graph/core 0.16.2 → 0.17.0

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,7 +3174,10 @@ 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";
3180
3183
 
@@ -26295,6 +26298,54 @@ var UPG_ANTI_PATTERNS = [
26295
26298
  severity: "medium",
26296
26299
  source: { kind: "practitioner", attribution: "Marty Cagan, Inspired (product discovery)" }
26297
26300
  },
26301
+ // ── Operating-function layer (0.17.0): member_kind: operating_function ─────
26302
+ // Carry concern 'operating' (UPG_ANTI_PATTERN_CONCERNS) and are evaluated ONLY
26303
+ // for operating_function graphs — a function a team operates (revenue / success
26304
+ // / finance / people / marketing), not a product it ships. The product-spine
26305
+ // patterns above are not evaluated for that kind (category errors); these assert
26306
+ // the function spine instead.
26307
+ {
26308
+ id: "operating-function-without-north-star",
26309
+ since: "0.17.0",
26310
+ name: "Operating function without a north-star metric",
26311
+ 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.",
26312
+ structured_condition: {
26313
+ operator: "and",
26314
+ checks: [
26315
+ { check: { type: "total_entity_count", comparison: "gt", threshold: 3 } },
26316
+ { check: { type: "entity_count", entity_type: "metric", filter: { property: "designation", value: "north_star" }, comparison: "zero" } }
26317
+ ]
26318
+ },
26319
+ 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.",
26320
+ 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`.",
26321
+ stages: ["concept", "validation", "build", "beta", "launch", "growth", "mature", "maintenance", "sunset"],
26322
+ severity: "high",
26323
+ source: { kind: "fundamental" }
26324
+ },
26325
+ {
26326
+ id: "operating-function-without-operating-content",
26327
+ since: "0.17.0",
26328
+ name: "Operating function without operating content",
26329
+ 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.",
26330
+ structured_condition: {
26331
+ operator: "and",
26332
+ checks: [
26333
+ { check: { type: "total_entity_count", comparison: "gt", threshold: 3 } },
26334
+ { check: { type: "domain_population", domain_id: "sales", comparison: "zero" } },
26335
+ { check: { type: "domain_population", domain_id: "go_to_market", comparison: "zero" } },
26336
+ { check: { type: "domain_population", domain_id: "customer_success", comparison: "zero" } },
26337
+ { check: { type: "domain_population", domain_id: "growth", comparison: "zero" } },
26338
+ { check: { type: "domain_population", domain_id: "marketing", comparison: "zero" } },
26339
+ { check: { type: "domain_population", domain_id: "business_model", comparison: "zero" } },
26340
+ { check: { type: "domain_population", domain_id: "pricing", comparison: "zero" } }
26341
+ ]
26342
+ },
26343
+ 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.",
26344
+ 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`.",
26345
+ stages: ["concept", "validation", "build", "beta", "launch", "growth", "mature", "maintenance", "sunset"],
26346
+ severity: "medium",
26347
+ source: { kind: "fundamental" }
26348
+ },
26298
26349
  // ── Foundations layer (0.9.13): portfolio-scoped, registry-aware ──────────
26299
26350
  // These read the shared registry + cross-product edges, so they are evaluated
26300
26351
  // by portfolio_validate, not the single-graph evaluator. They carry no
@@ -26335,6 +26386,23 @@ var UPG_ANTI_PATTERNS = [
26335
26386
  stages: ["build", "beta", "launch", "growth", "mature"],
26336
26387
  severity: "low",
26337
26388
  source: { kind: "fundamental" }
26389
+ },
26390
+ // ── Operating-function org link (0.17.0): portfolio-scoped ─────────────────
26391
+ // Cross-product detector (in portfolio_validate): an operating_function graph
26392
+ // should reference the org unit it operates under, which lives once in the
26393
+ // rollup's team_org map. Carries no structured_condition — the org link is a
26394
+ // cross-product edge in portfolio.upg, not a single-graph shape.
26395
+ {
26396
+ id: "operating-function-without-org-link",
26397
+ since: "0.17.0",
26398
+ scope: "portfolio",
26399
+ name: "Operating function without an org link",
26400
+ 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.",
26401
+ 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.",
26402
+ 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.",
26403
+ stages: ["concept", "validation", "build", "beta", "launch", "growth", "mature", "maintenance", "sunset"],
26404
+ severity: "medium",
26405
+ source: { kind: "fundamental" }
26338
26406
  }
26339
26407
  ];
26340
26408
  function getAntiPatternById(id) {
@@ -26347,6 +26415,45 @@ function getAntiPatternsBySeverity(severity) {
26347
26415
  return UPG_ANTI_PATTERNS.filter((ap) => ap.severity === severity);
26348
26416
  }
26349
26417
 
26418
+ // src/intelligence/validation-profiles.ts
26419
+ var UPG_ANTI_PATTERN_CONCERNS = {
26420
+ // universal — kind-independent graph hygiene + strategy discipline
26421
+ "objective-without-key-results": "universal",
26422
+ "single-domain-graph": "universal",
26423
+ "orphan-loose-thoughts": "universal",
26424
+ // operating — only evaluated for operating_function graphs
26425
+ "operating-function-without-north-star": "operating",
26426
+ "operating-function-without-operating-content": "operating"
26427
+ // everything else defaults to 'product_spine'
26428
+ };
26429
+ function concernFor(antiPatternId) {
26430
+ return UPG_ANTI_PATTERN_CONCERNS[antiPatternId] ?? "product_spine";
26431
+ }
26432
+ var UPG_VALIDATION_PROFILES = {
26433
+ product: { evaluate_concerns: ["product_spine", "universal"], gating_concerns: ["product_spine", "universal"] },
26434
+ watched: { evaluate_concerns: ["product_spine", "universal"], gating_concerns: [] },
26435
+ org_rollup: { evaluate_concerns: ["product_spine", "universal"], gating_concerns: ["universal"] },
26436
+ operating_function: { evaluate_concerns: ["universal", "operating"], gating_concerns: ["universal", "operating"] }
26437
+ };
26438
+ function validationProfileFor(kind) {
26439
+ return UPG_VALIDATION_PROFILES[kind ?? "product"] ?? UPG_VALIDATION_PROFILES.product;
26440
+ }
26441
+ function concernEvaluatedFor(kind, concern) {
26442
+ return validationProfileFor(kind).evaluate_concerns.includes(concern);
26443
+ }
26444
+ function concernGatesFor(kind, concern) {
26445
+ return validationProfileFor(kind).gating_concerns.includes(concern);
26446
+ }
26447
+ var COVERAGE_ANTI_PATTERNS = /* @__PURE__ */ new Set([
26448
+ "single-domain-graph",
26449
+ "persona-count-below-stage-benchmark",
26450
+ "competitors-missing-past-validation"
26451
+ ]);
26452
+ var THIN_GRAPH_THRESHOLD = 8;
26453
+ function isThinCoverageAdvisory(antiPatternId, totalEntityCount) {
26454
+ return COVERAGE_ANTI_PATTERNS.has(antiPatternId) && totalEntityCount < THIN_GRAPH_THRESHOLD;
26455
+ }
26456
+
26350
26457
  // src/intelligence/evaluator.ts
26351
26458
  var SEVERITY_ORDER = {
26352
26459
  high: 0,
@@ -26364,8 +26471,10 @@ function evaluateAntiPatterns(inputs, options) {
26364
26471
  if (inputs.productStage && !ap.stages.includes(inputs.productStage)) {
26365
26472
  continue;
26366
26473
  }
26474
+ const concern = concernFor(ap.id);
26475
+ if (!concernEvaluatedFor(inputs.memberKind, concern)) continue;
26367
26476
  if (evaluateCondition(ap.structured_condition, inputs)) {
26368
- fires.push(buildViolation(ap));
26477
+ fires.push(buildViolation(ap, concern));
26369
26478
  }
26370
26479
  }
26371
26480
  fires.sort((a, b) => {
@@ -26434,11 +26543,12 @@ function compareNumber(value, comparison, threshold) {
26434
26543
  }
26435
26544
  }
26436
26545
  function evaluateEntityCount(check, inputs) {
26437
- const statusFilter = check.filter && typeof check.filter.status === "string" ? check.filter.status : void 0;
26546
+ const filter = check.filter;
26438
26547
  let count = 0;
26439
- if (statusFilter) {
26440
- const byStatus = inputs.countsByTypeAndStatus?.[check.entity_type];
26441
- count = byStatus?.[statusFilter] ?? 0;
26548
+ if (filter && typeof filter.property === "string" && typeof filter.value === "string") {
26549
+ count = inputs.countsByTypeAndProperty?.[check.entity_type]?.[filter.property]?.[filter.value] ?? 0;
26550
+ } else if (filter && typeof filter.status === "string") {
26551
+ count = inputs.countsByTypeAndStatus?.[check.entity_type]?.[filter.status] ?? 0;
26442
26552
  } else {
26443
26553
  count = inputs.countsByType[check.entity_type] ?? 0;
26444
26554
  }
@@ -26505,11 +26615,12 @@ function evaluateDomainPopulation(check, inputs) {
26505
26615
  function evaluateOrphanCount(check, inputs) {
26506
26616
  return compareNumber(inputs.orphanCount, check.comparison, check.threshold);
26507
26617
  }
26508
- function buildViolation(ap) {
26618
+ function buildViolation(ap, concern) {
26509
26619
  return {
26510
26620
  anti_pattern_id: ap.id,
26511
26621
  name: ap.name,
26512
26622
  severity: ap.severity,
26623
+ concern,
26513
26624
  target_entities: collectTargetEntities(ap.structured_condition),
26514
26625
  description: ap.description,
26515
26626
  why_it_matters: ap.why_it_matters,
@@ -29337,7 +29448,7 @@ function serializeSingleWithHeader(doc, opts) {
29337
29448
  const summary = deriveSummary(product.description);
29338
29449
  if (summary) header.summary = summary;
29339
29450
  header.counts = { nodes: doc.nodes?.length ?? 0, edges: doc.edges?.length ?? 0 };
29340
- if (doc.member_kind === "org_rollup" || doc.member_kind === "watched") {
29451
+ if (doc.member_kind === "org_rollup" || doc.member_kind === "watched" || doc.member_kind === "operating_function") {
29341
29452
  header.member_kind = doc.member_kind;
29342
29453
  }
29343
29454
  header.provenance = buildProvenance(doc, opts);
@@ -29363,10 +29474,12 @@ function serializePortfolioWithHeader(doc, opts) {
29363
29474
  const memberKindOf = (p) => p.member_kind ?? "product";
29364
29475
  const watchedCount = members.filter((p) => memberKindOf(p) === "watched").length;
29365
29476
  const rollupCount = members.filter((p) => memberKindOf(p) === "org_rollup").length;
29477
+ const operatingFunctionCount = members.filter((p) => memberKindOf(p) === "operating_function").length;
29366
29478
  header.counts = {
29367
29479
  products: members.filter((p) => memberKindOf(p) === "product").length,
29368
29480
  ...watchedCount > 0 ? { watched_products: watchedCount } : {},
29369
29481
  ...rollupCount > 0 ? { org_rollups: rollupCount } : {},
29482
+ ...operatingFunctionCount > 0 ? { operating_functions: operatingFunctionCount } : {},
29370
29483
  product_areas: doc.product_areas?.length ?? 0,
29371
29484
  portfolios: doc.portfolios?.length ?? 0,
29372
29485
  cross_edges: doc.cross_edges?.length ?? 0
@@ -29425,7 +29538,9 @@ function normalizeDocument(obj) {
29425
29538
  edges: raw.edges ?? []
29426
29539
  };
29427
29540
  const memberKind = header?.member_kind ?? raw.member_kind;
29428
- if (memberKind === "org_rollup" || memberKind === "watched") out.member_kind = memberKind;
29541
+ if (memberKind === "org_rollup" || memberKind === "watched" || memberKind === "operating_function") {
29542
+ out.member_kind = memberKind;
29543
+ }
29429
29544
  if (_integrity) out._integrity = _integrity;
29430
29545
  return out;
29431
29546
  }
@@ -29438,7 +29553,7 @@ function isCanonical(text) {
29438
29553
  }
29439
29554
 
29440
29555
  // src/index.ts
29441
- var UPG_VERSION = "0.16.2";
29556
+ var UPG_VERSION = "0.17.0";
29442
29557
  var UPG_FORMAT_VERSION = "0.4.0";
29443
29558
  var MARKDOWN_FORMAT_VERSION = "0.1";
29444
29559
  var UPG_TYPES = getTypes();
@@ -29508,6 +29623,7 @@ export {
29508
29623
  BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK,
29509
29624
  CLASSIFICATION_EDGE_PROPERTY_SCHEMA,
29510
29625
  CONTENT_DEPTH_WARNING_RULES,
29626
+ COVERAGE_ANTI_PATTERNS,
29511
29627
  DEFAULT_ENTITY_EMOJI,
29512
29628
  DEFERRED_PROPOSED_BY_CONTRACT,
29513
29629
  DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK,
@@ -29527,8 +29643,10 @@ export {
29527
29643
  REFLECT_MODES,
29528
29644
  REGISTRY_PRODUCT_ID,
29529
29645
  STRATEGY_OUTCOMES_PLAYBOOK,
29646
+ THIN_GRAPH_THRESHOLD,
29530
29647
  UPG_ACTIVE_TYPES,
29531
29648
  UPG_ANTI_PATTERNS,
29649
+ UPG_ANTI_PATTERN_CONCERNS,
29532
29650
  UPG_APPROACHES,
29533
29651
  UPG_APPROACHES_BY_ID,
29534
29652
  UPG_AREA_TAXONOMY,
@@ -29589,6 +29707,7 @@ export {
29589
29707
  UPG_TYPE_LABELS,
29590
29708
  UPG_TYPE_LABELS_MAP,
29591
29709
  UPG_TYPE_NAMES,
29710
+ UPG_VALIDATION_PROFILES,
29592
29711
  UPG_VALID_CHILDREN,
29593
29712
  UPG_VERSION,
29594
29713
  UPG_WILDCARD_ENDPOINT,
@@ -29601,6 +29720,9 @@ export {
29601
29720
  collectSlugsForType,
29602
29721
  compareVersions,
29603
29722
  computeBodyChecksum,
29723
+ concernEvaluatedFor,
29724
+ concernFor,
29725
+ concernGatesFor,
29604
29726
  describeTreePattern,
29605
29727
  edgeCarriesProperties,
29606
29728
  evaluateAntiPatterns,
@@ -29675,6 +29797,7 @@ export {
29675
29797
  isRegisteredPolymorphicEdge,
29676
29798
  isRuntimeStateShapedProperty,
29677
29799
  isSubSequenceStep,
29800
+ isThinCoverageAdvisory,
29678
29801
  isUPGDocument,
29679
29802
  listPropertiesByModifier,
29680
29803
  listStatusMigrations,
@@ -29700,6 +29823,7 @@ export {
29700
29823
  validateProductStageStrict,
29701
29824
  validateUPGDocument,
29702
29825
  validateUPGFramework,
29826
+ validationProfileFor,
29703
29827
  walkMigrationChainToCanonical
29704
29828
  };
29705
29829
  //# sourceMappingURL=index.js.map