@unified-product-graph/core 0.8.6 → 0.8.8
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 +26 -0
- package/dist/index.cjs +813 -220
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +97 -3
- package/dist/index.d.ts +97 -3
- package/dist/index.js +812 -220
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -8475,209 +8475,6 @@ function rotateSlug(node, next) {
|
|
|
8475
8475
|
return node;
|
|
8476
8476
|
}
|
|
8477
8477
|
|
|
8478
|
-
// src/grammar/validate.ts
|
|
8479
|
-
var crossProductEdgeTypeSet = new Set(UPG_CROSS_EDGE_TYPES);
|
|
8480
|
-
function validateUPGDocument(doc) {
|
|
8481
|
-
const errors = [];
|
|
8482
|
-
const warnings = [];
|
|
8483
|
-
if (!doc || typeof doc !== "object") {
|
|
8484
|
-
return { valid: false, errors: [{ path: "$", message: "Document must be an object" }], warnings };
|
|
8485
|
-
}
|
|
8486
|
-
const d = doc;
|
|
8487
|
-
if (!d.upg_version || typeof d.upg_version !== "string") {
|
|
8488
|
-
errors.push({ path: "$.upg_version", message: "upg_version is required and must be a string" });
|
|
8489
|
-
}
|
|
8490
|
-
if (!d.exported_at || typeof d.exported_at !== "string") {
|
|
8491
|
-
errors.push({ path: "$.exported_at", message: "exported_at is required and must be an ISO 8601 string" });
|
|
8492
|
-
}
|
|
8493
|
-
if (!d.source || typeof d.source !== "object") {
|
|
8494
|
-
errors.push({ path: "$.source", message: "source is required and must be an object" });
|
|
8495
|
-
} else {
|
|
8496
|
-
const source = d.source;
|
|
8497
|
-
if (!source.tool || typeof source.tool !== "string") {
|
|
8498
|
-
errors.push({ path: "$.source.tool", message: "source.tool is required and must be a string" });
|
|
8499
|
-
}
|
|
8500
|
-
}
|
|
8501
|
-
if (!d.product || typeof d.product !== "object") {
|
|
8502
|
-
errors.push({ path: "$.product", message: "product is required and must be an object" });
|
|
8503
|
-
} else {
|
|
8504
|
-
const product = d.product;
|
|
8505
|
-
if (!product.id || typeof product.id !== "string") {
|
|
8506
|
-
errors.push({ path: "$.product.id", message: "product.id is required and must be a string" });
|
|
8507
|
-
}
|
|
8508
|
-
if (!product.title || typeof product.title !== "string") {
|
|
8509
|
-
errors.push({ path: "$.product.title", message: "product.title is required and must be a string" });
|
|
8510
|
-
}
|
|
8511
|
-
}
|
|
8512
|
-
const allKnownTypes = new Set(getTypes());
|
|
8513
|
-
const allKnownEdgeTypes = new Set(Object.keys(UPG_EDGE_CATALOG));
|
|
8514
|
-
if (!Array.isArray(d.nodes)) {
|
|
8515
|
-
errors.push({ path: "$.nodes", message: "nodes is required and must be an array" });
|
|
8516
|
-
} else {
|
|
8517
|
-
const nodeIds = /* @__PURE__ */ new Set();
|
|
8518
|
-
const nodesById = /* @__PURE__ */ new Map();
|
|
8519
|
-
d.nodes.forEach((node, i) => {
|
|
8520
|
-
const path = `$.nodes[${i}]`;
|
|
8521
|
-
if (!node || typeof node !== "object") {
|
|
8522
|
-
errors.push({ path, message: "Each node must be an object" });
|
|
8523
|
-
return;
|
|
8524
|
-
}
|
|
8525
|
-
const n = node;
|
|
8526
|
-
if (!n.id || typeof n.id !== "string") {
|
|
8527
|
-
errors.push({ path: `${path}.id`, message: "Node id is required and must be a string" });
|
|
8528
|
-
} else {
|
|
8529
|
-
if (nodeIds.has(n.id)) {
|
|
8530
|
-
errors.push({ path: `${path}.id`, message: `Duplicate node id: ${n.id}` });
|
|
8531
|
-
}
|
|
8532
|
-
nodeIds.add(n.id);
|
|
8533
|
-
nodesById.set(n.id, n);
|
|
8534
|
-
}
|
|
8535
|
-
if (!n.type || typeof n.type !== "string") {
|
|
8536
|
-
errors.push({ path: `${path}.type`, message: "Node type is required and must be a string" });
|
|
8537
|
-
} else if (!allKnownTypes.has(n.type)) {
|
|
8538
|
-
warnings.push({ path: `${path}.type`, message: `Unknown UPG type: "${n.type}". Node will be preserved with its original type.` });
|
|
8539
|
-
}
|
|
8540
|
-
if (!n.title || typeof n.title !== "string") {
|
|
8541
|
-
errors.push({ path: `${path}.title`, message: "Node title is required and must be a string" });
|
|
8542
|
-
}
|
|
8543
|
-
});
|
|
8544
|
-
if (!Array.isArray(d.edges)) {
|
|
8545
|
-
errors.push({ path: "$.edges", message: "edges is required and must be an array" });
|
|
8546
|
-
} else {
|
|
8547
|
-
d.edges.forEach((edge, i) => {
|
|
8548
|
-
const path = `$.edges[${i}]`;
|
|
8549
|
-
if (!edge || typeof edge !== "object") {
|
|
8550
|
-
errors.push({ path, message: "Each edge must be an object" });
|
|
8551
|
-
return;
|
|
8552
|
-
}
|
|
8553
|
-
const e = edge;
|
|
8554
|
-
if (!e.id || typeof e.id !== "string") {
|
|
8555
|
-
errors.push({ path: `${path}.id`, message: "Edge id is required and must be a string" });
|
|
8556
|
-
}
|
|
8557
|
-
if (typeof e.type === "string" && crossProductEdgeTypeSet.has(e.type)) {
|
|
8558
|
-
errors.push({
|
|
8559
|
-
path: `${path}.type`,
|
|
8560
|
-
message: `Cross-product edge type "${e.type}" must live in portfolio.cross_edges[], not product edges[].`
|
|
8561
|
-
});
|
|
8562
|
-
return;
|
|
8563
|
-
}
|
|
8564
|
-
if (!e.source || typeof e.source !== "string") {
|
|
8565
|
-
errors.push({ path: `${path}.source`, message: "Edge source is required and must be a string" });
|
|
8566
|
-
} else if (!nodeIds.has(e.source)) {
|
|
8567
|
-
errors.push({ path: `${path}.source`, message: `Edge source references unknown node id: ${e.source}` });
|
|
8568
|
-
}
|
|
8569
|
-
if (!e.target || typeof e.target !== "string") {
|
|
8570
|
-
errors.push({ path: `${path}.target`, message: "Edge target is required and must be a string" });
|
|
8571
|
-
} else if (!nodeIds.has(e.target)) {
|
|
8572
|
-
errors.push({ path: `${path}.target`, message: `Edge target references unknown node id: ${e.target}` });
|
|
8573
|
-
}
|
|
8574
|
-
if (!e.type || typeof e.type !== "string") {
|
|
8575
|
-
errors.push({ path: `${path}.type`, message: "Edge type is required and must be a string" });
|
|
8576
|
-
} else if (!allKnownEdgeTypes.has(e.type)) {
|
|
8577
|
-
warnings.push({
|
|
8578
|
-
path: `${path}.type`,
|
|
8579
|
-
message: `Unknown edge type: "${e.type}". Edge preserved but not in the edge registry.`
|
|
8580
|
-
});
|
|
8581
|
-
}
|
|
8582
|
-
});
|
|
8583
|
-
const valueToAxis = /* @__PURE__ */ new Map();
|
|
8584
|
-
const edgesArr = d.edges;
|
|
8585
|
-
for (const edge of edgesArr) {
|
|
8586
|
-
if (!edge || typeof edge !== "object") continue;
|
|
8587
|
-
const e = edge;
|
|
8588
|
-
if (e.type === "classification_axis_includes_classification_value" && typeof e.source === "string" && typeof e.target === "string") {
|
|
8589
|
-
valueToAxis.set(e.target, e.source);
|
|
8590
|
-
}
|
|
8591
|
-
}
|
|
8592
|
-
for (const [id, n] of nodesById.entries()) {
|
|
8593
|
-
if (n.type !== "classification_value") continue;
|
|
8594
|
-
if (valueToAxis.has(id)) continue;
|
|
8595
|
-
const pid = n.parent_id;
|
|
8596
|
-
if (typeof pid === "string") {
|
|
8597
|
-
const parent = nodesById.get(pid);
|
|
8598
|
-
if (parent && parent.type === "classification_axis") {
|
|
8599
|
-
valueToAxis.set(id, pid);
|
|
8600
|
-
}
|
|
8601
|
-
}
|
|
8602
|
-
}
|
|
8603
|
-
const axesPerAnalysis = /* @__PURE__ */ new Map();
|
|
8604
|
-
for (const edge of edgesArr) {
|
|
8605
|
-
if (!edge || typeof edge !== "object") continue;
|
|
8606
|
-
const e = edge;
|
|
8607
|
-
if (e.type === "competitive_analysis_dimensioned_by_classification_axis" && typeof e.source === "string") {
|
|
8608
|
-
axesPerAnalysis.set(e.source, (axesPerAnalysis.get(e.source) ?? 0) + 1);
|
|
8609
|
-
}
|
|
8610
|
-
}
|
|
8611
|
-
d.nodes.forEach((node, i) => {
|
|
8612
|
-
if (!node || typeof node !== "object") return;
|
|
8613
|
-
const n = node;
|
|
8614
|
-
if (n.type !== "competitive_analysis") return;
|
|
8615
|
-
const props = n.properties;
|
|
8616
|
-
if (!props || typeof props !== "object") return;
|
|
8617
|
-
const empty = props.empty_cells;
|
|
8618
|
-
if (!Array.isArray(empty)) return;
|
|
8619
|
-
const axisCount = axesPerAnalysis.get(n.id ?? "") ?? 0;
|
|
8620
|
-
if (axisCount !== 2) {
|
|
8621
|
-
warnings.push({
|
|
8622
|
-
path: `$.nodes[${i}].properties.empty_cells`,
|
|
8623
|
-
message: `empty_cells is meaningful only on a 2-axis competitive_analysis; this node is dimensioned by ${axisCount} classification_axis ${axisCount === 1 ? "child" : "children"}.`
|
|
8624
|
-
});
|
|
8625
|
-
}
|
|
8626
|
-
empty.forEach((cell, j) => {
|
|
8627
|
-
const cpath = `$.nodes[${i}].properties.empty_cells[${j}]`;
|
|
8628
|
-
if (!cell || typeof cell !== "object") {
|
|
8629
|
-
errors.push({ path: cpath, message: "empty_cells entry must be an object" });
|
|
8630
|
-
return;
|
|
8631
|
-
}
|
|
8632
|
-
const c = cell;
|
|
8633
|
-
const aRef = c.axis_a_value_ref;
|
|
8634
|
-
const bRef = c.axis_b_value_ref;
|
|
8635
|
-
if (typeof aRef !== "string") {
|
|
8636
|
-
errors.push({ path: `${cpath}.axis_a_value_ref`, message: "axis_a_value_ref is required and must be a string" });
|
|
8637
|
-
}
|
|
8638
|
-
if (typeof bRef !== "string") {
|
|
8639
|
-
errors.push({ path: `${cpath}.axis_b_value_ref`, message: "axis_b_value_ref is required and must be a string" });
|
|
8640
|
-
}
|
|
8641
|
-
if (typeof aRef !== "string" || typeof bRef !== "string") return;
|
|
8642
|
-
const aNode = nodesById.get(aRef);
|
|
8643
|
-
const bNode = nodesById.get(bRef);
|
|
8644
|
-
if (!aNode) {
|
|
8645
|
-
errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Unknown node id: ${aRef}` });
|
|
8646
|
-
} else if (aNode.type !== "classification_value") {
|
|
8647
|
-
errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Expected classification_value, found "${String(aNode.type)}" at ${aRef}` });
|
|
8648
|
-
}
|
|
8649
|
-
if (!bNode) {
|
|
8650
|
-
errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Unknown node id: ${bRef}` });
|
|
8651
|
-
} else if (bNode.type !== "classification_value") {
|
|
8652
|
-
errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Expected classification_value, found "${String(bNode.type)}" at ${bRef}` });
|
|
8653
|
-
}
|
|
8654
|
-
if (aNode && bNode && aNode.type === "classification_value" && bNode.type === "classification_value") {
|
|
8655
|
-
const aAxis = valueToAxis.get(aRef);
|
|
8656
|
-
const bAxis = valueToAxis.get(bRef);
|
|
8657
|
-
if (aAxis && bAxis && aAxis === bAxis) {
|
|
8658
|
-
errors.push({
|
|
8659
|
-
path: cpath,
|
|
8660
|
-
message: `empty_cells references two classification_values from the same axis (${aAxis}); axis_a and axis_b must come from distinct classification_axis parents.`
|
|
8661
|
-
});
|
|
8662
|
-
}
|
|
8663
|
-
}
|
|
8664
|
-
const kind = c.rationale_kind;
|
|
8665
|
-
if (kind !== void 0 && kind !== "structural" && kind !== "ideological" && kind !== "opportunity") {
|
|
8666
|
-
errors.push({
|
|
8667
|
-
path: `${cpath}.rationale_kind`,
|
|
8668
|
-
message: `rationale_kind must be one of 'structural' | 'ideological' | 'opportunity'; got "${String(kind)}".`
|
|
8669
|
-
});
|
|
8670
|
-
}
|
|
8671
|
-
});
|
|
8672
|
-
});
|
|
8673
|
-
}
|
|
8674
|
-
}
|
|
8675
|
-
return { valid: errors.length === 0, errors, warnings };
|
|
8676
|
-
}
|
|
8677
|
-
function isUPGDocument(doc) {
|
|
8678
|
-
return validateUPGDocument(doc).valid;
|
|
8679
|
-
}
|
|
8680
|
-
|
|
8681
8478
|
// src/properties/property-schema.ts
|
|
8682
8479
|
var UPG_PROPERTY_SCHEMA = {
|
|
8683
8480
|
// A11yAnnotationProperties: Accessibility annotation.
|
|
@@ -12054,31 +11851,37 @@ var UPG_FRAMEWORKS = [
|
|
|
12054
11851
|
"slots": [
|
|
12055
11852
|
{
|
|
12056
11853
|
"label": "Customer Jobs",
|
|
11854
|
+
"role": "customer_job",
|
|
12057
11855
|
"entityTypeId": "job",
|
|
12058
11856
|
"description": "Place job entities in the Customer Jobs position of the matrix"
|
|
12059
11857
|
},
|
|
12060
11858
|
{
|
|
12061
11859
|
"label": "Pains",
|
|
11860
|
+
"role": "pain",
|
|
12062
11861
|
"entityTypeId": "need",
|
|
12063
11862
|
"description": "Place need entities in the Pains position of the matrix"
|
|
12064
11863
|
},
|
|
12065
11864
|
{
|
|
12066
11865
|
"label": "Gains",
|
|
11866
|
+
"role": "gain",
|
|
12067
11867
|
"entityTypeId": "desired_outcome",
|
|
12068
11868
|
"description": "Place desired outcome entities in the Gains position of the matrix"
|
|
12069
11869
|
},
|
|
12070
11870
|
{
|
|
12071
11871
|
"label": "Products & Services",
|
|
11872
|
+
"role": "product_or_service",
|
|
12072
11873
|
"entityTypeId": "feature",
|
|
12073
11874
|
"description": "Place feature entities in the Products & Services position of the matrix"
|
|
12074
11875
|
},
|
|
12075
11876
|
{
|
|
12076
11877
|
"label": "Pain Relievers",
|
|
11878
|
+
"role": "pain_reliever",
|
|
12077
11879
|
"entityTypeId": "feature",
|
|
12078
11880
|
"description": "Place feature entities in the Pain Relievers position of the matrix"
|
|
12079
11881
|
},
|
|
12080
11882
|
{
|
|
12081
11883
|
"label": "Gain Creators",
|
|
11884
|
+
"role": "gain_creator",
|
|
12082
11885
|
"entityTypeId": "feature",
|
|
12083
11886
|
"description": "Place feature entities in the Gain Creators position of the matrix"
|
|
12084
11887
|
}
|
|
@@ -12707,26 +12510,31 @@ var UPG_FRAMEWORKS = [
|
|
|
12707
12510
|
"slots": [
|
|
12708
12511
|
{
|
|
12709
12512
|
"label": "Competitive Rivalry",
|
|
12513
|
+
"role": "competitive_rivalry",
|
|
12710
12514
|
"entityTypeId": "competitor",
|
|
12711
12515
|
"description": "Intensity of competition among existing players"
|
|
12712
12516
|
},
|
|
12713
12517
|
{
|
|
12714
12518
|
"label": "Threat of New Entrants",
|
|
12519
|
+
"role": "new_entrants",
|
|
12715
12520
|
"entityTypeId": "competitor",
|
|
12716
12521
|
"description": "How easy is it for new competitors to enter?"
|
|
12717
12522
|
},
|
|
12718
12523
|
{
|
|
12719
12524
|
"label": "Threat of Substitutes",
|
|
12525
|
+
"role": "substitutes",
|
|
12720
12526
|
"entityTypeId": "competitor",
|
|
12721
12527
|
"description": "Can customers switch to alternatives?"
|
|
12722
12528
|
},
|
|
12723
12529
|
{
|
|
12724
12530
|
"label": "Buyer Power",
|
|
12531
|
+
"role": "buyer_power",
|
|
12725
12532
|
"entityTypeId": "persona",
|
|
12726
12533
|
"description": "How much leverage do buyers have?"
|
|
12727
12534
|
},
|
|
12728
12535
|
{
|
|
12729
12536
|
"label": "Supplier Power",
|
|
12537
|
+
"role": "supplier_power",
|
|
12730
12538
|
"entityTypeId": "persona",
|
|
12731
12539
|
"description": "How much leverage do suppliers have?"
|
|
12732
12540
|
}
|
|
@@ -12894,31 +12702,37 @@ var UPG_FRAMEWORKS = [
|
|
|
12894
12702
|
"slots": [
|
|
12895
12703
|
{
|
|
12896
12704
|
"label": "Inbound Logistics",
|
|
12705
|
+
"role": "inbound_logistics",
|
|
12897
12706
|
"entityTypeId": "key_activity",
|
|
12898
12707
|
"description": "Inbound Logistics phase: key activity entities move through this stage"
|
|
12899
12708
|
},
|
|
12900
12709
|
{
|
|
12901
12710
|
"label": "Operations",
|
|
12711
|
+
"role": "operations",
|
|
12902
12712
|
"entityTypeId": "key_activity",
|
|
12903
12713
|
"description": "Operations phase: key activity entities move through this stage"
|
|
12904
12714
|
},
|
|
12905
12715
|
{
|
|
12906
12716
|
"label": "Outbound Logistics",
|
|
12717
|
+
"role": "outbound_logistics",
|
|
12907
12718
|
"entityTypeId": "key_activity",
|
|
12908
12719
|
"description": "Outbound Logistics phase: key activity entities move through this stage"
|
|
12909
12720
|
},
|
|
12910
12721
|
{
|
|
12911
12722
|
"label": "Marketing & Sales",
|
|
12723
|
+
"role": "marketing_and_sales",
|
|
12912
12724
|
"entityTypeId": "key_activity",
|
|
12913
12725
|
"description": "Marketing & Sales phase: key activity entities move through this stage"
|
|
12914
12726
|
},
|
|
12915
12727
|
{
|
|
12916
12728
|
"label": "Service",
|
|
12729
|
+
"role": "service",
|
|
12917
12730
|
"entityTypeId": "key_activity",
|
|
12918
12731
|
"description": "Service phase: key activity entities move through this stage"
|
|
12919
12732
|
},
|
|
12920
12733
|
{
|
|
12921
12734
|
"label": "Support Activities",
|
|
12735
|
+
"role": "support_activity",
|
|
12922
12736
|
"entityTypeId": "capability",
|
|
12923
12737
|
"description": "Support Activities phase: capability entities move through this stage"
|
|
12924
12738
|
}
|
|
@@ -12989,26 +12803,31 @@ var UPG_FRAMEWORKS = [
|
|
|
12989
12803
|
"slots": [
|
|
12990
12804
|
{
|
|
12991
12805
|
"label": "Portfolio",
|
|
12806
|
+
"role": "portfolio",
|
|
12992
12807
|
"entityTypeId": "portfolio",
|
|
12993
12808
|
"description": "Innovation portfolio balanced across three horizons: core performance, emerging growth, and future creation"
|
|
12994
12809
|
},
|
|
12995
12810
|
{
|
|
12996
12811
|
"label": "Product",
|
|
12812
|
+
"role": "product",
|
|
12997
12813
|
"entityTypeId": "product",
|
|
12998
12814
|
"description": "Product or business unit assigned to a horizon: H1 (defend and extend), H2 (build), or H3 (seed and explore)"
|
|
12999
12815
|
},
|
|
13000
12816
|
{
|
|
13001
12817
|
"label": "Initiative",
|
|
12818
|
+
"role": "initiative",
|
|
13002
12819
|
"entityTypeId": "portfolio",
|
|
13003
12820
|
"description": "Growth initiative positioned in a horizon, with appropriate funding model, governance, and success metrics"
|
|
13004
12821
|
},
|
|
13005
12822
|
{
|
|
13006
12823
|
"label": "Product Area",
|
|
12824
|
+
"role": "product_area",
|
|
13007
12825
|
"entityTypeId": "product_area",
|
|
13008
12826
|
"description": "Product area representing a horizon: core products (H1), adjacent expansions (H2), or experimental ventures (H3)"
|
|
13009
12827
|
},
|
|
13010
12828
|
{
|
|
13011
12829
|
"label": "Capability",
|
|
12830
|
+
"role": "capability",
|
|
13012
12831
|
"entityTypeId": "capability",
|
|
13013
12832
|
"description": "Capability needed to execute across horizons: H1 needs efficiency, H2 needs scaling, H3 needs discovery"
|
|
13014
12833
|
}
|
|
@@ -13072,7 +12891,7 @@ var UPG_FRAMEWORKS = [
|
|
|
13072
12891
|
],
|
|
13073
12892
|
"name": "RICE Scoring",
|
|
13074
12893
|
"version": "1.0.0",
|
|
13075
|
-
"description": "Score features and
|
|
12894
|
+
"description": "Score features, opportunities, and needs by Reach, Impact, Confidence, and Effort to produce a ranked priority list.",
|
|
13076
12895
|
"category": "prioritization",
|
|
13077
12896
|
"origin": {
|
|
13078
12897
|
"type": "practitioner",
|
|
@@ -13090,7 +12909,7 @@ var UPG_FRAMEWORKS = [
|
|
|
13090
12909
|
{
|
|
13091
12910
|
"label": "Items to score",
|
|
13092
12911
|
"entityTypeId": "feature",
|
|
13093
|
-
"description": "Features, opportunities, or
|
|
12912
|
+
"description": "Features, opportunities, or needs being evaluated"
|
|
13094
12913
|
},
|
|
13095
12914
|
{
|
|
13096
12915
|
"label": "Opportunities to score",
|
|
@@ -13256,7 +13075,60 @@ var UPG_FRAMEWORKS = [
|
|
|
13256
13075
|
"label": "RICE Score",
|
|
13257
13076
|
"format": "number"
|
|
13258
13077
|
}
|
|
13259
|
-
]
|
|
13078
|
+
],
|
|
13079
|
+
"scoring_lens": {
|
|
13080
|
+
"applies_to": [
|
|
13081
|
+
"feature",
|
|
13082
|
+
"opportunity",
|
|
13083
|
+
"need"
|
|
13084
|
+
],
|
|
13085
|
+
"inputs": [
|
|
13086
|
+
{
|
|
13087
|
+
"property": "reach",
|
|
13088
|
+
"type": "assessment",
|
|
13089
|
+
"scale_id": "reach_5",
|
|
13090
|
+
"required": true,
|
|
13091
|
+
"scope": "framework",
|
|
13092
|
+
"label": "Reach",
|
|
13093
|
+
"description": "How many users will this impact per quarter?"
|
|
13094
|
+
},
|
|
13095
|
+
{
|
|
13096
|
+
"property": "impact",
|
|
13097
|
+
"type": "assessment",
|
|
13098
|
+
"scale_id": "impact_5",
|
|
13099
|
+
"required": true,
|
|
13100
|
+
"scope": "framework",
|
|
13101
|
+
"label": "Impact",
|
|
13102
|
+
"description": "How much will this impact each user, on the impact scale?"
|
|
13103
|
+
},
|
|
13104
|
+
{
|
|
13105
|
+
"property": "confidence",
|
|
13106
|
+
"type": "assessment",
|
|
13107
|
+
"scale_id": "confidence_5",
|
|
13108
|
+
"required": true,
|
|
13109
|
+
"scope": "framework",
|
|
13110
|
+
"label": "Confidence",
|
|
13111
|
+
"description": "How confident are you in the reach, impact, and effort estimates?"
|
|
13112
|
+
},
|
|
13113
|
+
{
|
|
13114
|
+
"property": "effort",
|
|
13115
|
+
"type": "assessment",
|
|
13116
|
+
"scale_id": "effort_5",
|
|
13117
|
+
"required": true,
|
|
13118
|
+
"scope": "framework",
|
|
13119
|
+
"label": "Effort",
|
|
13120
|
+
"description": "How much work is required to build and ship this, on the effort scale?"
|
|
13121
|
+
}
|
|
13122
|
+
],
|
|
13123
|
+
"computed": [
|
|
13124
|
+
{
|
|
13125
|
+
"property": "rice_score",
|
|
13126
|
+
"expression": "(reach * impact * confidence) / effort",
|
|
13127
|
+
"label": "RICE Score",
|
|
13128
|
+
"format": "number"
|
|
13129
|
+
}
|
|
13130
|
+
]
|
|
13131
|
+
}
|
|
13260
13132
|
},
|
|
13261
13133
|
"structure": {
|
|
13262
13134
|
"pattern": "table"
|
|
@@ -13435,21 +13307,25 @@ var UPG_FRAMEWORKS = [
|
|
|
13435
13307
|
"slots": [
|
|
13436
13308
|
{
|
|
13437
13309
|
"label": "Must-haves",
|
|
13310
|
+
"role": "must_have",
|
|
13438
13311
|
"entityTypeId": "feature",
|
|
13439
13312
|
"description": "Expected features: absence causes dissatisfaction"
|
|
13440
13313
|
},
|
|
13441
13314
|
{
|
|
13442
13315
|
"label": "Performance",
|
|
13316
|
+
"role": "performance",
|
|
13443
13317
|
"entityTypeId": "feature",
|
|
13444
13318
|
"description": "More is better: linear satisfaction increase"
|
|
13445
13319
|
},
|
|
13446
13320
|
{
|
|
13447
13321
|
"label": "Delighters",
|
|
13322
|
+
"role": "delighter",
|
|
13448
13323
|
"entityTypeId": "feature",
|
|
13449
13324
|
"description": "Unexpected features: presence creates delight"
|
|
13450
13325
|
},
|
|
13451
13326
|
{
|
|
13452
13327
|
"label": "Indifferent",
|
|
13328
|
+
"role": "indifferent",
|
|
13453
13329
|
"entityTypeId": "feature",
|
|
13454
13330
|
"description": "Features users don't care about either way"
|
|
13455
13331
|
}
|
|
@@ -13458,7 +13334,7 @@ var UPG_FRAMEWORKS = [
|
|
|
13458
13334
|
"entity_types": [
|
|
13459
13335
|
{
|
|
13460
13336
|
"type": "feature",
|
|
13461
|
-
"role": "
|
|
13337
|
+
"role": "scored_item"
|
|
13462
13338
|
}
|
|
13463
13339
|
],
|
|
13464
13340
|
"required_properties": {
|
|
@@ -13542,12 +13418,95 @@ var UPG_FRAMEWORKS = [
|
|
|
13542
13418
|
"label": "Dissatisfaction Coefficient",
|
|
13543
13419
|
"format": "number"
|
|
13544
13420
|
}
|
|
13545
|
-
]
|
|
13546
|
-
|
|
13547
|
-
|
|
13548
|
-
|
|
13549
|
-
|
|
13550
|
-
|
|
13421
|
+
],
|
|
13422
|
+
"scoring_lens": {
|
|
13423
|
+
"applies_to": [
|
|
13424
|
+
"feature"
|
|
13425
|
+
],
|
|
13426
|
+
"inputs": [
|
|
13427
|
+
{
|
|
13428
|
+
"property": "functional_response",
|
|
13429
|
+
"type": "enum",
|
|
13430
|
+
"required": true,
|
|
13431
|
+
"scope": "framework",
|
|
13432
|
+
"label": "Functional Response",
|
|
13433
|
+
"description": "How users feel when the feature IS present",
|
|
13434
|
+
"enum_values": [
|
|
13435
|
+
"i_like_it",
|
|
13436
|
+
"i_expect_it",
|
|
13437
|
+
"i_am_neutral",
|
|
13438
|
+
"i_can_tolerate_it",
|
|
13439
|
+
"i_dislike_it"
|
|
13440
|
+
]
|
|
13441
|
+
},
|
|
13442
|
+
{
|
|
13443
|
+
"property": "dysfunctional_response",
|
|
13444
|
+
"type": "enum",
|
|
13445
|
+
"required": true,
|
|
13446
|
+
"scope": "framework",
|
|
13447
|
+
"label": "Dysfunctional Response",
|
|
13448
|
+
"description": "How users feel when the feature IS NOT present",
|
|
13449
|
+
"enum_values": [
|
|
13450
|
+
"i_like_it",
|
|
13451
|
+
"i_expect_it",
|
|
13452
|
+
"i_am_neutral",
|
|
13453
|
+
"i_can_tolerate_it",
|
|
13454
|
+
"i_dislike_it"
|
|
13455
|
+
]
|
|
13456
|
+
},
|
|
13457
|
+
{
|
|
13458
|
+
"property": "delighter_count",
|
|
13459
|
+
"type": "number",
|
|
13460
|
+
"required": false,
|
|
13461
|
+
"scope": "framework",
|
|
13462
|
+
"label": "Delighter classifications",
|
|
13463
|
+
"description": "Count of survey responses classifying this feature as a delighter (attractive)"
|
|
13464
|
+
},
|
|
13465
|
+
{
|
|
13466
|
+
"property": "performance_count",
|
|
13467
|
+
"type": "number",
|
|
13468
|
+
"required": false,
|
|
13469
|
+
"scope": "framework",
|
|
13470
|
+
"label": "Performance classifications",
|
|
13471
|
+
"description": "Count of survey responses classifying this feature as performance (one-dimensional)"
|
|
13472
|
+
},
|
|
13473
|
+
{
|
|
13474
|
+
"property": "must_be_count",
|
|
13475
|
+
"type": "number",
|
|
13476
|
+
"required": false,
|
|
13477
|
+
"scope": "framework",
|
|
13478
|
+
"label": "Must-be classifications",
|
|
13479
|
+
"description": "Count of survey responses classifying this feature as must-be (basic)"
|
|
13480
|
+
},
|
|
13481
|
+
{
|
|
13482
|
+
"property": "indifferent_count",
|
|
13483
|
+
"type": "number",
|
|
13484
|
+
"required": false,
|
|
13485
|
+
"scope": "framework",
|
|
13486
|
+
"label": "Indifferent classifications",
|
|
13487
|
+
"description": "Count of survey responses classifying this feature as indifferent"
|
|
13488
|
+
}
|
|
13489
|
+
],
|
|
13490
|
+
"computed": [
|
|
13491
|
+
{
|
|
13492
|
+
"property": "satisfaction_coefficient",
|
|
13493
|
+
"expression": "(delighter_count + performance_count) / (delighter_count + performance_count + must_be_count + indifferent_count)",
|
|
13494
|
+
"label": "Satisfaction Coefficient",
|
|
13495
|
+
"format": "number"
|
|
13496
|
+
},
|
|
13497
|
+
{
|
|
13498
|
+
"property": "dissatisfaction_coefficient",
|
|
13499
|
+
"expression": "(must_be_count + performance_count) / (delighter_count + performance_count + must_be_count + indifferent_count) * -1",
|
|
13500
|
+
"label": "Dissatisfaction Coefficient",
|
|
13501
|
+
"format": "number"
|
|
13502
|
+
}
|
|
13503
|
+
]
|
|
13504
|
+
}
|
|
13505
|
+
},
|
|
13506
|
+
"structure": {
|
|
13507
|
+
"pattern": "matrix"
|
|
13508
|
+
},
|
|
13509
|
+
"presentation": {
|
|
13551
13510
|
"layout": {
|
|
13552
13511
|
"type": "matrix",
|
|
13553
13512
|
"rows": 5,
|
|
@@ -13597,16 +13556,19 @@ var UPG_FRAMEWORKS = [
|
|
|
13597
13556
|
"slots": [
|
|
13598
13557
|
{
|
|
13599
13558
|
"label": "Now",
|
|
13559
|
+
"role": "now",
|
|
13600
13560
|
"entityTypeId": "feature",
|
|
13601
13561
|
"description": "Committed work in progress"
|
|
13602
13562
|
},
|
|
13603
13563
|
{
|
|
13604
13564
|
"label": "Next",
|
|
13565
|
+
"role": "next",
|
|
13605
13566
|
"entityTypeId": "feature",
|
|
13606
13567
|
"description": "High-confidence upcoming work"
|
|
13607
13568
|
},
|
|
13608
13569
|
{
|
|
13609
13570
|
"label": "Later",
|
|
13571
|
+
"role": "later",
|
|
13610
13572
|
"entityTypeId": "initiative",
|
|
13611
13573
|
"description": "Exploratory, needs more discovery"
|
|
13612
13574
|
}
|
|
@@ -14122,26 +14084,31 @@ var UPG_FRAMEWORKS = [
|
|
|
14122
14084
|
"slots": [
|
|
14123
14085
|
{
|
|
14124
14086
|
"label": "Atoms",
|
|
14087
|
+
"role": "atom",
|
|
14125
14088
|
"entityTypeId": "design_component",
|
|
14126
14089
|
"description": "Atoms: design component entities for this dimension of the framework"
|
|
14127
14090
|
},
|
|
14128
14091
|
{
|
|
14129
14092
|
"label": "Molecules",
|
|
14093
|
+
"role": "molecule",
|
|
14130
14094
|
"entityTypeId": "design_component",
|
|
14131
14095
|
"description": "Molecules: design token entities for this dimension of the framework"
|
|
14132
14096
|
},
|
|
14133
14097
|
{
|
|
14134
14098
|
"label": "Organisms",
|
|
14099
|
+
"role": "organism",
|
|
14135
14100
|
"entityTypeId": "design_pattern",
|
|
14136
14101
|
"description": "Organisms: design pattern entities for this dimension of the framework"
|
|
14137
14102
|
},
|
|
14138
14103
|
{
|
|
14139
14104
|
"label": "Templates",
|
|
14105
|
+
"role": "template",
|
|
14140
14106
|
"entityTypeId": "wireframe",
|
|
14141
14107
|
"description": "Templates: design system entities for this dimension of the framework"
|
|
14142
14108
|
},
|
|
14143
14109
|
{
|
|
14144
14110
|
"label": "Pages",
|
|
14111
|
+
"role": "page",
|
|
14145
14112
|
"entityTypeId": "screen",
|
|
14146
14113
|
"description": "Pages: screen entities for this dimension of the framework"
|
|
14147
14114
|
}
|
|
@@ -14806,26 +14773,31 @@ var UPG_FRAMEWORKS = [
|
|
|
14806
14773
|
"slots": [
|
|
14807
14774
|
{
|
|
14808
14775
|
"label": "Outer Ring",
|
|
14776
|
+
"role": "outer_ring",
|
|
14809
14777
|
"entityTypeId": "acquisition_channel",
|
|
14810
14778
|
"description": "All 19 traction channels brainstormed"
|
|
14811
14779
|
},
|
|
14812
14780
|
{
|
|
14813
14781
|
"label": "Middle Ring",
|
|
14782
|
+
"role": "middle_ring",
|
|
14814
14783
|
"entityTypeId": "acquisition_channel",
|
|
14815
14784
|
"description": "Top 6 channels worth testing"
|
|
14816
14785
|
},
|
|
14817
14786
|
{
|
|
14818
14787
|
"label": "Inner Ring",
|
|
14788
|
+
"role": "inner_ring",
|
|
14819
14789
|
"entityTypeId": "acquisition_channel",
|
|
14820
14790
|
"description": "Top 3 channels to focus on"
|
|
14821
14791
|
},
|
|
14822
14792
|
{
|
|
14823
14793
|
"label": "Traction Test",
|
|
14794
|
+
"role": "traction_test",
|
|
14824
14795
|
"entityTypeId": "growth_campaign",
|
|
14825
14796
|
"description": "Cheap test for each channel"
|
|
14826
14797
|
},
|
|
14827
14798
|
{
|
|
14828
14799
|
"label": "Bullseye Channel",
|
|
14800
|
+
"role": "bullseye_channel",
|
|
14829
14801
|
"entityTypeId": "acquisition_channel",
|
|
14830
14802
|
"description": "The single best-performing channel"
|
|
14831
14803
|
}
|
|
@@ -14895,26 +14867,31 @@ var UPG_FRAMEWORKS = [
|
|
|
14895
14867
|
"slots": [
|
|
14896
14868
|
{
|
|
14897
14869
|
"label": "Free Entry",
|
|
14870
|
+
"role": "free_entry",
|
|
14898
14871
|
"entityTypeId": "gtm_strategy",
|
|
14899
14872
|
"description": "How users access the product for free"
|
|
14900
14873
|
},
|
|
14901
14874
|
{
|
|
14902
14875
|
"label": "Aha Moment",
|
|
14876
|
+
"role": "aha_moment",
|
|
14903
14877
|
"entityTypeId": "gtm_strategy",
|
|
14904
14878
|
"description": "First experience of core value"
|
|
14905
14879
|
},
|
|
14906
14880
|
{
|
|
14907
14881
|
"label": "Expansion Motion",
|
|
14882
|
+
"role": "expansion_motion",
|
|
14908
14883
|
"entityTypeId": "sales_motion",
|
|
14909
14884
|
"description": "How usage spreads within accounts"
|
|
14910
14885
|
},
|
|
14911
14886
|
{
|
|
14912
14887
|
"label": "Monetisation Trigger",
|
|
14888
|
+
"role": "monetisation_trigger",
|
|
14913
14889
|
"entityTypeId": "gtm_strategy",
|
|
14914
14890
|
"description": "When and how to convert to paid"
|
|
14915
14891
|
},
|
|
14916
14892
|
{
|
|
14917
14893
|
"label": "Metric",
|
|
14894
|
+
"role": "metric",
|
|
14918
14895
|
"entityTypeId": "metric",
|
|
14919
14896
|
"description": "Key metric driving the PLG motion"
|
|
14920
14897
|
}
|
|
@@ -14989,21 +14966,25 @@ var UPG_FRAMEWORKS = [
|
|
|
14989
14966
|
"slots": [
|
|
14990
14967
|
{
|
|
14991
14968
|
"label": "Objective",
|
|
14969
|
+
"role": "objective",
|
|
14992
14970
|
"entityTypeId": "objective",
|
|
14993
14971
|
"description": "Qualitative, inspiring goal"
|
|
14994
14972
|
},
|
|
14995
14973
|
{
|
|
14996
14974
|
"label": "Key Result 1",
|
|
14975
|
+
"role": "key_result",
|
|
14997
14976
|
"entityTypeId": "key_result",
|
|
14998
14977
|
"description": "Measurable outcome proving progress"
|
|
14999
14978
|
},
|
|
15000
14979
|
{
|
|
15001
14980
|
"label": "Key Result 2",
|
|
14981
|
+
"role": "key_result",
|
|
15002
14982
|
"entityTypeId": "key_result",
|
|
15003
14983
|
"description": "Measurable outcome proving progress"
|
|
15004
14984
|
},
|
|
15005
14985
|
{
|
|
15006
14986
|
"label": "Initiatives",
|
|
14987
|
+
"role": "initiative",
|
|
15007
14988
|
"entityTypeId": "initiative",
|
|
15008
14989
|
"description": "Work streams that drive key results"
|
|
15009
14990
|
}
|
|
@@ -15077,26 +15058,31 @@ var UPG_FRAMEWORKS = [
|
|
|
15077
15058
|
"slots": [
|
|
15078
15059
|
{
|
|
15079
15060
|
"label": "Activity",
|
|
15061
|
+
"role": "activity",
|
|
15080
15062
|
"entityTypeId": "key_activity",
|
|
15081
15063
|
"description": "Place persona entities in the Activity position of the matrix"
|
|
15082
15064
|
},
|
|
15083
15065
|
{
|
|
15084
15066
|
"label": "Responsible",
|
|
15067
|
+
"role": "responsible",
|
|
15085
15068
|
"entityTypeId": "role",
|
|
15086
15069
|
"description": "Does the work"
|
|
15087
15070
|
},
|
|
15088
15071
|
{
|
|
15089
15072
|
"label": "Accountable",
|
|
15073
|
+
"role": "accountable",
|
|
15090
15074
|
"entityTypeId": "role",
|
|
15091
15075
|
"description": "Makes the final call"
|
|
15092
15076
|
},
|
|
15093
15077
|
{
|
|
15094
15078
|
"label": "Consulted",
|
|
15079
|
+
"role": "consulted",
|
|
15095
15080
|
"entityTypeId": "role",
|
|
15096
15081
|
"description": "Gives input"
|
|
15097
15082
|
},
|
|
15098
15083
|
{
|
|
15099
15084
|
"label": "Informed",
|
|
15085
|
+
"role": "informed",
|
|
15100
15086
|
"entityTypeId": "role",
|
|
15101
15087
|
"description": "Kept in the loop"
|
|
15102
15088
|
}
|
|
@@ -15170,21 +15156,25 @@ var UPG_FRAMEWORKS = [
|
|
|
15170
15156
|
"slots": [
|
|
15171
15157
|
{
|
|
15172
15158
|
"label": "What Went Well",
|
|
15159
|
+
"role": "went_well",
|
|
15173
15160
|
"entityTypeId": "outcome",
|
|
15174
15161
|
"description": "Practices to continue"
|
|
15175
15162
|
},
|
|
15176
15163
|
{
|
|
15177
15164
|
"label": "What Didn't Go Well",
|
|
15165
|
+
"role": "went_poorly",
|
|
15178
15166
|
"entityTypeId": "need",
|
|
15179
15167
|
"description": "Issues to address"
|
|
15180
15168
|
},
|
|
15181
15169
|
{
|
|
15182
15170
|
"label": "Action Items",
|
|
15171
|
+
"role": "action_item",
|
|
15183
15172
|
"entityTypeId": "learning",
|
|
15184
15173
|
"description": "Changes for next iteration"
|
|
15185
15174
|
},
|
|
15186
15175
|
{
|
|
15187
15176
|
"label": "Learnings",
|
|
15177
|
+
"role": "learning",
|
|
15188
15178
|
"entityTypeId": "learning",
|
|
15189
15179
|
"description": "Insights to carry forward"
|
|
15190
15180
|
}
|
|
@@ -15381,7 +15371,7 @@ var UPG_FRAMEWORKS = [
|
|
|
15381
15371
|
"entity_types": [
|
|
15382
15372
|
{
|
|
15383
15373
|
"type": "team",
|
|
15384
|
-
"role": "
|
|
15374
|
+
"role": "item"
|
|
15385
15375
|
},
|
|
15386
15376
|
{
|
|
15387
15377
|
"type": "retrospective",
|
|
@@ -15545,7 +15535,36 @@ var UPG_FRAMEWORKS = [
|
|
|
15545
15535
|
"label": "Severity",
|
|
15546
15536
|
"format": "number"
|
|
15547
15537
|
}
|
|
15548
|
-
]
|
|
15538
|
+
],
|
|
15539
|
+
"scoring_lens": {
|
|
15540
|
+
"applies_to": [
|
|
15541
|
+
"risk"
|
|
15542
|
+
],
|
|
15543
|
+
"inputs": [
|
|
15544
|
+
{
|
|
15545
|
+
"property": "probability",
|
|
15546
|
+
"type": "number",
|
|
15547
|
+
"required": true,
|
|
15548
|
+
"label": "Probability",
|
|
15549
|
+
"description": "Likelihood the risk materialises (risk.probability assessment)"
|
|
15550
|
+
},
|
|
15551
|
+
{
|
|
15552
|
+
"property": "impact",
|
|
15553
|
+
"type": "number",
|
|
15554
|
+
"required": true,
|
|
15555
|
+
"label": "Impact",
|
|
15556
|
+
"description": "Consequence severity if the risk materialises (risk.impact assessment)"
|
|
15557
|
+
}
|
|
15558
|
+
],
|
|
15559
|
+
"computed": [
|
|
15560
|
+
{
|
|
15561
|
+
"property": "severity",
|
|
15562
|
+
"expression": "probability * impact",
|
|
15563
|
+
"label": "Severity",
|
|
15564
|
+
"format": "number"
|
|
15565
|
+
}
|
|
15566
|
+
]
|
|
15567
|
+
}
|
|
15549
15568
|
},
|
|
15550
15569
|
"structure": {
|
|
15551
15570
|
"pattern": "table"
|
|
@@ -15621,31 +15640,37 @@ var UPG_FRAMEWORKS = [
|
|
|
15621
15640
|
"slots": [
|
|
15622
15641
|
{
|
|
15623
15642
|
"label": "Items to score",
|
|
15643
|
+
"role": "candidate",
|
|
15624
15644
|
"entityTypeId": "feature",
|
|
15625
15645
|
"description": "Features or experiments being evaluated"
|
|
15626
15646
|
},
|
|
15627
15647
|
{
|
|
15628
15648
|
"label": "Impact",
|
|
15649
|
+
"role": "impact",
|
|
15629
15650
|
"entityTypeId": "outcome",
|
|
15630
15651
|
"description": "How much will this move the needle?"
|
|
15631
15652
|
},
|
|
15632
15653
|
{
|
|
15633
15654
|
"label": "Confidence",
|
|
15655
|
+
"role": "confidence",
|
|
15634
15656
|
"entityTypeId": "assumption",
|
|
15635
15657
|
"description": "How sure are we about the impact?"
|
|
15636
15658
|
},
|
|
15637
15659
|
{
|
|
15638
15660
|
"label": "Ease",
|
|
15661
|
+
"role": "ease",
|
|
15639
15662
|
"entityTypeId": "feature",
|
|
15640
15663
|
"description": "How easy is this to implement?"
|
|
15641
15664
|
},
|
|
15642
15665
|
{
|
|
15643
15666
|
"label": "Opportunities to score",
|
|
15667
|
+
"role": "candidate",
|
|
15644
15668
|
"entityTypeId": "opportunity",
|
|
15645
15669
|
"description": "Opportunities scored on the same ICE Scoring inputs as features."
|
|
15646
15670
|
},
|
|
15647
15671
|
{
|
|
15648
15672
|
"label": "Needs to score",
|
|
15673
|
+
"role": "candidate",
|
|
15649
15674
|
"entityTypeId": "need",
|
|
15650
15675
|
"description": "Needs scored on the same ICE Scoring inputs as features."
|
|
15651
15676
|
}
|
|
@@ -15775,7 +15800,48 @@ var UPG_FRAMEWORKS = [
|
|
|
15775
15800
|
"label": "ICE Score",
|
|
15776
15801
|
"format": "number"
|
|
15777
15802
|
}
|
|
15778
|
-
]
|
|
15803
|
+
],
|
|
15804
|
+
"scoring_lens": {
|
|
15805
|
+
"applies_to": [
|
|
15806
|
+
"feature",
|
|
15807
|
+
"opportunity",
|
|
15808
|
+
"need"
|
|
15809
|
+
],
|
|
15810
|
+
"inputs": [
|
|
15811
|
+
{
|
|
15812
|
+
"property": "impact",
|
|
15813
|
+
"type": "number",
|
|
15814
|
+
"required": true,
|
|
15815
|
+
"scope": "framework",
|
|
15816
|
+
"label": "Impact",
|
|
15817
|
+
"description": "Expected impact on the target metric (1-10)"
|
|
15818
|
+
},
|
|
15819
|
+
{
|
|
15820
|
+
"property": "confidence",
|
|
15821
|
+
"type": "number",
|
|
15822
|
+
"required": true,
|
|
15823
|
+
"scope": "framework",
|
|
15824
|
+
"label": "Confidence",
|
|
15825
|
+
"description": "Confidence in the impact estimate (1-10)"
|
|
15826
|
+
},
|
|
15827
|
+
{
|
|
15828
|
+
"property": "ease",
|
|
15829
|
+
"type": "number",
|
|
15830
|
+
"required": true,
|
|
15831
|
+
"scope": "framework",
|
|
15832
|
+
"label": "Ease",
|
|
15833
|
+
"description": "Ease of implementation (1-10)"
|
|
15834
|
+
}
|
|
15835
|
+
],
|
|
15836
|
+
"computed": [
|
|
15837
|
+
{
|
|
15838
|
+
"property": "ice_score",
|
|
15839
|
+
"expression": "impact * confidence * ease",
|
|
15840
|
+
"label": "ICE Score",
|
|
15841
|
+
"format": "number"
|
|
15842
|
+
}
|
|
15843
|
+
]
|
|
15844
|
+
}
|
|
15779
15845
|
},
|
|
15780
15846
|
"structure": {
|
|
15781
15847
|
"pattern": "table"
|
|
@@ -15855,31 +15921,37 @@ var UPG_FRAMEWORKS = [
|
|
|
15855
15921
|
"slots": [
|
|
15856
15922
|
{
|
|
15857
15923
|
"label": "Backlog Items",
|
|
15924
|
+
"role": "candidate",
|
|
15858
15925
|
"entityTypeId": "feature",
|
|
15859
15926
|
"description": "Backlog Items: feature entries to evaluate"
|
|
15860
15927
|
},
|
|
15861
15928
|
{
|
|
15862
15929
|
"label": "User/Business Value",
|
|
15930
|
+
"role": "user_business_value",
|
|
15863
15931
|
"entityTypeId": "metric",
|
|
15864
15932
|
"description": "User/Business Value: metric entries to evaluate"
|
|
15865
15933
|
},
|
|
15866
15934
|
{
|
|
15867
15935
|
"label": "Time Criticality",
|
|
15936
|
+
"role": "time_criticality",
|
|
15868
15937
|
"entityTypeId": "metric",
|
|
15869
15938
|
"description": "How much value decays if delivery is delayed (deadlines, competition, seasonal windows)"
|
|
15870
15939
|
},
|
|
15871
15940
|
{
|
|
15872
15941
|
"label": "Risk Reduction / Opportunity Enablement",
|
|
15942
|
+
"role": "risk_reduction",
|
|
15873
15943
|
"entityTypeId": "metric",
|
|
15874
15944
|
"description": "Risk Reduction / Opportunity Enablement: metric entries to evaluate"
|
|
15875
15945
|
},
|
|
15876
15946
|
{
|
|
15877
15947
|
"label": "Job Size",
|
|
15948
|
+
"role": "job_size",
|
|
15878
15949
|
"entityTypeId": "metric",
|
|
15879
15950
|
"description": "Estimated effort (story points, t-shirt size, or person-weeks)"
|
|
15880
15951
|
},
|
|
15881
15952
|
{
|
|
15882
15953
|
"label": "Opportunities to score",
|
|
15954
|
+
"role": "candidate",
|
|
15883
15955
|
"entityTypeId": "opportunity",
|
|
15884
15956
|
"description": "Opportunities scored on the same WSJF (Weighted Shortest Job First) inputs as features."
|
|
15885
15957
|
}
|
|
@@ -15984,7 +16056,55 @@ var UPG_FRAMEWORKS = [
|
|
|
15984
16056
|
"label": "WSJF Score",
|
|
15985
16057
|
"format": "number"
|
|
15986
16058
|
}
|
|
15987
|
-
]
|
|
16059
|
+
],
|
|
16060
|
+
"scoring_lens": {
|
|
16061
|
+
"applies_to": [
|
|
16062
|
+
"feature",
|
|
16063
|
+
"opportunity"
|
|
16064
|
+
],
|
|
16065
|
+
"inputs": [
|
|
16066
|
+
{
|
|
16067
|
+
"property": "user_value",
|
|
16068
|
+
"type": "number",
|
|
16069
|
+
"required": true,
|
|
16070
|
+
"scope": "framework",
|
|
16071
|
+
"label": "User/Business Value",
|
|
16072
|
+
"description": "Relative value to users and the business if delivered"
|
|
16073
|
+
},
|
|
16074
|
+
{
|
|
16075
|
+
"property": "time_criticality",
|
|
16076
|
+
"type": "number",
|
|
16077
|
+
"required": true,
|
|
16078
|
+
"scope": "framework",
|
|
16079
|
+
"label": "Time Criticality",
|
|
16080
|
+
"description": "How much value decays if delivery is delayed (deadlines, competition, seasonal windows)"
|
|
16081
|
+
},
|
|
16082
|
+
{
|
|
16083
|
+
"property": "risk_reduction",
|
|
16084
|
+
"type": "number",
|
|
16085
|
+
"required": true,
|
|
16086
|
+
"scope": "framework",
|
|
16087
|
+
"label": "Risk Reduction / Opportunity Enablement",
|
|
16088
|
+
"description": "Value from reducing risk or enabling future opportunities"
|
|
16089
|
+
},
|
|
16090
|
+
{
|
|
16091
|
+
"property": "job_size",
|
|
16092
|
+
"type": "number",
|
|
16093
|
+
"required": true,
|
|
16094
|
+
"scope": "framework",
|
|
16095
|
+
"label": "Job Size",
|
|
16096
|
+
"description": "Estimated effort (story points, t-shirt size, or person-weeks)"
|
|
16097
|
+
}
|
|
16098
|
+
],
|
|
16099
|
+
"computed": [
|
|
16100
|
+
{
|
|
16101
|
+
"property": "wsjf_score",
|
|
16102
|
+
"expression": "(user_value + time_criticality + risk_reduction) / job_size",
|
|
16103
|
+
"label": "WSJF Score",
|
|
16104
|
+
"format": "number"
|
|
16105
|
+
}
|
|
16106
|
+
]
|
|
16107
|
+
}
|
|
15988
16108
|
},
|
|
15989
16109
|
"structure": {
|
|
15990
16110
|
"pattern": "table"
|
|
@@ -16057,7 +16177,7 @@ var UPG_FRAMEWORKS = [
|
|
|
16057
16177
|
],
|
|
16058
16178
|
"name": "Cost of Delay",
|
|
16059
16179
|
"version": "1.0.0",
|
|
16060
|
-
"description": "Quantify the economic cost of not shipping a feature to drive priority decisions. Combines urgency with value.",
|
|
16180
|
+
"description": "Quantify the economic cost of not shipping a feature or opportunity to drive priority decisions. Combines urgency with value.",
|
|
16061
16181
|
"category": "prioritization",
|
|
16062
16182
|
"origin": {
|
|
16063
16183
|
"type": "practitioner",
|
|
@@ -16173,7 +16293,39 @@ var UPG_FRAMEWORKS = [
|
|
|
16173
16293
|
"label": "WSJF Score",
|
|
16174
16294
|
"format": "number"
|
|
16175
16295
|
}
|
|
16176
|
-
]
|
|
16296
|
+
],
|
|
16297
|
+
"scoring_lens": {
|
|
16298
|
+
"applies_to": [
|
|
16299
|
+
"feature",
|
|
16300
|
+
"opportunity"
|
|
16301
|
+
],
|
|
16302
|
+
"inputs": [
|
|
16303
|
+
{
|
|
16304
|
+
"property": "cost_of_delay",
|
|
16305
|
+
"type": "number",
|
|
16306
|
+
"required": true,
|
|
16307
|
+
"scope": "framework",
|
|
16308
|
+
"label": "Cost of Delay",
|
|
16309
|
+
"description": "Weekly revenue impact of not shipping"
|
|
16310
|
+
},
|
|
16311
|
+
{
|
|
16312
|
+
"property": "job_size",
|
|
16313
|
+
"type": "number",
|
|
16314
|
+
"required": true,
|
|
16315
|
+
"scope": "framework",
|
|
16316
|
+
"label": "Job Size",
|
|
16317
|
+
"description": "Weeks of development effort"
|
|
16318
|
+
}
|
|
16319
|
+
],
|
|
16320
|
+
"computed": [
|
|
16321
|
+
{
|
|
16322
|
+
"property": "wsjf_score",
|
|
16323
|
+
"expression": "cost_of_delay / job_size",
|
|
16324
|
+
"label": "WSJF Score",
|
|
16325
|
+
"format": "number"
|
|
16326
|
+
}
|
|
16327
|
+
]
|
|
16328
|
+
}
|
|
16177
16329
|
},
|
|
16178
16330
|
"structure": {
|
|
16179
16331
|
"pattern": "table"
|
|
@@ -16255,16 +16407,19 @@ var UPG_FRAMEWORKS = [
|
|
|
16255
16407
|
"slots": [
|
|
16256
16408
|
{
|
|
16257
16409
|
"label": "Symptom",
|
|
16410
|
+
"role": "symptom",
|
|
16258
16411
|
"entityTypeId": "need",
|
|
16259
16412
|
"description": "The observed problem the analysis starts from."
|
|
16260
16413
|
},
|
|
16261
16414
|
{
|
|
16262
16415
|
"label": "Why chain",
|
|
16416
|
+
"role": "why",
|
|
16263
16417
|
"entityTypeId": "insight",
|
|
16264
16418
|
"description": 'Each "why?" answer along the chain, typically five iterations deep.'
|
|
16265
16419
|
},
|
|
16266
16420
|
{
|
|
16267
16421
|
"label": "Root cause",
|
|
16422
|
+
"role": "root_cause",
|
|
16268
16423
|
"entityTypeId": "insight",
|
|
16269
16424
|
"description": "The terminal answer at the bottom of the chain: the underlying cause to address."
|
|
16270
16425
|
}
|
|
@@ -16609,16 +16764,19 @@ var UPG_FRAMEWORKS = [
|
|
|
16609
16764
|
"slots": [
|
|
16610
16765
|
{
|
|
16611
16766
|
"label": "First-order move",
|
|
16767
|
+
"role": "first_order",
|
|
16612
16768
|
"entityTypeId": "decision",
|
|
16613
16769
|
"description": "The decision or move under consideration."
|
|
16614
16770
|
},
|
|
16615
16771
|
{
|
|
16616
16772
|
"label": "Second-order consequences",
|
|
16773
|
+
"role": "second_order",
|
|
16617
16774
|
"entityTypeId": "insight",
|
|
16618
16775
|
"description": "Downstream effects that follow from the first-order move."
|
|
16619
16776
|
},
|
|
16620
16777
|
{
|
|
16621
16778
|
"label": "Higher-order consequences",
|
|
16779
|
+
"role": "higher_order",
|
|
16622
16780
|
"entityTypeId": "insight",
|
|
16623
16781
|
"description": "Third-, fourth-, fifth-order ripples: second-order consequences of the second-order consequences."
|
|
16624
16782
|
}
|
|
@@ -16678,6 +16836,439 @@ for (const fw of UPG_FRAMEWORKS) {
|
|
|
16678
16836
|
UPG_FRAMEWORKS_BY_CATEGORY[fw.category].push(fw);
|
|
16679
16837
|
}
|
|
16680
16838
|
|
|
16839
|
+
// src/grammar/validate.ts
|
|
16840
|
+
var crossProductEdgeTypeSet = new Set(UPG_CROSS_EDGE_TYPES);
|
|
16841
|
+
var CONTENT_DEPTH_WARNING_RULES = /* @__PURE__ */ new Set([
|
|
16842
|
+
"property-type",
|
|
16843
|
+
"property-enum",
|
|
16844
|
+
"self-loop",
|
|
16845
|
+
"framework-score"
|
|
16846
|
+
]);
|
|
16847
|
+
function validateUPGDocument(doc) {
|
|
16848
|
+
const errors = [];
|
|
16849
|
+
const warnings = [];
|
|
16850
|
+
if (!doc || typeof doc !== "object") {
|
|
16851
|
+
return { valid: false, errors: [{ path: "$", message: "Document must be an object" }], warnings };
|
|
16852
|
+
}
|
|
16853
|
+
const d = doc;
|
|
16854
|
+
if (!d.upg_version || typeof d.upg_version !== "string") {
|
|
16855
|
+
errors.push({ path: "$.upg_version", message: "upg_version is required and must be a string" });
|
|
16856
|
+
}
|
|
16857
|
+
if (!d.exported_at || typeof d.exported_at !== "string") {
|
|
16858
|
+
errors.push({ path: "$.exported_at", message: "exported_at is required and must be an ISO 8601 string" });
|
|
16859
|
+
}
|
|
16860
|
+
if (!d.source || typeof d.source !== "object") {
|
|
16861
|
+
errors.push({ path: "$.source", message: "source is required and must be an object" });
|
|
16862
|
+
} else {
|
|
16863
|
+
const source = d.source;
|
|
16864
|
+
if (!source.tool || typeof source.tool !== "string") {
|
|
16865
|
+
errors.push({ path: "$.source.tool", message: "source.tool is required and must be a string" });
|
|
16866
|
+
}
|
|
16867
|
+
}
|
|
16868
|
+
if (!d.product || typeof d.product !== "object") {
|
|
16869
|
+
errors.push({ path: "$.product", message: "product is required and must be an object" });
|
|
16870
|
+
} else {
|
|
16871
|
+
const product = d.product;
|
|
16872
|
+
if (!product.id || typeof product.id !== "string") {
|
|
16873
|
+
errors.push({ path: "$.product.id", message: "product.id is required and must be a string" });
|
|
16874
|
+
}
|
|
16875
|
+
if (!product.title || typeof product.title !== "string") {
|
|
16876
|
+
errors.push({ path: "$.product.title", message: "product.title is required and must be a string" });
|
|
16877
|
+
}
|
|
16878
|
+
}
|
|
16879
|
+
const allKnownTypes = new Set(getTypes());
|
|
16880
|
+
const allKnownEdgeTypes = new Set(Object.keys(UPG_EDGE_CATALOG));
|
|
16881
|
+
if (!Array.isArray(d.nodes)) {
|
|
16882
|
+
errors.push({
|
|
16883
|
+
path: "$.nodes",
|
|
16884
|
+
message: d.nodes === void 0 ? "nodes is required and must be an array" : `nodes must be an array, got ${describeKind(d.nodes)}`
|
|
16885
|
+
});
|
|
16886
|
+
} else {
|
|
16887
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
16888
|
+
const nodesById = /* @__PURE__ */ new Map();
|
|
16889
|
+
d.nodes.forEach((node, i) => {
|
|
16890
|
+
const path = `$.nodes[${i}]`;
|
|
16891
|
+
if (!node || typeof node !== "object") {
|
|
16892
|
+
errors.push({ path, message: "Each node must be an object" });
|
|
16893
|
+
return;
|
|
16894
|
+
}
|
|
16895
|
+
const n = node;
|
|
16896
|
+
if (!n.id || typeof n.id !== "string") {
|
|
16897
|
+
errors.push({ path: `${path}.id`, message: "Node id is required and must be a string" });
|
|
16898
|
+
} else {
|
|
16899
|
+
if (nodeIds.has(n.id)) {
|
|
16900
|
+
errors.push({ path: `${path}.id`, message: `Duplicate node id: ${n.id}` });
|
|
16901
|
+
}
|
|
16902
|
+
nodeIds.add(n.id);
|
|
16903
|
+
nodesById.set(n.id, n);
|
|
16904
|
+
}
|
|
16905
|
+
if (!n.type || typeof n.type !== "string") {
|
|
16906
|
+
errors.push({ path: `${path}.type`, message: "Node type is required and must be a string" });
|
|
16907
|
+
} else if (!allKnownTypes.has(n.type)) {
|
|
16908
|
+
warnings.push({ path: `${path}.type`, message: `Unknown UPG type: "${n.type}". Node will be preserved with its original type.` });
|
|
16909
|
+
}
|
|
16910
|
+
if (!n.title || typeof n.title !== "string") {
|
|
16911
|
+
errors.push({ path: `${path}.title`, message: "Node title is required and must be a string" });
|
|
16912
|
+
} else if (n.title.trim().length === 0) {
|
|
16913
|
+
errors.push({ path: `${path}.title`, message: "Node title must not be blank (whitespace-only)" });
|
|
16914
|
+
}
|
|
16915
|
+
if (n.properties !== void 0 && n.properties !== null) {
|
|
16916
|
+
if (typeof n.properties !== "object" || Array.isArray(n.properties)) {
|
|
16917
|
+
errors.push({
|
|
16918
|
+
path: `${path}.properties`,
|
|
16919
|
+
message: `Node properties must be a plain object when present, got ${describeKind(n.properties)}.`
|
|
16920
|
+
});
|
|
16921
|
+
}
|
|
16922
|
+
}
|
|
16923
|
+
if (typeof n.type === "string") {
|
|
16924
|
+
validateNodeProperties(n, `${path}`, warnings);
|
|
16925
|
+
}
|
|
16926
|
+
});
|
|
16927
|
+
if (!Array.isArray(d.edges)) {
|
|
16928
|
+
errors.push({
|
|
16929
|
+
path: "$.edges",
|
|
16930
|
+
message: d.edges === void 0 ? "edges is required and must be an array" : `edges must be an array, got ${describeKind(d.edges)}`
|
|
16931
|
+
});
|
|
16932
|
+
} else {
|
|
16933
|
+
d.edges.forEach((edge, i) => {
|
|
16934
|
+
const path = `$.edges[${i}]`;
|
|
16935
|
+
if (!edge || typeof edge !== "object") {
|
|
16936
|
+
errors.push({ path, message: "Each edge must be an object" });
|
|
16937
|
+
return;
|
|
16938
|
+
}
|
|
16939
|
+
const e = edge;
|
|
16940
|
+
if (!e.id || typeof e.id !== "string") {
|
|
16941
|
+
errors.push({ path: `${path}.id`, message: "Edge id is required and must be a string" });
|
|
16942
|
+
}
|
|
16943
|
+
if (typeof e.type === "string" && crossProductEdgeTypeSet.has(e.type)) {
|
|
16944
|
+
errors.push({
|
|
16945
|
+
path: `${path}.type`,
|
|
16946
|
+
message: `Cross-product edge type "${e.type}" must live in portfolio.cross_edges[], not product edges[].`
|
|
16947
|
+
});
|
|
16948
|
+
return;
|
|
16949
|
+
}
|
|
16950
|
+
if (!e.source || typeof e.source !== "string") {
|
|
16951
|
+
errors.push({ path: `${path}.source`, message: "Edge source is required and must be a string" });
|
|
16952
|
+
} else if (!nodeIds.has(e.source)) {
|
|
16953
|
+
errors.push({ path: `${path}.source`, message: `Edge source references unknown node id: ${e.source}` });
|
|
16954
|
+
}
|
|
16955
|
+
if (!e.target || typeof e.target !== "string") {
|
|
16956
|
+
errors.push({ path: `${path}.target`, message: "Edge target is required and must be a string" });
|
|
16957
|
+
} else if (!nodeIds.has(e.target)) {
|
|
16958
|
+
errors.push({ path: `${path}.target`, message: `Edge target references unknown node id: ${e.target}` });
|
|
16959
|
+
}
|
|
16960
|
+
if (typeof e.source === "string" && typeof e.target === "string" && e.source === e.target) {
|
|
16961
|
+
warnings.push({
|
|
16962
|
+
path: `${path}`,
|
|
16963
|
+
message: `Self-loop edge: source and target are the same node ("${e.source}"). A node related to itself is almost always an error.`,
|
|
16964
|
+
rule: "self-loop"
|
|
16965
|
+
});
|
|
16966
|
+
}
|
|
16967
|
+
if (!e.type || typeof e.type !== "string") {
|
|
16968
|
+
errors.push({ path: `${path}.type`, message: "Edge type is required and must be a string" });
|
|
16969
|
+
} else if (!allKnownEdgeTypes.has(e.type)) {
|
|
16970
|
+
warnings.push({
|
|
16971
|
+
path: `${path}.type`,
|
|
16972
|
+
message: `Unknown edge type: "${e.type}". Edge preserved but not in the edge registry.`
|
|
16973
|
+
});
|
|
16974
|
+
}
|
|
16975
|
+
});
|
|
16976
|
+
const valueToAxis = /* @__PURE__ */ new Map();
|
|
16977
|
+
const edgesArr = d.edges;
|
|
16978
|
+
for (const edge of edgesArr) {
|
|
16979
|
+
if (!edge || typeof edge !== "object") continue;
|
|
16980
|
+
const e = edge;
|
|
16981
|
+
if (e.type === "classification_axis_includes_classification_value" && typeof e.source === "string" && typeof e.target === "string") {
|
|
16982
|
+
valueToAxis.set(e.target, e.source);
|
|
16983
|
+
}
|
|
16984
|
+
}
|
|
16985
|
+
for (const [id, n] of nodesById.entries()) {
|
|
16986
|
+
if (n.type !== "classification_value") continue;
|
|
16987
|
+
if (valueToAxis.has(id)) continue;
|
|
16988
|
+
const pid = n.parent_id;
|
|
16989
|
+
if (typeof pid === "string") {
|
|
16990
|
+
const parent = nodesById.get(pid);
|
|
16991
|
+
if (parent && parent.type === "classification_axis") {
|
|
16992
|
+
valueToAxis.set(id, pid);
|
|
16993
|
+
}
|
|
16994
|
+
}
|
|
16995
|
+
}
|
|
16996
|
+
const axesPerAnalysis = /* @__PURE__ */ new Map();
|
|
16997
|
+
for (const edge of edgesArr) {
|
|
16998
|
+
if (!edge || typeof edge !== "object") continue;
|
|
16999
|
+
const e = edge;
|
|
17000
|
+
if (e.type === "competitive_analysis_dimensioned_by_classification_axis" && typeof e.source === "string") {
|
|
17001
|
+
axesPerAnalysis.set(e.source, (axesPerAnalysis.get(e.source) ?? 0) + 1);
|
|
17002
|
+
}
|
|
17003
|
+
}
|
|
17004
|
+
d.nodes.forEach((node, i) => {
|
|
17005
|
+
if (!node || typeof node !== "object") return;
|
|
17006
|
+
const n = node;
|
|
17007
|
+
if (n.type !== "competitive_analysis") return;
|
|
17008
|
+
const props = n.properties;
|
|
17009
|
+
if (!props || typeof props !== "object") return;
|
|
17010
|
+
const empty = props.empty_cells;
|
|
17011
|
+
if (!Array.isArray(empty)) return;
|
|
17012
|
+
const axisCount = axesPerAnalysis.get(n.id ?? "") ?? 0;
|
|
17013
|
+
if (axisCount !== 2) {
|
|
17014
|
+
warnings.push({
|
|
17015
|
+
path: `$.nodes[${i}].properties.empty_cells`,
|
|
17016
|
+
message: `empty_cells is meaningful only on a 2-axis competitive_analysis; this node is dimensioned by ${axisCount} classification_axis ${axisCount === 1 ? "child" : "children"}.`
|
|
17017
|
+
});
|
|
17018
|
+
}
|
|
17019
|
+
empty.forEach((cell, j) => {
|
|
17020
|
+
const cpath = `$.nodes[${i}].properties.empty_cells[${j}]`;
|
|
17021
|
+
if (!cell || typeof cell !== "object") {
|
|
17022
|
+
errors.push({ path: cpath, message: "empty_cells entry must be an object" });
|
|
17023
|
+
return;
|
|
17024
|
+
}
|
|
17025
|
+
const c = cell;
|
|
17026
|
+
const aRef = c.axis_a_value_ref;
|
|
17027
|
+
const bRef = c.axis_b_value_ref;
|
|
17028
|
+
if (typeof aRef !== "string") {
|
|
17029
|
+
errors.push({ path: `${cpath}.axis_a_value_ref`, message: "axis_a_value_ref is required and must be a string" });
|
|
17030
|
+
}
|
|
17031
|
+
if (typeof bRef !== "string") {
|
|
17032
|
+
errors.push({ path: `${cpath}.axis_b_value_ref`, message: "axis_b_value_ref is required and must be a string" });
|
|
17033
|
+
}
|
|
17034
|
+
if (typeof aRef !== "string" || typeof bRef !== "string") return;
|
|
17035
|
+
const aNode = nodesById.get(aRef);
|
|
17036
|
+
const bNode = nodesById.get(bRef);
|
|
17037
|
+
if (!aNode) {
|
|
17038
|
+
errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Unknown node id: ${aRef}` });
|
|
17039
|
+
} else if (aNode.type !== "classification_value") {
|
|
17040
|
+
errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Expected classification_value, found "${String(aNode.type)}" at ${aRef}` });
|
|
17041
|
+
}
|
|
17042
|
+
if (!bNode) {
|
|
17043
|
+
errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Unknown node id: ${bRef}` });
|
|
17044
|
+
} else if (bNode.type !== "classification_value") {
|
|
17045
|
+
errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Expected classification_value, found "${String(bNode.type)}" at ${bRef}` });
|
|
17046
|
+
}
|
|
17047
|
+
if (aNode && bNode && aNode.type === "classification_value" && bNode.type === "classification_value") {
|
|
17048
|
+
const aAxis = valueToAxis.get(aRef);
|
|
17049
|
+
const bAxis = valueToAxis.get(bRef);
|
|
17050
|
+
if (aAxis && bAxis && aAxis === bAxis) {
|
|
17051
|
+
errors.push({
|
|
17052
|
+
path: cpath,
|
|
17053
|
+
message: `empty_cells references two classification_values from the same axis (${aAxis}); axis_a and axis_b must come from distinct classification_axis parents.`
|
|
17054
|
+
});
|
|
17055
|
+
}
|
|
17056
|
+
}
|
|
17057
|
+
const kind = c.rationale_kind;
|
|
17058
|
+
if (kind !== void 0 && kind !== "structural" && kind !== "ideological" && kind !== "opportunity") {
|
|
17059
|
+
errors.push({
|
|
17060
|
+
path: `${cpath}.rationale_kind`,
|
|
17061
|
+
message: `rationale_kind must be one of 'structural' | 'ideological' | 'opportunity'; got "${String(kind)}".`
|
|
17062
|
+
});
|
|
17063
|
+
}
|
|
17064
|
+
});
|
|
17065
|
+
});
|
|
17066
|
+
validateFrameworkScores(d.nodes, d.edges, warnings);
|
|
17067
|
+
}
|
|
17068
|
+
}
|
|
17069
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
17070
|
+
}
|
|
17071
|
+
function validateFrameworkScores(nodes, edges, warnings) {
|
|
17072
|
+
const nodeById = /* @__PURE__ */ new Map();
|
|
17073
|
+
const exercises = [];
|
|
17074
|
+
for (const node of nodes) {
|
|
17075
|
+
if (!node || typeof node !== "object") continue;
|
|
17076
|
+
const n = node;
|
|
17077
|
+
if (typeof n.id === "string") nodeById.set(n.id, n);
|
|
17078
|
+
if (n.type !== "framework_exercise") continue;
|
|
17079
|
+
const props = n.properties;
|
|
17080
|
+
if (!props || typeof props !== "object" || Array.isArray(props)) continue;
|
|
17081
|
+
const frameworkId = props.framework_id;
|
|
17082
|
+
if (typeof frameworkId !== "string") continue;
|
|
17083
|
+
const framework = UPG_FRAMEWORKS_BY_ID[frameworkId];
|
|
17084
|
+
if (!framework) continue;
|
|
17085
|
+
if (typeof n.id === "string") exercises.push({ id: n.id, framework });
|
|
17086
|
+
}
|
|
17087
|
+
if (exercises.length === 0) return;
|
|
17088
|
+
const exerciseById = new Map(exercises.map((e) => [e.id, e.framework]));
|
|
17089
|
+
edges.forEach((edge, i) => {
|
|
17090
|
+
if (!edge || typeof edge !== "object") return;
|
|
17091
|
+
const e = edge;
|
|
17092
|
+
if (e.type !== "framework_exercise_includes_node") return;
|
|
17093
|
+
if (typeof e.source !== "string" || typeof e.target !== "string") return;
|
|
17094
|
+
const framework = exerciseById.get(e.source);
|
|
17095
|
+
if (!framework) return;
|
|
17096
|
+
const score = e.properties;
|
|
17097
|
+
if (!score || typeof score !== "object" || Array.isArray(score)) return;
|
|
17098
|
+
const targetNode = nodeById.get(e.target);
|
|
17099
|
+
const targetType = typeof targetNode?.type === "string" ? targetNode.type : void 0;
|
|
17100
|
+
if (!targetType) return;
|
|
17101
|
+
const requirements = framework.data?.required_properties?.[targetType];
|
|
17102
|
+
if (!requirements || requirements.length === 0) return;
|
|
17103
|
+
const divisors = divisorInputs(framework);
|
|
17104
|
+
const path = `$.edges[${i}].properties`;
|
|
17105
|
+
const scoreObj = score;
|
|
17106
|
+
for (const req of requirements) {
|
|
17107
|
+
const value = scoreObj[req.property];
|
|
17108
|
+
if (value === void 0 || value === null) continue;
|
|
17109
|
+
const finding = checkScoreValue(req, value, divisors.has(req.property), framework.id);
|
|
17110
|
+
if (finding) {
|
|
17111
|
+
warnings.push({
|
|
17112
|
+
path: `${path}.${req.property}`,
|
|
17113
|
+
message: finding,
|
|
17114
|
+
rule: "framework-score"
|
|
17115
|
+
});
|
|
17116
|
+
}
|
|
17117
|
+
}
|
|
17118
|
+
});
|
|
17119
|
+
}
|
|
17120
|
+
function divisorInputs(framework) {
|
|
17121
|
+
const expr = framework.data?.computed_properties?.[0]?.expression;
|
|
17122
|
+
if (typeof expr !== "string" || expr.length === 0) return EMPTY_STRING_SET;
|
|
17123
|
+
const out = /* @__PURE__ */ new Set();
|
|
17124
|
+
const re = /\/\s*\(?\s*([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
17125
|
+
let m;
|
|
17126
|
+
while ((m = re.exec(expr)) !== null) out.add(m[1]);
|
|
17127
|
+
return out;
|
|
17128
|
+
}
|
|
17129
|
+
var EMPTY_STRING_SET = /* @__PURE__ */ new Set();
|
|
17130
|
+
function checkScoreValue(req, value, isDivisor, frameworkId) {
|
|
17131
|
+
switch (req.type) {
|
|
17132
|
+
case "enum": {
|
|
17133
|
+
const allowed = req.enum_values ?? [];
|
|
17134
|
+
if (typeof value !== "string") {
|
|
17135
|
+
return `${frameworkId} score "${req.property}" must be one of ${allowed.join(", ")}; got ${describeKind(value)}.`;
|
|
17136
|
+
}
|
|
17137
|
+
if (allowed.length > 0 && !allowed.includes(value)) {
|
|
17138
|
+
return `${frameworkId} score "${req.property}" has value "${value}" outside its allowed set: ${allowed.join(", ")}.`;
|
|
17139
|
+
}
|
|
17140
|
+
return null;
|
|
17141
|
+
}
|
|
17142
|
+
case "assessment": {
|
|
17143
|
+
const num = numericScore(value);
|
|
17144
|
+
if (num === null) {
|
|
17145
|
+
return `${frameworkId} score "${req.property}" must be a number; got ${describeKind(value)}.`;
|
|
17146
|
+
}
|
|
17147
|
+
const scale = req.scale_id ? getScale(req.scale_id) : void 0;
|
|
17148
|
+
if (scale) {
|
|
17149
|
+
if (num < scale.min || num > scale.max) {
|
|
17150
|
+
return `${frameworkId} score "${req.property}" is ${num}, outside the ${req.scale_id} scale range [${scale.min}, ${scale.max}].`;
|
|
17151
|
+
}
|
|
17152
|
+
} else if (num <= 0) {
|
|
17153
|
+
return `${frameworkId} score "${req.property}" must be greater than 0; got ${num}.`;
|
|
17154
|
+
}
|
|
17155
|
+
if (isDivisor && num === 0) {
|
|
17156
|
+
return `${frameworkId} score "${req.property}" must not be 0 (it is a divisor in the framework's formula).`;
|
|
17157
|
+
}
|
|
17158
|
+
return null;
|
|
17159
|
+
}
|
|
17160
|
+
case "number": {
|
|
17161
|
+
const num = numericScore(value);
|
|
17162
|
+
if (num === null) {
|
|
17163
|
+
return `${frameworkId} score "${req.property}" must be a number; got ${describeKind(value)}.`;
|
|
17164
|
+
}
|
|
17165
|
+
if (isDivisor && num === 0) {
|
|
17166
|
+
return `${frameworkId} score "${req.property}" must not be 0 (it is a divisor in the framework's formula).`;
|
|
17167
|
+
}
|
|
17168
|
+
if (num < 0) {
|
|
17169
|
+
return `${frameworkId} score "${req.property}" must not be negative; got ${num}.`;
|
|
17170
|
+
}
|
|
17171
|
+
return null;
|
|
17172
|
+
}
|
|
17173
|
+
case "boolean": {
|
|
17174
|
+
if (typeof value !== "boolean") {
|
|
17175
|
+
return `${frameworkId} score "${req.property}" must be a boolean; got ${describeKind(value)}.`;
|
|
17176
|
+
}
|
|
17177
|
+
return null;
|
|
17178
|
+
}
|
|
17179
|
+
case "string": {
|
|
17180
|
+
if (typeof value !== "string") {
|
|
17181
|
+
return `${frameworkId} score "${req.property}" must be a string; got ${describeKind(value)}.`;
|
|
17182
|
+
}
|
|
17183
|
+
return null;
|
|
17184
|
+
}
|
|
17185
|
+
default:
|
|
17186
|
+
return null;
|
|
17187
|
+
}
|
|
17188
|
+
}
|
|
17189
|
+
function numericScore(value) {
|
|
17190
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
17191
|
+
if (typeof value === "string") {
|
|
17192
|
+
const n = Number.parseFloat(value);
|
|
17193
|
+
return Number.isFinite(n) ? n : null;
|
|
17194
|
+
}
|
|
17195
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
17196
|
+
const v = value.value;
|
|
17197
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : null;
|
|
17198
|
+
}
|
|
17199
|
+
return null;
|
|
17200
|
+
}
|
|
17201
|
+
function runtimeKind(val) {
|
|
17202
|
+
if (val === null || val === void 0) return null;
|
|
17203
|
+
if (Array.isArray(val)) return "string[]";
|
|
17204
|
+
switch (typeof val) {
|
|
17205
|
+
case "string":
|
|
17206
|
+
return "string";
|
|
17207
|
+
case "number":
|
|
17208
|
+
return "number";
|
|
17209
|
+
case "boolean":
|
|
17210
|
+
return "boolean";
|
|
17211
|
+
case "object":
|
|
17212
|
+
return "object";
|
|
17213
|
+
default:
|
|
17214
|
+
return null;
|
|
17215
|
+
}
|
|
17216
|
+
}
|
|
17217
|
+
function valueMatchesType(defType, val) {
|
|
17218
|
+
const kind = runtimeKind(val);
|
|
17219
|
+
if (kind === null) return true;
|
|
17220
|
+
switch (defType) {
|
|
17221
|
+
case "string":
|
|
17222
|
+
return kind === "string";
|
|
17223
|
+
case "number":
|
|
17224
|
+
return kind === "number";
|
|
17225
|
+
case "boolean":
|
|
17226
|
+
return kind === "boolean";
|
|
17227
|
+
case "string[]":
|
|
17228
|
+
return kind === "string[]";
|
|
17229
|
+
case "object":
|
|
17230
|
+
case "assessment":
|
|
17231
|
+
return kind === "object";
|
|
17232
|
+
default:
|
|
17233
|
+
return true;
|
|
17234
|
+
}
|
|
17235
|
+
}
|
|
17236
|
+
function describeKind(val) {
|
|
17237
|
+
if (Array.isArray(val)) return "array";
|
|
17238
|
+
return typeof val;
|
|
17239
|
+
}
|
|
17240
|
+
function validateNodeProperties(node, basePath, warnings) {
|
|
17241
|
+
const type = node.type;
|
|
17242
|
+
if (typeof type !== "string") return;
|
|
17243
|
+
const schema = getPropertySchema(type);
|
|
17244
|
+
if (!schema) return;
|
|
17245
|
+
const props = node.properties;
|
|
17246
|
+
if (!props || typeof props !== "object" || Array.isArray(props)) return;
|
|
17247
|
+
for (const [key, value] of Object.entries(props)) {
|
|
17248
|
+
const def = schema[key];
|
|
17249
|
+
if (!def) continue;
|
|
17250
|
+
if (value === null || value === void 0) continue;
|
|
17251
|
+
if (!valueMatchesType(def.type, value)) {
|
|
17252
|
+
warnings.push({
|
|
17253
|
+
path: `${basePath}.properties.${key}`,
|
|
17254
|
+
message: `Property "${key}" on ${type} should be ${def.type}, got ${describeKind(value)}.`,
|
|
17255
|
+
rule: "property-type"
|
|
17256
|
+
});
|
|
17257
|
+
continue;
|
|
17258
|
+
}
|
|
17259
|
+
if (def.enum && typeof value === "string" && !def.enum.includes(value)) {
|
|
17260
|
+
warnings.push({
|
|
17261
|
+
path: `${basePath}.properties.${key}`,
|
|
17262
|
+
message: `Property "${key}" on ${type} has value "${value}" outside its allowed set: ${def.enum.join(", ")}.`,
|
|
17263
|
+
rule: "property-enum"
|
|
17264
|
+
});
|
|
17265
|
+
}
|
|
17266
|
+
}
|
|
17267
|
+
}
|
|
17268
|
+
function isUPGDocument(doc) {
|
|
17269
|
+
return validateUPGDocument(doc).valid;
|
|
17270
|
+
}
|
|
17271
|
+
|
|
16681
17272
|
// src/presentation/labels.ts
|
|
16682
17273
|
var PRIORITY_LABELS = [
|
|
16683
17274
|
// ── need (CONSOLIDATED: replaces pain_point + user_need) ─────────────────────
|
|
@@ -25074,7 +25665,7 @@ function isCanonical(text) {
|
|
|
25074
25665
|
}
|
|
25075
25666
|
|
|
25076
25667
|
// src/index.ts
|
|
25077
|
-
var UPG_VERSION = "0.8.
|
|
25668
|
+
var UPG_VERSION = "0.8.7";
|
|
25078
25669
|
var UPG_FORMAT_VERSION = "0.4.0";
|
|
25079
25670
|
var MARKDOWN_FORMAT_VERSION = "0.1";
|
|
25080
25671
|
var UPG_TYPES = getTypes();
|
|
@@ -25142,6 +25733,7 @@ export {
|
|
|
25142
25733
|
BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK,
|
|
25143
25734
|
BUSINESS_GTM_GROWTH_PLAYBOOK,
|
|
25144
25735
|
BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK,
|
|
25736
|
+
CONTENT_DEPTH_WARNING_RULES,
|
|
25145
25737
|
DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK,
|
|
25146
25738
|
ENGINEERING_PLATFORM_PLAYBOOK,
|
|
25147
25739
|
EXPERIENCE_DESIGN_BRAND_PLAYBOOK,
|