@unified-product-graph/core 0.8.5 → 0.8.7
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 +12 -0
- package/dist/index.cjs +801 -265
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -3
- package/dist/index.d.ts +71 -3
- package/dist/index.js +800 -265
- 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.
|
|
@@ -11836,59 +11633,346 @@ function getPropertySchema(entityType) {
|
|
|
11836
11633
|
return UPG_PROPERTY_SCHEMA[entityType];
|
|
11837
11634
|
}
|
|
11838
11635
|
|
|
11839
|
-
// src/
|
|
11840
|
-
var
|
|
11841
|
-
|
|
11842
|
-
|
|
11843
|
-
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
|
|
11849
|
-
|
|
11850
|
-
"
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
|
|
11854
|
-
|
|
11855
|
-
|
|
11856
|
-
|
|
11857
|
-
}
|
|
11858
|
-
|
|
11859
|
-
|
|
11860
|
-
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
|
|
11870
|
-
|
|
11871
|
-
|
|
11872
|
-
}
|
|
11873
|
-
|
|
11874
|
-
|
|
11875
|
-
|
|
11876
|
-
|
|
11877
|
-
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
|
|
11883
|
-
|
|
11884
|
-
|
|
11885
|
-
|
|
11886
|
-
|
|
11887
|
-
|
|
11888
|
-
|
|
11889
|
-
|
|
11890
|
-
|
|
11891
|
-
|
|
11636
|
+
// src/grammar/validate.ts
|
|
11637
|
+
var crossProductEdgeTypeSet = new Set(UPG_CROSS_EDGE_TYPES);
|
|
11638
|
+
var CONTENT_DEPTH_WARNING_RULES = /* @__PURE__ */ new Set([
|
|
11639
|
+
"property-type",
|
|
11640
|
+
"property-enum",
|
|
11641
|
+
"self-loop"
|
|
11642
|
+
]);
|
|
11643
|
+
function validateUPGDocument(doc) {
|
|
11644
|
+
const errors = [];
|
|
11645
|
+
const warnings = [];
|
|
11646
|
+
if (!doc || typeof doc !== "object") {
|
|
11647
|
+
return { valid: false, errors: [{ path: "$", message: "Document must be an object" }], warnings };
|
|
11648
|
+
}
|
|
11649
|
+
const d = doc;
|
|
11650
|
+
if (!d.upg_version || typeof d.upg_version !== "string") {
|
|
11651
|
+
errors.push({ path: "$.upg_version", message: "upg_version is required and must be a string" });
|
|
11652
|
+
}
|
|
11653
|
+
if (!d.exported_at || typeof d.exported_at !== "string") {
|
|
11654
|
+
errors.push({ path: "$.exported_at", message: "exported_at is required and must be an ISO 8601 string" });
|
|
11655
|
+
}
|
|
11656
|
+
if (!d.source || typeof d.source !== "object") {
|
|
11657
|
+
errors.push({ path: "$.source", message: "source is required and must be an object" });
|
|
11658
|
+
} else {
|
|
11659
|
+
const source = d.source;
|
|
11660
|
+
if (!source.tool || typeof source.tool !== "string") {
|
|
11661
|
+
errors.push({ path: "$.source.tool", message: "source.tool is required and must be a string" });
|
|
11662
|
+
}
|
|
11663
|
+
}
|
|
11664
|
+
if (!d.product || typeof d.product !== "object") {
|
|
11665
|
+
errors.push({ path: "$.product", message: "product is required and must be an object" });
|
|
11666
|
+
} else {
|
|
11667
|
+
const product = d.product;
|
|
11668
|
+
if (!product.id || typeof product.id !== "string") {
|
|
11669
|
+
errors.push({ path: "$.product.id", message: "product.id is required and must be a string" });
|
|
11670
|
+
}
|
|
11671
|
+
if (!product.title || typeof product.title !== "string") {
|
|
11672
|
+
errors.push({ path: "$.product.title", message: "product.title is required and must be a string" });
|
|
11673
|
+
}
|
|
11674
|
+
}
|
|
11675
|
+
const allKnownTypes = new Set(getTypes());
|
|
11676
|
+
const allKnownEdgeTypes = new Set(Object.keys(UPG_EDGE_CATALOG));
|
|
11677
|
+
if (!Array.isArray(d.nodes)) {
|
|
11678
|
+
errors.push({ path: "$.nodes", message: "nodes is required and must be an array" });
|
|
11679
|
+
} else {
|
|
11680
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
11681
|
+
const nodesById = /* @__PURE__ */ new Map();
|
|
11682
|
+
d.nodes.forEach((node, i) => {
|
|
11683
|
+
const path = `$.nodes[${i}]`;
|
|
11684
|
+
if (!node || typeof node !== "object") {
|
|
11685
|
+
errors.push({ path, message: "Each node must be an object" });
|
|
11686
|
+
return;
|
|
11687
|
+
}
|
|
11688
|
+
const n = node;
|
|
11689
|
+
if (!n.id || typeof n.id !== "string") {
|
|
11690
|
+
errors.push({ path: `${path}.id`, message: "Node id is required and must be a string" });
|
|
11691
|
+
} else {
|
|
11692
|
+
if (nodeIds.has(n.id)) {
|
|
11693
|
+
errors.push({ path: `${path}.id`, message: `Duplicate node id: ${n.id}` });
|
|
11694
|
+
}
|
|
11695
|
+
nodeIds.add(n.id);
|
|
11696
|
+
nodesById.set(n.id, n);
|
|
11697
|
+
}
|
|
11698
|
+
if (!n.type || typeof n.type !== "string") {
|
|
11699
|
+
errors.push({ path: `${path}.type`, message: "Node type is required and must be a string" });
|
|
11700
|
+
} else if (!allKnownTypes.has(n.type)) {
|
|
11701
|
+
warnings.push({ path: `${path}.type`, message: `Unknown UPG type: "${n.type}". Node will be preserved with its original type.` });
|
|
11702
|
+
}
|
|
11703
|
+
if (!n.title || typeof n.title !== "string") {
|
|
11704
|
+
errors.push({ path: `${path}.title`, message: "Node title is required and must be a string" });
|
|
11705
|
+
} else if (n.title.trim().length === 0) {
|
|
11706
|
+
errors.push({ path: `${path}.title`, message: "Node title must not be blank (whitespace-only)" });
|
|
11707
|
+
}
|
|
11708
|
+
if (typeof n.type === "string") {
|
|
11709
|
+
validateNodeProperties(n, `${path}`, warnings);
|
|
11710
|
+
}
|
|
11711
|
+
});
|
|
11712
|
+
if (!Array.isArray(d.edges)) {
|
|
11713
|
+
errors.push({ path: "$.edges", message: "edges is required and must be an array" });
|
|
11714
|
+
} else {
|
|
11715
|
+
d.edges.forEach((edge, i) => {
|
|
11716
|
+
const path = `$.edges[${i}]`;
|
|
11717
|
+
if (!edge || typeof edge !== "object") {
|
|
11718
|
+
errors.push({ path, message: "Each edge must be an object" });
|
|
11719
|
+
return;
|
|
11720
|
+
}
|
|
11721
|
+
const e = edge;
|
|
11722
|
+
if (!e.id || typeof e.id !== "string") {
|
|
11723
|
+
errors.push({ path: `${path}.id`, message: "Edge id is required and must be a string" });
|
|
11724
|
+
}
|
|
11725
|
+
if (typeof e.type === "string" && crossProductEdgeTypeSet.has(e.type)) {
|
|
11726
|
+
errors.push({
|
|
11727
|
+
path: `${path}.type`,
|
|
11728
|
+
message: `Cross-product edge type "${e.type}" must live in portfolio.cross_edges[], not product edges[].`
|
|
11729
|
+
});
|
|
11730
|
+
return;
|
|
11731
|
+
}
|
|
11732
|
+
if (!e.source || typeof e.source !== "string") {
|
|
11733
|
+
errors.push({ path: `${path}.source`, message: "Edge source is required and must be a string" });
|
|
11734
|
+
} else if (!nodeIds.has(e.source)) {
|
|
11735
|
+
errors.push({ path: `${path}.source`, message: `Edge source references unknown node id: ${e.source}` });
|
|
11736
|
+
}
|
|
11737
|
+
if (!e.target || typeof e.target !== "string") {
|
|
11738
|
+
errors.push({ path: `${path}.target`, message: "Edge target is required and must be a string" });
|
|
11739
|
+
} else if (!nodeIds.has(e.target)) {
|
|
11740
|
+
errors.push({ path: `${path}.target`, message: `Edge target references unknown node id: ${e.target}` });
|
|
11741
|
+
}
|
|
11742
|
+
if (typeof e.source === "string" && typeof e.target === "string" && e.source === e.target) {
|
|
11743
|
+
warnings.push({
|
|
11744
|
+
path: `${path}`,
|
|
11745
|
+
message: `Self-loop edge: source and target are the same node ("${e.source}"). A node related to itself is almost always an error.`,
|
|
11746
|
+
rule: "self-loop"
|
|
11747
|
+
});
|
|
11748
|
+
}
|
|
11749
|
+
if (!e.type || typeof e.type !== "string") {
|
|
11750
|
+
errors.push({ path: `${path}.type`, message: "Edge type is required and must be a string" });
|
|
11751
|
+
} else if (!allKnownEdgeTypes.has(e.type)) {
|
|
11752
|
+
warnings.push({
|
|
11753
|
+
path: `${path}.type`,
|
|
11754
|
+
message: `Unknown edge type: "${e.type}". Edge preserved but not in the edge registry.`
|
|
11755
|
+
});
|
|
11756
|
+
}
|
|
11757
|
+
});
|
|
11758
|
+
const valueToAxis = /* @__PURE__ */ new Map();
|
|
11759
|
+
const edgesArr = d.edges;
|
|
11760
|
+
for (const edge of edgesArr) {
|
|
11761
|
+
if (!edge || typeof edge !== "object") continue;
|
|
11762
|
+
const e = edge;
|
|
11763
|
+
if (e.type === "classification_axis_includes_classification_value" && typeof e.source === "string" && typeof e.target === "string") {
|
|
11764
|
+
valueToAxis.set(e.target, e.source);
|
|
11765
|
+
}
|
|
11766
|
+
}
|
|
11767
|
+
for (const [id, n] of nodesById.entries()) {
|
|
11768
|
+
if (n.type !== "classification_value") continue;
|
|
11769
|
+
if (valueToAxis.has(id)) continue;
|
|
11770
|
+
const pid = n.parent_id;
|
|
11771
|
+
if (typeof pid === "string") {
|
|
11772
|
+
const parent = nodesById.get(pid);
|
|
11773
|
+
if (parent && parent.type === "classification_axis") {
|
|
11774
|
+
valueToAxis.set(id, pid);
|
|
11775
|
+
}
|
|
11776
|
+
}
|
|
11777
|
+
}
|
|
11778
|
+
const axesPerAnalysis = /* @__PURE__ */ new Map();
|
|
11779
|
+
for (const edge of edgesArr) {
|
|
11780
|
+
if (!edge || typeof edge !== "object") continue;
|
|
11781
|
+
const e = edge;
|
|
11782
|
+
if (e.type === "competitive_analysis_dimensioned_by_classification_axis" && typeof e.source === "string") {
|
|
11783
|
+
axesPerAnalysis.set(e.source, (axesPerAnalysis.get(e.source) ?? 0) + 1);
|
|
11784
|
+
}
|
|
11785
|
+
}
|
|
11786
|
+
d.nodes.forEach((node, i) => {
|
|
11787
|
+
if (!node || typeof node !== "object") return;
|
|
11788
|
+
const n = node;
|
|
11789
|
+
if (n.type !== "competitive_analysis") return;
|
|
11790
|
+
const props = n.properties;
|
|
11791
|
+
if (!props || typeof props !== "object") return;
|
|
11792
|
+
const empty = props.empty_cells;
|
|
11793
|
+
if (!Array.isArray(empty)) return;
|
|
11794
|
+
const axisCount = axesPerAnalysis.get(n.id ?? "") ?? 0;
|
|
11795
|
+
if (axisCount !== 2) {
|
|
11796
|
+
warnings.push({
|
|
11797
|
+
path: `$.nodes[${i}].properties.empty_cells`,
|
|
11798
|
+
message: `empty_cells is meaningful only on a 2-axis competitive_analysis; this node is dimensioned by ${axisCount} classification_axis ${axisCount === 1 ? "child" : "children"}.`
|
|
11799
|
+
});
|
|
11800
|
+
}
|
|
11801
|
+
empty.forEach((cell, j) => {
|
|
11802
|
+
const cpath = `$.nodes[${i}].properties.empty_cells[${j}]`;
|
|
11803
|
+
if (!cell || typeof cell !== "object") {
|
|
11804
|
+
errors.push({ path: cpath, message: "empty_cells entry must be an object" });
|
|
11805
|
+
return;
|
|
11806
|
+
}
|
|
11807
|
+
const c = cell;
|
|
11808
|
+
const aRef = c.axis_a_value_ref;
|
|
11809
|
+
const bRef = c.axis_b_value_ref;
|
|
11810
|
+
if (typeof aRef !== "string") {
|
|
11811
|
+
errors.push({ path: `${cpath}.axis_a_value_ref`, message: "axis_a_value_ref is required and must be a string" });
|
|
11812
|
+
}
|
|
11813
|
+
if (typeof bRef !== "string") {
|
|
11814
|
+
errors.push({ path: `${cpath}.axis_b_value_ref`, message: "axis_b_value_ref is required and must be a string" });
|
|
11815
|
+
}
|
|
11816
|
+
if (typeof aRef !== "string" || typeof bRef !== "string") return;
|
|
11817
|
+
const aNode = nodesById.get(aRef);
|
|
11818
|
+
const bNode = nodesById.get(bRef);
|
|
11819
|
+
if (!aNode) {
|
|
11820
|
+
errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Unknown node id: ${aRef}` });
|
|
11821
|
+
} else if (aNode.type !== "classification_value") {
|
|
11822
|
+
errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Expected classification_value, found "${String(aNode.type)}" at ${aRef}` });
|
|
11823
|
+
}
|
|
11824
|
+
if (!bNode) {
|
|
11825
|
+
errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Unknown node id: ${bRef}` });
|
|
11826
|
+
} else if (bNode.type !== "classification_value") {
|
|
11827
|
+
errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Expected classification_value, found "${String(bNode.type)}" at ${bRef}` });
|
|
11828
|
+
}
|
|
11829
|
+
if (aNode && bNode && aNode.type === "classification_value" && bNode.type === "classification_value") {
|
|
11830
|
+
const aAxis = valueToAxis.get(aRef);
|
|
11831
|
+
const bAxis = valueToAxis.get(bRef);
|
|
11832
|
+
if (aAxis && bAxis && aAxis === bAxis) {
|
|
11833
|
+
errors.push({
|
|
11834
|
+
path: cpath,
|
|
11835
|
+
message: `empty_cells references two classification_values from the same axis (${aAxis}); axis_a and axis_b must come from distinct classification_axis parents.`
|
|
11836
|
+
});
|
|
11837
|
+
}
|
|
11838
|
+
}
|
|
11839
|
+
const kind = c.rationale_kind;
|
|
11840
|
+
if (kind !== void 0 && kind !== "structural" && kind !== "ideological" && kind !== "opportunity") {
|
|
11841
|
+
errors.push({
|
|
11842
|
+
path: `${cpath}.rationale_kind`,
|
|
11843
|
+
message: `rationale_kind must be one of 'structural' | 'ideological' | 'opportunity'; got "${String(kind)}".`
|
|
11844
|
+
});
|
|
11845
|
+
}
|
|
11846
|
+
});
|
|
11847
|
+
});
|
|
11848
|
+
}
|
|
11849
|
+
}
|
|
11850
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
11851
|
+
}
|
|
11852
|
+
function runtimeKind(val) {
|
|
11853
|
+
if (val === null || val === void 0) return null;
|
|
11854
|
+
if (Array.isArray(val)) return "string[]";
|
|
11855
|
+
switch (typeof val) {
|
|
11856
|
+
case "string":
|
|
11857
|
+
return "string";
|
|
11858
|
+
case "number":
|
|
11859
|
+
return "number";
|
|
11860
|
+
case "boolean":
|
|
11861
|
+
return "boolean";
|
|
11862
|
+
case "object":
|
|
11863
|
+
return "object";
|
|
11864
|
+
default:
|
|
11865
|
+
return null;
|
|
11866
|
+
}
|
|
11867
|
+
}
|
|
11868
|
+
function valueMatchesType(defType, val) {
|
|
11869
|
+
const kind = runtimeKind(val);
|
|
11870
|
+
if (kind === null) return true;
|
|
11871
|
+
switch (defType) {
|
|
11872
|
+
case "string":
|
|
11873
|
+
return kind === "string";
|
|
11874
|
+
case "number":
|
|
11875
|
+
return kind === "number";
|
|
11876
|
+
case "boolean":
|
|
11877
|
+
return kind === "boolean";
|
|
11878
|
+
case "string[]":
|
|
11879
|
+
return kind === "string[]";
|
|
11880
|
+
case "object":
|
|
11881
|
+
case "assessment":
|
|
11882
|
+
return kind === "object";
|
|
11883
|
+
default:
|
|
11884
|
+
return true;
|
|
11885
|
+
}
|
|
11886
|
+
}
|
|
11887
|
+
function describeKind(val) {
|
|
11888
|
+
if (Array.isArray(val)) return "array";
|
|
11889
|
+
return typeof val;
|
|
11890
|
+
}
|
|
11891
|
+
function validateNodeProperties(node, basePath, warnings) {
|
|
11892
|
+
const type = node.type;
|
|
11893
|
+
if (typeof type !== "string") return;
|
|
11894
|
+
const schema = getPropertySchema(type);
|
|
11895
|
+
if (!schema) return;
|
|
11896
|
+
const props = node.properties;
|
|
11897
|
+
if (!props || typeof props !== "object" || Array.isArray(props)) return;
|
|
11898
|
+
for (const [key, value] of Object.entries(props)) {
|
|
11899
|
+
const def = schema[key];
|
|
11900
|
+
if (!def) continue;
|
|
11901
|
+
if (value === null || value === void 0) continue;
|
|
11902
|
+
if (!valueMatchesType(def.type, value)) {
|
|
11903
|
+
warnings.push({
|
|
11904
|
+
path: `${basePath}.properties.${key}`,
|
|
11905
|
+
message: `Property "${key}" on ${type} should be ${def.type}, got ${describeKind(value)}.`,
|
|
11906
|
+
rule: "property-type"
|
|
11907
|
+
});
|
|
11908
|
+
continue;
|
|
11909
|
+
}
|
|
11910
|
+
if (def.enum && typeof value === "string" && !def.enum.includes(value)) {
|
|
11911
|
+
warnings.push({
|
|
11912
|
+
path: `${basePath}.properties.${key}`,
|
|
11913
|
+
message: `Property "${key}" on ${type} has value "${value}" outside its allowed set: ${def.enum.join(", ")}.`,
|
|
11914
|
+
rule: "property-enum"
|
|
11915
|
+
});
|
|
11916
|
+
}
|
|
11917
|
+
}
|
|
11918
|
+
}
|
|
11919
|
+
function isUPGDocument(doc) {
|
|
11920
|
+
return validateUPGDocument(doc).valid;
|
|
11921
|
+
}
|
|
11922
|
+
|
|
11923
|
+
// src/frameworks/canonical.ts
|
|
11924
|
+
var UPG_FRAMEWORKS = [
|
|
11925
|
+
{
|
|
11926
|
+
"id": "opportunity-solution-tree",
|
|
11927
|
+
"approach_ids": [
|
|
11928
|
+
"trace"
|
|
11929
|
+
],
|
|
11930
|
+
"name": "Opportunity Solution Tree",
|
|
11931
|
+
"version": "1.0.0",
|
|
11932
|
+
"description": "Map desired outcomes to opportunities, then branch into solutions and experiments. Ensures every solution traces back to a real user need.",
|
|
11933
|
+
"category": "discovery",
|
|
11934
|
+
"origin": {
|
|
11935
|
+
"type": "practitioner",
|
|
11936
|
+
"attribution": "Teresa Torres",
|
|
11937
|
+
"description": "Introduced in Continuous Discovery Habits. Maps outcomes to opportunities, solutions, and experiments.",
|
|
11938
|
+
"url": "https://www.producttalk.org/opportunity-solution-tree/",
|
|
11939
|
+
"year": 2021,
|
|
11940
|
+
"license": "published_methodology"
|
|
11941
|
+
},
|
|
11942
|
+
"tags": [
|
|
11943
|
+
"discovery",
|
|
11944
|
+
"tree"
|
|
11945
|
+
],
|
|
11946
|
+
"slots": [
|
|
11947
|
+
{
|
|
11948
|
+
"label": "Root Outcome",
|
|
11949
|
+
"entityTypeId": "outcome",
|
|
11950
|
+
"description": "The desired business or user outcome"
|
|
11951
|
+
},
|
|
11952
|
+
{
|
|
11953
|
+
"label": "Opportunities",
|
|
11954
|
+
"entityTypeId": "opportunity",
|
|
11955
|
+
"description": "User needs, pain points, or desires"
|
|
11956
|
+
},
|
|
11957
|
+
{
|
|
11958
|
+
"label": "Solutions",
|
|
11959
|
+
"entityTypeId": "solution",
|
|
11960
|
+
"description": "Ideas to address each opportunity"
|
|
11961
|
+
},
|
|
11962
|
+
{
|
|
11963
|
+
"label": "Experiments",
|
|
11964
|
+
"entityTypeId": "experiment_run",
|
|
11965
|
+
"description": "Tests to validate each solution"
|
|
11966
|
+
}
|
|
11967
|
+
],
|
|
11968
|
+
"data": {
|
|
11969
|
+
"entity_types": [
|
|
11970
|
+
{
|
|
11971
|
+
"type": "outcome",
|
|
11972
|
+
"role": "root"
|
|
11973
|
+
},
|
|
11974
|
+
{
|
|
11975
|
+
"type": "opportunity",
|
|
11892
11976
|
"role": "item"
|
|
11893
11977
|
},
|
|
11894
11978
|
{
|
|
@@ -13072,7 +13156,7 @@ var UPG_FRAMEWORKS = [
|
|
|
13072
13156
|
],
|
|
13073
13157
|
"name": "RICE Scoring",
|
|
13074
13158
|
"version": "1.0.0",
|
|
13075
|
-
"description": "Score features and
|
|
13159
|
+
"description": "Score features, opportunities, and needs by Reach, Impact, Confidence, and Effort to produce a ranked priority list.",
|
|
13076
13160
|
"category": "prioritization",
|
|
13077
13161
|
"origin": {
|
|
13078
13162
|
"type": "practitioner",
|
|
@@ -13090,7 +13174,17 @@ var UPG_FRAMEWORKS = [
|
|
|
13090
13174
|
{
|
|
13091
13175
|
"label": "Items to score",
|
|
13092
13176
|
"entityTypeId": "feature",
|
|
13093
|
-
"description": "Features, opportunities, or
|
|
13177
|
+
"description": "Features, opportunities, or needs being evaluated"
|
|
13178
|
+
},
|
|
13179
|
+
{
|
|
13180
|
+
"label": "Opportunities to score",
|
|
13181
|
+
"entityTypeId": "opportunity",
|
|
13182
|
+
"description": "Opportunities scored on the same RICE Scoring inputs as features."
|
|
13183
|
+
},
|
|
13184
|
+
{
|
|
13185
|
+
"label": "Needs to score",
|
|
13186
|
+
"entityTypeId": "need",
|
|
13187
|
+
"description": "Needs scored on the same RICE Scoring inputs as features."
|
|
13094
13188
|
}
|
|
13095
13189
|
],
|
|
13096
13190
|
"data": {
|
|
@@ -13098,6 +13192,14 @@ var UPG_FRAMEWORKS = [
|
|
|
13098
13192
|
{
|
|
13099
13193
|
"type": "feature",
|
|
13100
13194
|
"role": "scored_item"
|
|
13195
|
+
},
|
|
13196
|
+
{
|
|
13197
|
+
"type": "opportunity",
|
|
13198
|
+
"role": "scored_item"
|
|
13199
|
+
},
|
|
13200
|
+
{
|
|
13201
|
+
"type": "need",
|
|
13202
|
+
"role": "scored_item"
|
|
13101
13203
|
}
|
|
13102
13204
|
],
|
|
13103
13205
|
"required_properties": {
|
|
@@ -13138,6 +13240,82 @@ var UPG_FRAMEWORKS = [
|
|
|
13138
13240
|
"label": "Effort",
|
|
13139
13241
|
"description": "How much work is required to build and ship this, on the effort scale?"
|
|
13140
13242
|
}
|
|
13243
|
+
],
|
|
13244
|
+
"opportunity": [
|
|
13245
|
+
{
|
|
13246
|
+
"property": "reach",
|
|
13247
|
+
"type": "assessment",
|
|
13248
|
+
"scale_id": "reach_5",
|
|
13249
|
+
"required": true,
|
|
13250
|
+
"scope": "framework",
|
|
13251
|
+
"label": "Reach",
|
|
13252
|
+
"description": "How many users will this impact per quarter?"
|
|
13253
|
+
},
|
|
13254
|
+
{
|
|
13255
|
+
"property": "impact",
|
|
13256
|
+
"type": "assessment",
|
|
13257
|
+
"scale_id": "impact_5",
|
|
13258
|
+
"required": true,
|
|
13259
|
+
"scope": "framework",
|
|
13260
|
+
"label": "Impact",
|
|
13261
|
+
"description": "How much will this impact each user, on the impact scale?"
|
|
13262
|
+
},
|
|
13263
|
+
{
|
|
13264
|
+
"property": "confidence",
|
|
13265
|
+
"type": "assessment",
|
|
13266
|
+
"scale_id": "confidence_5",
|
|
13267
|
+
"required": true,
|
|
13268
|
+
"scope": "framework",
|
|
13269
|
+
"label": "Confidence",
|
|
13270
|
+
"description": "How confident are you in the reach, impact, and effort estimates?"
|
|
13271
|
+
},
|
|
13272
|
+
{
|
|
13273
|
+
"property": "effort",
|
|
13274
|
+
"type": "assessment",
|
|
13275
|
+
"scale_id": "effort_5",
|
|
13276
|
+
"required": true,
|
|
13277
|
+
"scope": "framework",
|
|
13278
|
+
"label": "Effort",
|
|
13279
|
+
"description": "How much work is required to build and ship this, on the effort scale?"
|
|
13280
|
+
}
|
|
13281
|
+
],
|
|
13282
|
+
"need": [
|
|
13283
|
+
{
|
|
13284
|
+
"property": "reach",
|
|
13285
|
+
"type": "assessment",
|
|
13286
|
+
"scale_id": "reach_5",
|
|
13287
|
+
"required": true,
|
|
13288
|
+
"scope": "framework",
|
|
13289
|
+
"label": "Reach",
|
|
13290
|
+
"description": "How many users will this impact per quarter?"
|
|
13291
|
+
},
|
|
13292
|
+
{
|
|
13293
|
+
"property": "impact",
|
|
13294
|
+
"type": "assessment",
|
|
13295
|
+
"scale_id": "impact_5",
|
|
13296
|
+
"required": true,
|
|
13297
|
+
"scope": "framework",
|
|
13298
|
+
"label": "Impact",
|
|
13299
|
+
"description": "How much will this impact each user, on the impact scale?"
|
|
13300
|
+
},
|
|
13301
|
+
{
|
|
13302
|
+
"property": "confidence",
|
|
13303
|
+
"type": "assessment",
|
|
13304
|
+
"scale_id": "confidence_5",
|
|
13305
|
+
"required": true,
|
|
13306
|
+
"scope": "framework",
|
|
13307
|
+
"label": "Confidence",
|
|
13308
|
+
"description": "How confident are you in the reach, impact, and effort estimates?"
|
|
13309
|
+
},
|
|
13310
|
+
{
|
|
13311
|
+
"property": "effort",
|
|
13312
|
+
"type": "assessment",
|
|
13313
|
+
"scale_id": "effort_5",
|
|
13314
|
+
"required": true,
|
|
13315
|
+
"scope": "framework",
|
|
13316
|
+
"label": "Effort",
|
|
13317
|
+
"description": "How much work is required to build and ship this, on the effort scale?"
|
|
13318
|
+
}
|
|
13141
13319
|
]
|
|
13142
13320
|
},
|
|
13143
13321
|
"computed_properties": [
|
|
@@ -13147,8 +13325,75 @@ var UPG_FRAMEWORKS = [
|
|
|
13147
13325
|
"entity_type": "feature",
|
|
13148
13326
|
"label": "RICE Score",
|
|
13149
13327
|
"format": "number"
|
|
13328
|
+
},
|
|
13329
|
+
{
|
|
13330
|
+
"property": "rice_score",
|
|
13331
|
+
"expression": "(reach * impact * confidence) / effort",
|
|
13332
|
+
"entity_type": "opportunity",
|
|
13333
|
+
"label": "RICE Score",
|
|
13334
|
+
"format": "number"
|
|
13335
|
+
},
|
|
13336
|
+
{
|
|
13337
|
+
"property": "rice_score",
|
|
13338
|
+
"expression": "(reach * impact * confidence) / effort",
|
|
13339
|
+
"entity_type": "need",
|
|
13340
|
+
"label": "RICE Score",
|
|
13341
|
+
"format": "number"
|
|
13150
13342
|
}
|
|
13151
|
-
]
|
|
13343
|
+
],
|
|
13344
|
+
"scoring_lens": {
|
|
13345
|
+
"applies_to": [
|
|
13346
|
+
"feature",
|
|
13347
|
+
"opportunity",
|
|
13348
|
+
"need"
|
|
13349
|
+
],
|
|
13350
|
+
"inputs": [
|
|
13351
|
+
{
|
|
13352
|
+
"property": "reach",
|
|
13353
|
+
"type": "assessment",
|
|
13354
|
+
"scale_id": "reach_5",
|
|
13355
|
+
"required": true,
|
|
13356
|
+
"scope": "framework",
|
|
13357
|
+
"label": "Reach",
|
|
13358
|
+
"description": "How many users will this impact per quarter?"
|
|
13359
|
+
},
|
|
13360
|
+
{
|
|
13361
|
+
"property": "impact",
|
|
13362
|
+
"type": "assessment",
|
|
13363
|
+
"scale_id": "impact_5",
|
|
13364
|
+
"required": true,
|
|
13365
|
+
"scope": "framework",
|
|
13366
|
+
"label": "Impact",
|
|
13367
|
+
"description": "How much will this impact each user, on the impact scale?"
|
|
13368
|
+
},
|
|
13369
|
+
{
|
|
13370
|
+
"property": "confidence",
|
|
13371
|
+
"type": "assessment",
|
|
13372
|
+
"scale_id": "confidence_5",
|
|
13373
|
+
"required": true,
|
|
13374
|
+
"scope": "framework",
|
|
13375
|
+
"label": "Confidence",
|
|
13376
|
+
"description": "How confident are you in the reach, impact, and effort estimates?"
|
|
13377
|
+
},
|
|
13378
|
+
{
|
|
13379
|
+
"property": "effort",
|
|
13380
|
+
"type": "assessment",
|
|
13381
|
+
"scale_id": "effort_5",
|
|
13382
|
+
"required": true,
|
|
13383
|
+
"scope": "framework",
|
|
13384
|
+
"label": "Effort",
|
|
13385
|
+
"description": "How much work is required to build and ship this, on the effort scale?"
|
|
13386
|
+
}
|
|
13387
|
+
],
|
|
13388
|
+
"computed": [
|
|
13389
|
+
{
|
|
13390
|
+
"property": "rice_score",
|
|
13391
|
+
"expression": "(reach * impact * confidence) / effort",
|
|
13392
|
+
"label": "RICE Score",
|
|
13393
|
+
"format": "number"
|
|
13394
|
+
}
|
|
13395
|
+
]
|
|
13396
|
+
}
|
|
13152
13397
|
},
|
|
13153
13398
|
"structure": {
|
|
13154
13399
|
"pattern": "table"
|
|
@@ -15273,7 +15518,7 @@ var UPG_FRAMEWORKS = [
|
|
|
15273
15518
|
"entity_types": [
|
|
15274
15519
|
{
|
|
15275
15520
|
"type": "team",
|
|
15276
|
-
"role": "
|
|
15521
|
+
"role": "item"
|
|
15277
15522
|
},
|
|
15278
15523
|
{
|
|
15279
15524
|
"type": "retrospective",
|
|
@@ -15530,6 +15775,16 @@ var UPG_FRAMEWORKS = [
|
|
|
15530
15775
|
"label": "Ease",
|
|
15531
15776
|
"entityTypeId": "feature",
|
|
15532
15777
|
"description": "How easy is this to implement?"
|
|
15778
|
+
},
|
|
15779
|
+
{
|
|
15780
|
+
"label": "Opportunities to score",
|
|
15781
|
+
"entityTypeId": "opportunity",
|
|
15782
|
+
"description": "Opportunities scored on the same ICE Scoring inputs as features."
|
|
15783
|
+
},
|
|
15784
|
+
{
|
|
15785
|
+
"label": "Needs to score",
|
|
15786
|
+
"entityTypeId": "need",
|
|
15787
|
+
"description": "Needs scored on the same ICE Scoring inputs as features."
|
|
15533
15788
|
}
|
|
15534
15789
|
],
|
|
15535
15790
|
"data": {
|
|
@@ -15545,6 +15800,14 @@ var UPG_FRAMEWORKS = [
|
|
|
15545
15800
|
{
|
|
15546
15801
|
"type": "assumption",
|
|
15547
15802
|
"role": "item"
|
|
15803
|
+
},
|
|
15804
|
+
{
|
|
15805
|
+
"type": "opportunity",
|
|
15806
|
+
"role": "scored_item"
|
|
15807
|
+
},
|
|
15808
|
+
{
|
|
15809
|
+
"type": "need",
|
|
15810
|
+
"role": "scored_item"
|
|
15548
15811
|
}
|
|
15549
15812
|
],
|
|
15550
15813
|
"required_properties": {
|
|
@@ -15573,6 +15836,58 @@ var UPG_FRAMEWORKS = [
|
|
|
15573
15836
|
"label": "Ease",
|
|
15574
15837
|
"description": "Ease of implementation (1-10)"
|
|
15575
15838
|
}
|
|
15839
|
+
],
|
|
15840
|
+
"opportunity": [
|
|
15841
|
+
{
|
|
15842
|
+
"property": "impact",
|
|
15843
|
+
"type": "number",
|
|
15844
|
+
"required": true,
|
|
15845
|
+
"scope": "framework",
|
|
15846
|
+
"label": "Impact",
|
|
15847
|
+
"description": "Expected impact on the target metric (1-10)"
|
|
15848
|
+
},
|
|
15849
|
+
{
|
|
15850
|
+
"property": "confidence",
|
|
15851
|
+
"type": "number",
|
|
15852
|
+
"required": true,
|
|
15853
|
+
"scope": "framework",
|
|
15854
|
+
"label": "Confidence",
|
|
15855
|
+
"description": "Confidence in the impact estimate (1-10)"
|
|
15856
|
+
},
|
|
15857
|
+
{
|
|
15858
|
+
"property": "ease",
|
|
15859
|
+
"type": "number",
|
|
15860
|
+
"required": true,
|
|
15861
|
+
"scope": "framework",
|
|
15862
|
+
"label": "Ease",
|
|
15863
|
+
"description": "Ease of implementation (1-10)"
|
|
15864
|
+
}
|
|
15865
|
+
],
|
|
15866
|
+
"need": [
|
|
15867
|
+
{
|
|
15868
|
+
"property": "impact",
|
|
15869
|
+
"type": "number",
|
|
15870
|
+
"required": true,
|
|
15871
|
+
"scope": "framework",
|
|
15872
|
+
"label": "Impact",
|
|
15873
|
+
"description": "Expected impact on the target metric (1-10)"
|
|
15874
|
+
},
|
|
15875
|
+
{
|
|
15876
|
+
"property": "confidence",
|
|
15877
|
+
"type": "number",
|
|
15878
|
+
"required": true,
|
|
15879
|
+
"scope": "framework",
|
|
15880
|
+
"label": "Confidence",
|
|
15881
|
+
"description": "Confidence in the impact estimate (1-10)"
|
|
15882
|
+
},
|
|
15883
|
+
{
|
|
15884
|
+
"property": "ease",
|
|
15885
|
+
"type": "number",
|
|
15886
|
+
"required": true,
|
|
15887
|
+
"scope": "framework",
|
|
15888
|
+
"label": "Ease",
|
|
15889
|
+
"description": "Ease of implementation (1-10)"
|
|
15890
|
+
}
|
|
15576
15891
|
]
|
|
15577
15892
|
},
|
|
15578
15893
|
"computed_properties": [
|
|
@@ -15582,8 +15897,63 @@ var UPG_FRAMEWORKS = [
|
|
|
15582
15897
|
"entity_type": "feature",
|
|
15583
15898
|
"label": "ICE Score",
|
|
15584
15899
|
"format": "number"
|
|
15900
|
+
},
|
|
15901
|
+
{
|
|
15902
|
+
"property": "ice_score",
|
|
15903
|
+
"expression": "impact * confidence * ease",
|
|
15904
|
+
"entity_type": "opportunity",
|
|
15905
|
+
"label": "ICE Score",
|
|
15906
|
+
"format": "number"
|
|
15907
|
+
},
|
|
15908
|
+
{
|
|
15909
|
+
"property": "ice_score",
|
|
15910
|
+
"expression": "impact * confidence * ease",
|
|
15911
|
+
"entity_type": "need",
|
|
15912
|
+
"label": "ICE Score",
|
|
15913
|
+
"format": "number"
|
|
15585
15914
|
}
|
|
15586
|
-
]
|
|
15915
|
+
],
|
|
15916
|
+
"scoring_lens": {
|
|
15917
|
+
"applies_to": [
|
|
15918
|
+
"feature",
|
|
15919
|
+
"opportunity",
|
|
15920
|
+
"need"
|
|
15921
|
+
],
|
|
15922
|
+
"inputs": [
|
|
15923
|
+
{
|
|
15924
|
+
"property": "impact",
|
|
15925
|
+
"type": "number",
|
|
15926
|
+
"required": true,
|
|
15927
|
+
"scope": "framework",
|
|
15928
|
+
"label": "Impact",
|
|
15929
|
+
"description": "Expected impact on the target metric (1-10)"
|
|
15930
|
+
},
|
|
15931
|
+
{
|
|
15932
|
+
"property": "confidence",
|
|
15933
|
+
"type": "number",
|
|
15934
|
+
"required": true,
|
|
15935
|
+
"scope": "framework",
|
|
15936
|
+
"label": "Confidence",
|
|
15937
|
+
"description": "Confidence in the impact estimate (1-10)"
|
|
15938
|
+
},
|
|
15939
|
+
{
|
|
15940
|
+
"property": "ease",
|
|
15941
|
+
"type": "number",
|
|
15942
|
+
"required": true,
|
|
15943
|
+
"scope": "framework",
|
|
15944
|
+
"label": "Ease",
|
|
15945
|
+
"description": "Ease of implementation (1-10)"
|
|
15946
|
+
}
|
|
15947
|
+
],
|
|
15948
|
+
"computed": [
|
|
15949
|
+
{
|
|
15950
|
+
"property": "ice_score",
|
|
15951
|
+
"expression": "impact * confidence * ease",
|
|
15952
|
+
"label": "ICE Score",
|
|
15953
|
+
"format": "number"
|
|
15954
|
+
}
|
|
15955
|
+
]
|
|
15956
|
+
}
|
|
15587
15957
|
},
|
|
15588
15958
|
"structure": {
|
|
15589
15959
|
"pattern": "table"
|
|
@@ -15685,6 +16055,11 @@ var UPG_FRAMEWORKS = [
|
|
|
15685
16055
|
"label": "Job Size",
|
|
15686
16056
|
"entityTypeId": "metric",
|
|
15687
16057
|
"description": "Estimated effort (story points, t-shirt size, or person-weeks)"
|
|
16058
|
+
},
|
|
16059
|
+
{
|
|
16060
|
+
"label": "Opportunities to score",
|
|
16061
|
+
"entityTypeId": "opportunity",
|
|
16062
|
+
"description": "Opportunities scored on the same WSJF (Weighted Shortest Job First) inputs as features."
|
|
15688
16063
|
}
|
|
15689
16064
|
],
|
|
15690
16065
|
"data": {
|
|
@@ -15696,6 +16071,10 @@ var UPG_FRAMEWORKS = [
|
|
|
15696
16071
|
{
|
|
15697
16072
|
"type": "metric",
|
|
15698
16073
|
"role": "item"
|
|
16074
|
+
},
|
|
16075
|
+
{
|
|
16076
|
+
"type": "opportunity",
|
|
16077
|
+
"role": "scored_item"
|
|
15699
16078
|
}
|
|
15700
16079
|
],
|
|
15701
16080
|
"required_properties": {
|
|
@@ -15732,6 +16111,40 @@ var UPG_FRAMEWORKS = [
|
|
|
15732
16111
|
"label": "Job Size",
|
|
15733
16112
|
"description": "Estimated effort (story points, t-shirt size, or person-weeks)"
|
|
15734
16113
|
}
|
|
16114
|
+
],
|
|
16115
|
+
"opportunity": [
|
|
16116
|
+
{
|
|
16117
|
+
"property": "user_value",
|
|
16118
|
+
"type": "number",
|
|
16119
|
+
"required": true,
|
|
16120
|
+
"scope": "framework",
|
|
16121
|
+
"label": "User/Business Value",
|
|
16122
|
+
"description": "Relative value to users and the business if delivered"
|
|
16123
|
+
},
|
|
16124
|
+
{
|
|
16125
|
+
"property": "time_criticality",
|
|
16126
|
+
"type": "number",
|
|
16127
|
+
"required": true,
|
|
16128
|
+
"scope": "framework",
|
|
16129
|
+
"label": "Time Criticality",
|
|
16130
|
+
"description": "How much value decays if delivery is delayed (deadlines, competition, seasonal windows)"
|
|
16131
|
+
},
|
|
16132
|
+
{
|
|
16133
|
+
"property": "risk_reduction",
|
|
16134
|
+
"type": "number",
|
|
16135
|
+
"required": true,
|
|
16136
|
+
"scope": "framework",
|
|
16137
|
+
"label": "Risk Reduction / Opportunity Enablement",
|
|
16138
|
+
"description": "Value from reducing risk or enabling future opportunities"
|
|
16139
|
+
},
|
|
16140
|
+
{
|
|
16141
|
+
"property": "job_size",
|
|
16142
|
+
"type": "number",
|
|
16143
|
+
"required": true,
|
|
16144
|
+
"scope": "framework",
|
|
16145
|
+
"label": "Job Size",
|
|
16146
|
+
"description": "Estimated effort (story points, t-shirt size, or person-weeks)"
|
|
16147
|
+
}
|
|
15735
16148
|
]
|
|
15736
16149
|
},
|
|
15737
16150
|
"computed_properties": [
|
|
@@ -15741,8 +16154,63 @@ var UPG_FRAMEWORKS = [
|
|
|
15741
16154
|
"entity_type": "feature",
|
|
15742
16155
|
"label": "WSJF Score",
|
|
15743
16156
|
"format": "number"
|
|
16157
|
+
},
|
|
16158
|
+
{
|
|
16159
|
+
"property": "wsjf_score",
|
|
16160
|
+
"expression": "(user_value + time_criticality + risk_reduction) / job_size",
|
|
16161
|
+
"entity_type": "opportunity",
|
|
16162
|
+
"label": "WSJF Score",
|
|
16163
|
+
"format": "number"
|
|
15744
16164
|
}
|
|
15745
|
-
]
|
|
16165
|
+
],
|
|
16166
|
+
"scoring_lens": {
|
|
16167
|
+
"applies_to": [
|
|
16168
|
+
"feature",
|
|
16169
|
+
"opportunity"
|
|
16170
|
+
],
|
|
16171
|
+
"inputs": [
|
|
16172
|
+
{
|
|
16173
|
+
"property": "user_value",
|
|
16174
|
+
"type": "number",
|
|
16175
|
+
"required": true,
|
|
16176
|
+
"scope": "framework",
|
|
16177
|
+
"label": "User/Business Value",
|
|
16178
|
+
"description": "Relative value to users and the business if delivered"
|
|
16179
|
+
},
|
|
16180
|
+
{
|
|
16181
|
+
"property": "time_criticality",
|
|
16182
|
+
"type": "number",
|
|
16183
|
+
"required": true,
|
|
16184
|
+
"scope": "framework",
|
|
16185
|
+
"label": "Time Criticality",
|
|
16186
|
+
"description": "How much value decays if delivery is delayed (deadlines, competition, seasonal windows)"
|
|
16187
|
+
},
|
|
16188
|
+
{
|
|
16189
|
+
"property": "risk_reduction",
|
|
16190
|
+
"type": "number",
|
|
16191
|
+
"required": true,
|
|
16192
|
+
"scope": "framework",
|
|
16193
|
+
"label": "Risk Reduction / Opportunity Enablement",
|
|
16194
|
+
"description": "Value from reducing risk or enabling future opportunities"
|
|
16195
|
+
},
|
|
16196
|
+
{
|
|
16197
|
+
"property": "job_size",
|
|
16198
|
+
"type": "number",
|
|
16199
|
+
"required": true,
|
|
16200
|
+
"scope": "framework",
|
|
16201
|
+
"label": "Job Size",
|
|
16202
|
+
"description": "Estimated effort (story points, t-shirt size, or person-weeks)"
|
|
16203
|
+
}
|
|
16204
|
+
],
|
|
16205
|
+
"computed": [
|
|
16206
|
+
{
|
|
16207
|
+
"property": "wsjf_score",
|
|
16208
|
+
"expression": "(user_value + time_criticality + risk_reduction) / job_size",
|
|
16209
|
+
"label": "WSJF Score",
|
|
16210
|
+
"format": "number"
|
|
16211
|
+
}
|
|
16212
|
+
]
|
|
16213
|
+
}
|
|
15746
16214
|
},
|
|
15747
16215
|
"structure": {
|
|
15748
16216
|
"pattern": "table"
|
|
@@ -15815,7 +16283,7 @@ var UPG_FRAMEWORKS = [
|
|
|
15815
16283
|
],
|
|
15816
16284
|
"name": "Cost of Delay",
|
|
15817
16285
|
"version": "1.0.0",
|
|
15818
|
-
"description": "Quantify the economic cost of not shipping a feature to drive priority decisions. Combines urgency with value.",
|
|
16286
|
+
"description": "Quantify the economic cost of not shipping a feature or opportunity to drive priority decisions. Combines urgency with value.",
|
|
15819
16287
|
"category": "prioritization",
|
|
15820
16288
|
"origin": {
|
|
15821
16289
|
"type": "practitioner",
|
|
@@ -15848,6 +16316,11 @@ var UPG_FRAMEWORKS = [
|
|
|
15848
16316
|
"label": "Risk Reduction",
|
|
15849
16317
|
"entityTypeId": "risk",
|
|
15850
16318
|
"description": "What risk does this mitigate?"
|
|
16319
|
+
},
|
|
16320
|
+
{
|
|
16321
|
+
"label": "Opportunities to score",
|
|
16322
|
+
"entityTypeId": "opportunity",
|
|
16323
|
+
"description": "Opportunities scored on the same Cost of Delay inputs as features."
|
|
15851
16324
|
}
|
|
15852
16325
|
],
|
|
15853
16326
|
"data": {
|
|
@@ -15867,6 +16340,10 @@ var UPG_FRAMEWORKS = [
|
|
|
15867
16340
|
{
|
|
15868
16341
|
"type": "risk",
|
|
15869
16342
|
"role": "item"
|
|
16343
|
+
},
|
|
16344
|
+
{
|
|
16345
|
+
"type": "opportunity",
|
|
16346
|
+
"role": "scored_item"
|
|
15870
16347
|
}
|
|
15871
16348
|
],
|
|
15872
16349
|
"required_properties": {
|
|
@@ -15887,6 +16364,24 @@ var UPG_FRAMEWORKS = [
|
|
|
15887
16364
|
"label": "Job Size",
|
|
15888
16365
|
"description": "Weeks of development effort"
|
|
15889
16366
|
}
|
|
16367
|
+
],
|
|
16368
|
+
"opportunity": [
|
|
16369
|
+
{
|
|
16370
|
+
"property": "cost_of_delay",
|
|
16371
|
+
"type": "number",
|
|
16372
|
+
"required": true,
|
|
16373
|
+
"scope": "framework",
|
|
16374
|
+
"label": "Cost of Delay",
|
|
16375
|
+
"description": "Weekly revenue impact of not shipping"
|
|
16376
|
+
},
|
|
16377
|
+
{
|
|
16378
|
+
"property": "job_size",
|
|
16379
|
+
"type": "number",
|
|
16380
|
+
"required": true,
|
|
16381
|
+
"scope": "framework",
|
|
16382
|
+
"label": "Job Size",
|
|
16383
|
+
"description": "Weeks of development effort"
|
|
16384
|
+
}
|
|
15890
16385
|
]
|
|
15891
16386
|
},
|
|
15892
16387
|
"computed_properties": [
|
|
@@ -15896,8 +16391,47 @@ var UPG_FRAMEWORKS = [
|
|
|
15896
16391
|
"entity_type": "feature",
|
|
15897
16392
|
"label": "WSJF Score",
|
|
15898
16393
|
"format": "number"
|
|
16394
|
+
},
|
|
16395
|
+
{
|
|
16396
|
+
"property": "wsjf_score",
|
|
16397
|
+
"expression": "cost_of_delay / job_size",
|
|
16398
|
+
"entity_type": "opportunity",
|
|
16399
|
+
"label": "WSJF Score",
|
|
16400
|
+
"format": "number"
|
|
15899
16401
|
}
|
|
15900
|
-
]
|
|
16402
|
+
],
|
|
16403
|
+
"scoring_lens": {
|
|
16404
|
+
"applies_to": [
|
|
16405
|
+
"feature",
|
|
16406
|
+
"opportunity"
|
|
16407
|
+
],
|
|
16408
|
+
"inputs": [
|
|
16409
|
+
{
|
|
16410
|
+
"property": "cost_of_delay",
|
|
16411
|
+
"type": "number",
|
|
16412
|
+
"required": true,
|
|
16413
|
+
"scope": "framework",
|
|
16414
|
+
"label": "Cost of Delay",
|
|
16415
|
+
"description": "Weekly revenue impact of not shipping"
|
|
16416
|
+
},
|
|
16417
|
+
{
|
|
16418
|
+
"property": "job_size",
|
|
16419
|
+
"type": "number",
|
|
16420
|
+
"required": true,
|
|
16421
|
+
"scope": "framework",
|
|
16422
|
+
"label": "Job Size",
|
|
16423
|
+
"description": "Weeks of development effort"
|
|
16424
|
+
}
|
|
16425
|
+
],
|
|
16426
|
+
"computed": [
|
|
16427
|
+
{
|
|
16428
|
+
"property": "wsjf_score",
|
|
16429
|
+
"expression": "cost_of_delay / job_size",
|
|
16430
|
+
"label": "WSJF Score",
|
|
16431
|
+
"format": "number"
|
|
16432
|
+
}
|
|
16433
|
+
]
|
|
16434
|
+
}
|
|
15901
16435
|
},
|
|
15902
16436
|
"structure": {
|
|
15903
16437
|
"pattern": "table"
|
|
@@ -24798,7 +25332,7 @@ function isCanonical(text) {
|
|
|
24798
25332
|
}
|
|
24799
25333
|
|
|
24800
25334
|
// src/index.ts
|
|
24801
|
-
var UPG_VERSION = "0.8.
|
|
25335
|
+
var UPG_VERSION = "0.8.7";
|
|
24802
25336
|
var UPG_FORMAT_VERSION = "0.4.0";
|
|
24803
25337
|
var MARKDOWN_FORMAT_VERSION = "0.1";
|
|
24804
25338
|
var UPG_TYPES = getTypes();
|
|
@@ -24866,6 +25400,7 @@ export {
|
|
|
24866
25400
|
BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK,
|
|
24867
25401
|
BUSINESS_GTM_GROWTH_PLAYBOOK,
|
|
24868
25402
|
BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK,
|
|
25403
|
+
CONTENT_DEPTH_WARNING_RULES,
|
|
24869
25404
|
DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK,
|
|
24870
25405
|
ENGINEERING_PLATFORM_PLAYBOOK,
|
|
24871
25406
|
EXPERIENCE_DESIGN_BRAND_PLAYBOOK,
|