@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 +34 -0
- package/dist/index.cjs +46 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +45 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
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
|
-
|
|
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", "
|
|
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: ["
|
|
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", "
|
|
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.
|
|
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,
|