@unified-product-graph/core 0.9.9 → 0.9.11

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/CHANGELOG.md CHANGED
@@ -7,6 +7,40 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.9.11] - 2026-06-10
11
+
12
+ **Tooling DX (batch 6, part 2): status pre-flight + anti-pattern visibility.** Two discoverability fixes from the same real portfolio session, plus a self-trip fix.
13
+
14
+ ### Added
15
+ - **`list_status_values(entity_type)`** (tools 118 to 119, on both the local and cloud servers): the valid `status` values a type can hold, as a focused pre-flight lookup so an author no longer learns the set only from a rejected write. Returns lifecycle phases as `{ status, label, terminal }` plus `initial_status` / `terminal_statuses`, or `lifecycle_free: true` with empty `values`. Sourced from `UPG_LIFECYCLES` (exactly what the write validator checks): the low-token sibling of `get_lifecycle`.
16
+ - **Anti-pattern version tracking.** `UPGCuratedAntiPattern` gains an optional `since` field, and `get_spec_version` now reports `anti_patterns: { total, versioned }`. A consumer can see which validators are newer than the version a graph was authored under, so a spec upgrade no longer silently flips a clean graph invalid with no heads-up.
17
+
18
+ ### Changed
19
+ - **A drafted hypothesis is auto-promoted to `active`** when it gains a `hypothesis_requires_experiment_plan` edge (via `create_edge` / `batch_create_edges`). Pairing a hypothesis with a plan is what activates it; this stops the documented structural-spine recipe (hypotheses paired with plans) from self-tripping the `untested-hypothesis-pile-up` anti-pattern. Best-effort: a promotion failure never fails the edge.
20
+
21
+ No entity, domain, or edge-catalogue count change (tool count 118 to 119).
22
+
23
+ ---
24
+
25
+ ## [0.9.10] - 2026-06-10
26
+
27
+ **Tooling DX + a born-valid product (batch 6, part 1) + a release-test gate.** Polish from a real multi-graph portfolio session: parity and discoverability fixes, one model correction, and — the headline — the release train now runs the unit suites, after they were found to have been silently red across a release.
28
+
29
+ ### Changed
30
+ - **Product `properties.stage` is canonical.** The stale `UPG_PROPERTY_MIGRATIONS['0.2.13']` lift of a product node's `properties.stage` to top-level `status` is retired: it contradicted the current convention the whole runtime uses (a product node's lifecycle stage lives in `properties.stage`), so a freshly-created product read as `property_drift` against its own validator. A new product is now born valid. Legacy values are still normalised on read by `coerceProductStage`. Decision: `2026-06-10-product-stage-properties-canonical.md`.
31
+ - **`get_nodes` resolves cross-product ids.** A qualified `{product_id}/{node_id}` (the form `list_registry` / `export_edges` / cross-edges return) now reads that product's graph (read-only for non-active products), so a connective pass fetches node content across the portfolio without a `switch_product` sweep. Results carry a `product_id`.
32
+ - **`create_edge` / `batch_create_edges` accept the `p_` product-header** where the in-graph product node is expected, resolving it to the `type:"product"` node (older graphs carry a distinct `n_…` product node).
33
+ - **`did_you_mean` for a mistyped edge type** now falls back to the closest catalogue key by name (edit distance) when the endpoints don't resolve to a canonical pair — so a typo always gets a concrete suggestion.
34
+ - **`batch_create_cross_product_edges` reaches enum parity** with `create_cross_product_edge` (both now derive their type list from one spec source, so the batch tool can no longer lag a release behind — it had been missing `rolls_up_to`).
35
+ - **`batch_register_instance` echoes** `aliased` / `alias_updated` per result, so a bulk sanction pass is visible without a second `portfolio_validate`.
36
+
37
+ ### Fixed
38
+ - **Release-test gate.** The publish train now runs the train packages' unit suites (`gate:tests`) as a hard pre-flight; a red test aborts the train. This closes the gap that let a prior release ship with several silently-red suites — including import adapters that emitted edge types a rename had retired (now corrected), stale compiled test copies, and fixtures referencing retired edges.
39
+
40
+ No new tools (118) and no entity / domain / edge-catalogue count change.
41
+
42
+ ---
43
+
10
44
  ## [0.9.9] - 2026-06-10
11
45
 
12
46
  **Structural-integrity forks: validation, AI-prompt, and journey models corrected; duplicate edges collapsed.** A spec-model cut that fixes several long-standing structural mismodels and retires shadow edges. Every renamed or retired edge ships a dual-read migration (`UPG_EDGE_MIGRATIONS['0.9.9']`), so stored graphs upgrade automatically on load. No new tools (118) and no entity-count change; the within-product edge catalogue settles at 975 after the duplicate collapse.
package/dist/index.cjs CHANGED
@@ -182,6 +182,7 @@ __export(index_exports, {
182
182
  resolveEntityType: () => resolveEntityType,
183
183
  resolveLabel: () => resolveLabel,
184
184
  resolveSlugCollision: () => resolveSlugCollision,
185
+ ringOrderedDomainIds: () => ringOrderedDomainIds,
185
186
  rotateSlug: () => rotateSlug,
186
187
  serializeCanonical: () => serializeCanonical,
187
188
  validateProductStageStrict: () => validateProductStageStrict,
@@ -7666,22 +7667,18 @@ var UPG_PROPERTY_MIGRATIONS = {
7666
7667
  // graphs typically carry only one of the two; the conflict path is
7667
7668
  // theoretical but documented for predictability.
7668
7669
  "0.2.13": [
7669
- // 1. Lift properties.stage → status (with value remap to UPGProductStage).
7670
- {
7671
- kind: "lift_property_to_top_level",
7672
- type: "product",
7673
- from_property: "stage",
7674
- to: "status",
7675
- value_map: {
7676
- // Pre-canonical UPGProductStage aliases from early v0.1 product-node
7677
- // shape. "idea" was the canonical pre-validation phase before the
7678
- // 9-stage UPGProductStage shipped; today that's "concept".
7679
- idea: "concept"
7680
- // Pass-through: the canonical 9 product stages already exist as
7681
- // UPGProductStage values and need no remap when surfaced inline.
7682
- },
7683
- reason: 'lifecycle phase belongs in top-level `status` per UPGBaseNode contract, not in `properties`. Pre-canonical "idea" alias mapped to canonical `concept` (UPGProductStage[0]).'
7684
- },
7670
+ // 1. (RETIRED in 0.9.10, batch-6 #33) The product `properties.stage` →
7671
+ // top-level `status` lift is superseded by UPG-654/661, which
7672
+ // re-canonicalised a product node's lifecycle stage to
7673
+ // `properties.stage` — a distinct 9-phase `UPGProductStage` axis,
7674
+ // separate from the generic top-level `status` other entities use. The
7675
+ // entire runtime (create_product, get_graph_digest, get_product_context,
7676
+ // completeness, header-sync) reads and writes `properties.stage`, so
7677
+ // lifting it to `status` made a freshly-created product read as
7678
+ // `property_drift` against its own validator (a tool emitting drift its
7679
+ // validator immediately flags). Legacy "idea"-style values are still
7680
+ // normalised on READ by `coerceProductStage`, so retiring the lift loses
7681
+ // no cleanup. Decision: 2026-06-10-product-stage-properties-canonical.md.
7685
7682
  // 2. Rename top-level lifecycle_status → status (with value remap).
7686
7683
  {
7687
7684
  kind: "rename_top_level",
@@ -20474,19 +20471,23 @@ var UPG_DOMAIN_RINGS = [
20474
20471
  id: "define",
20475
20472
  label: "Define",
20476
20473
  description: "What are we building?",
20477
- domain_ids: ["strategy", "product_spec", "legal", "ux_design", "design_system", "brand"]
20474
+ // `business_model` lives in the `define` ring (foundational "what are we
20475
+ // building & how it sustains"), not in `grow`. This is an intentional
20476
+ // ring-vs-region divergence: UPG_REGIONS still groups business_model under
20477
+ // `business_gtm_growth` (region membership is a separately-tracked decision).
20478
+ domain_ids: ["strategy", "product_spec", "business_model", "ux_design", "design_system", "brand", "legal"]
20478
20479
  },
20479
20480
  {
20480
20481
  id: "build",
20481
20482
  label: "Build",
20482
20483
  description: "How do we construct it?",
20483
- domain_ids: ["engineering", "devops", "testing", "security", "accessibility", "data_analytics", "ai", "automation"]
20484
+ domain_ids: ["engineering", "ai", "automation", "data_analytics", "testing", "devops", "security", "accessibility"]
20484
20485
  },
20485
20486
  {
20486
20487
  id: "grow",
20487
20488
  label: "Grow",
20488
20489
  description: "How do we make money?",
20489
- domain_ids: ["business_model", "growth", "go_to_market", "pricing", "sales", "marketing"]
20490
+ domain_ids: ["pricing", "go_to_market", "sales", "marketing", "growth"]
20490
20491
  },
20491
20492
  {
20492
20493
  id: "operate",
@@ -20498,9 +20499,27 @@ var UPG_DOMAIN_RINGS = [
20498
20499
  id: "extend",
20499
20500
  label: "Extend",
20500
20501
  description: "How do we scale?",
20501
- domain_ids: ["team_org", "program_mgmt", "localisation", "ecosystem", "compliance"]
20502
+ domain_ids: ["team_org", "program_mgmt", "ecosystem", "localisation", "compliance"]
20502
20503
  }
20503
20504
  ];
20505
+ function assertRingsPartitionDomains() {
20506
+ const ringDomainIds = UPG_DOMAIN_RINGS.flatMap((r) => [...r.domain_ids]);
20507
+ const canonical = new Set(UPG_DOMAINS.map((d) => d.id));
20508
+ const ringSet = new Set(ringDomainIds);
20509
+ const duplicates = ringDomainIds.filter((id, i) => ringDomainIds.indexOf(id) !== i);
20510
+ const unknown = [...ringSet].filter((id) => !canonical.has(id));
20511
+ const unassigned = [...canonical].filter((id) => !ringSet.has(id));
20512
+ if (duplicates.length || unknown.length || unassigned.length) {
20513
+ const parts = [];
20514
+ if (unassigned.length) parts.push(`domains in no ring: [${unassigned.join(", ")}]`);
20515
+ if (unknown.length) parts.push(`ring domains not in UPG_DOMAINS: [${unknown.join(", ")}]`);
20516
+ if (duplicates.length) parts.push(`domains in multiple rings: [${[...new Set(duplicates)].join(", ")}]`);
20517
+ throw new Error(
20518
+ `UPG_DOMAIN_RINGS must partition UPG_DOMAINS exactly: ${parts.join("; ")}. Slot every domain into exactly one ring in domain-rings.ts.`
20519
+ );
20520
+ }
20521
+ }
20522
+ assertRingsPartitionDomains();
20504
20523
  function getRingForDomain(domainId) {
20505
20524
  return UPG_DOMAIN_RINGS.find((ring) => ring.domain_ids.includes(domainId));
20506
20525
  }
@@ -20508,6 +20527,9 @@ function getDomainsInRing(ringId) {
20508
20527
  const ring = UPG_DOMAIN_RINGS.find((r) => r.id === ringId);
20509
20528
  return ring ? [...ring.domain_ids] : [];
20510
20529
  }
20530
+ function ringOrderedDomainIds() {
20531
+ return UPG_DOMAIN_RINGS.flatMap((ring) => [...ring.domain_ids]);
20532
+ }
20511
20533
 
20512
20534
  // src/intelligence/domain-guides.ts
20513
20535
  var USER_GUIDE = {
@@ -24071,6 +24093,7 @@ var UPG_ANTI_PATTERNS = [
24071
24093
  // RelationshipCheck (edge presence/absence) and are promoted here.
24072
24094
  {
24073
24095
  id: "insights-without-evidence",
24096
+ since: "0.9.7",
24074
24097
  name: "Insights without evidence",
24075
24098
  description: "The graph has insight entities but none are backed by a primary-evidence link: no observation yields them (`observation_yields_insight`), no survey response evidences them (`survey_response_evidences_insight`), and no quote is attached (`insight_evidenced_by_quote`). An insight with no evidence behind it is an opinion wearing a research label.",
24076
24099
  structured_condition: {
@@ -24114,6 +24137,7 @@ var UPG_ANTI_PATTERNS = [
24114
24137
  },
24115
24138
  {
24116
24139
  id: "feature-requests-without-provenance",
24140
+ since: "0.9.7",
24117
24141
  name: "Feature requests without provenance",
24118
24142
  description: "The graph has feature_request entities but none trace back to a source: no feedback program collects them (`feedback_program_collects_feature_request`), no customer feedback becomes one (`customer_feedback_becomes_feature_request`), and none originate from a behavioural segment (`feature_request_from_behavioral_segment`). A request with no provenance cannot be weighed against who asked or how many.",
24119
24143
  structured_condition: {
@@ -27174,7 +27198,7 @@ function isCanonical(text) {
27174
27198
  }
27175
27199
 
27176
27200
  // src/index.ts
27177
- var UPG_VERSION = "0.9.9";
27201
+ var UPG_VERSION = "0.9.11";
27178
27202
  var UPG_FORMAT_VERSION = "0.4.0";
27179
27203
  var MARKDOWN_FORMAT_VERSION = "0.1";
27180
27204
  var UPG_TYPES = getTypes();
@@ -27401,6 +27425,7 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
27401
27425
  resolveEntityType,
27402
27426
  resolveLabel,
27403
27427
  resolveSlugCollision,
27428
+ ringOrderedDomainIds,
27404
27429
  rotateSlug,
27405
27430
  serializeCanonical,
27406
27431
  validateProductStageStrict,