@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/dist/index.cjs CHANGED
@@ -24,6 +24,7 @@ __export(index_exports, {
24
24
  BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK: () => BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK,
25
25
  BUSINESS_GTM_GROWTH_PLAYBOOK: () => BUSINESS_GTM_GROWTH_PLAYBOOK,
26
26
  BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK: () => BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK,
27
+ CONTENT_DEPTH_WARNING_RULES: () => CONTENT_DEPTH_WARNING_RULES,
27
28
  DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK: () => DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK,
28
29
  ENGINEERING_PLATFORM_PLAYBOOK: () => ENGINEERING_PLATFORM_PLAYBOOK,
29
30
  EXPERIENCE_DESIGN_BRAND_PLAYBOOK: () => EXPERIENCE_DESIGN_BRAND_PLAYBOOK,
@@ -8666,209 +8667,6 @@ function rotateSlug(node, next) {
8666
8667
  return node;
8667
8668
  }
8668
8669
 
8669
- // src/grammar/validate.ts
8670
- var crossProductEdgeTypeSet = new Set(UPG_CROSS_EDGE_TYPES);
8671
- function validateUPGDocument(doc) {
8672
- const errors = [];
8673
- const warnings = [];
8674
- if (!doc || typeof doc !== "object") {
8675
- return { valid: false, errors: [{ path: "$", message: "Document must be an object" }], warnings };
8676
- }
8677
- const d = doc;
8678
- if (!d.upg_version || typeof d.upg_version !== "string") {
8679
- errors.push({ path: "$.upg_version", message: "upg_version is required and must be a string" });
8680
- }
8681
- if (!d.exported_at || typeof d.exported_at !== "string") {
8682
- errors.push({ path: "$.exported_at", message: "exported_at is required and must be an ISO 8601 string" });
8683
- }
8684
- if (!d.source || typeof d.source !== "object") {
8685
- errors.push({ path: "$.source", message: "source is required and must be an object" });
8686
- } else {
8687
- const source = d.source;
8688
- if (!source.tool || typeof source.tool !== "string") {
8689
- errors.push({ path: "$.source.tool", message: "source.tool is required and must be a string" });
8690
- }
8691
- }
8692
- if (!d.product || typeof d.product !== "object") {
8693
- errors.push({ path: "$.product", message: "product is required and must be an object" });
8694
- } else {
8695
- const product = d.product;
8696
- if (!product.id || typeof product.id !== "string") {
8697
- errors.push({ path: "$.product.id", message: "product.id is required and must be a string" });
8698
- }
8699
- if (!product.title || typeof product.title !== "string") {
8700
- errors.push({ path: "$.product.title", message: "product.title is required and must be a string" });
8701
- }
8702
- }
8703
- const allKnownTypes = new Set(getTypes());
8704
- const allKnownEdgeTypes = new Set(Object.keys(UPG_EDGE_CATALOG));
8705
- if (!Array.isArray(d.nodes)) {
8706
- errors.push({ path: "$.nodes", message: "nodes is required and must be an array" });
8707
- } else {
8708
- const nodeIds = /* @__PURE__ */ new Set();
8709
- const nodesById = /* @__PURE__ */ new Map();
8710
- d.nodes.forEach((node, i) => {
8711
- const path = `$.nodes[${i}]`;
8712
- if (!node || typeof node !== "object") {
8713
- errors.push({ path, message: "Each node must be an object" });
8714
- return;
8715
- }
8716
- const n = node;
8717
- if (!n.id || typeof n.id !== "string") {
8718
- errors.push({ path: `${path}.id`, message: "Node id is required and must be a string" });
8719
- } else {
8720
- if (nodeIds.has(n.id)) {
8721
- errors.push({ path: `${path}.id`, message: `Duplicate node id: ${n.id}` });
8722
- }
8723
- nodeIds.add(n.id);
8724
- nodesById.set(n.id, n);
8725
- }
8726
- if (!n.type || typeof n.type !== "string") {
8727
- errors.push({ path: `${path}.type`, message: "Node type is required and must be a string" });
8728
- } else if (!allKnownTypes.has(n.type)) {
8729
- warnings.push({ path: `${path}.type`, message: `Unknown UPG type: "${n.type}". Node will be preserved with its original type.` });
8730
- }
8731
- if (!n.title || typeof n.title !== "string") {
8732
- errors.push({ path: `${path}.title`, message: "Node title is required and must be a string" });
8733
- }
8734
- });
8735
- if (!Array.isArray(d.edges)) {
8736
- errors.push({ path: "$.edges", message: "edges is required and must be an array" });
8737
- } else {
8738
- d.edges.forEach((edge, i) => {
8739
- const path = `$.edges[${i}]`;
8740
- if (!edge || typeof edge !== "object") {
8741
- errors.push({ path, message: "Each edge must be an object" });
8742
- return;
8743
- }
8744
- const e = edge;
8745
- if (!e.id || typeof e.id !== "string") {
8746
- errors.push({ path: `${path}.id`, message: "Edge id is required and must be a string" });
8747
- }
8748
- if (typeof e.type === "string" && crossProductEdgeTypeSet.has(e.type)) {
8749
- errors.push({
8750
- path: `${path}.type`,
8751
- message: `Cross-product edge type "${e.type}" must live in portfolio.cross_edges[], not product edges[].`
8752
- });
8753
- return;
8754
- }
8755
- if (!e.source || typeof e.source !== "string") {
8756
- errors.push({ path: `${path}.source`, message: "Edge source is required and must be a string" });
8757
- } else if (!nodeIds.has(e.source)) {
8758
- errors.push({ path: `${path}.source`, message: `Edge source references unknown node id: ${e.source}` });
8759
- }
8760
- if (!e.target || typeof e.target !== "string") {
8761
- errors.push({ path: `${path}.target`, message: "Edge target is required and must be a string" });
8762
- } else if (!nodeIds.has(e.target)) {
8763
- errors.push({ path: `${path}.target`, message: `Edge target references unknown node id: ${e.target}` });
8764
- }
8765
- if (!e.type || typeof e.type !== "string") {
8766
- errors.push({ path: `${path}.type`, message: "Edge type is required and must be a string" });
8767
- } else if (!allKnownEdgeTypes.has(e.type)) {
8768
- warnings.push({
8769
- path: `${path}.type`,
8770
- message: `Unknown edge type: "${e.type}". Edge preserved but not in the edge registry.`
8771
- });
8772
- }
8773
- });
8774
- const valueToAxis = /* @__PURE__ */ new Map();
8775
- const edgesArr = d.edges;
8776
- for (const edge of edgesArr) {
8777
- if (!edge || typeof edge !== "object") continue;
8778
- const e = edge;
8779
- if (e.type === "classification_axis_includes_classification_value" && typeof e.source === "string" && typeof e.target === "string") {
8780
- valueToAxis.set(e.target, e.source);
8781
- }
8782
- }
8783
- for (const [id, n] of nodesById.entries()) {
8784
- if (n.type !== "classification_value") continue;
8785
- if (valueToAxis.has(id)) continue;
8786
- const pid = n.parent_id;
8787
- if (typeof pid === "string") {
8788
- const parent = nodesById.get(pid);
8789
- if (parent && parent.type === "classification_axis") {
8790
- valueToAxis.set(id, pid);
8791
- }
8792
- }
8793
- }
8794
- const axesPerAnalysis = /* @__PURE__ */ new Map();
8795
- for (const edge of edgesArr) {
8796
- if (!edge || typeof edge !== "object") continue;
8797
- const e = edge;
8798
- if (e.type === "competitive_analysis_dimensioned_by_classification_axis" && typeof e.source === "string") {
8799
- axesPerAnalysis.set(e.source, (axesPerAnalysis.get(e.source) ?? 0) + 1);
8800
- }
8801
- }
8802
- d.nodes.forEach((node, i) => {
8803
- if (!node || typeof node !== "object") return;
8804
- const n = node;
8805
- if (n.type !== "competitive_analysis") return;
8806
- const props = n.properties;
8807
- if (!props || typeof props !== "object") return;
8808
- const empty = props.empty_cells;
8809
- if (!Array.isArray(empty)) return;
8810
- const axisCount = axesPerAnalysis.get(n.id ?? "") ?? 0;
8811
- if (axisCount !== 2) {
8812
- warnings.push({
8813
- path: `$.nodes[${i}].properties.empty_cells`,
8814
- message: `empty_cells is meaningful only on a 2-axis competitive_analysis; this node is dimensioned by ${axisCount} classification_axis ${axisCount === 1 ? "child" : "children"}.`
8815
- });
8816
- }
8817
- empty.forEach((cell, j) => {
8818
- const cpath = `$.nodes[${i}].properties.empty_cells[${j}]`;
8819
- if (!cell || typeof cell !== "object") {
8820
- errors.push({ path: cpath, message: "empty_cells entry must be an object" });
8821
- return;
8822
- }
8823
- const c = cell;
8824
- const aRef = c.axis_a_value_ref;
8825
- const bRef = c.axis_b_value_ref;
8826
- if (typeof aRef !== "string") {
8827
- errors.push({ path: `${cpath}.axis_a_value_ref`, message: "axis_a_value_ref is required and must be a string" });
8828
- }
8829
- if (typeof bRef !== "string") {
8830
- errors.push({ path: `${cpath}.axis_b_value_ref`, message: "axis_b_value_ref is required and must be a string" });
8831
- }
8832
- if (typeof aRef !== "string" || typeof bRef !== "string") return;
8833
- const aNode = nodesById.get(aRef);
8834
- const bNode = nodesById.get(bRef);
8835
- if (!aNode) {
8836
- errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Unknown node id: ${aRef}` });
8837
- } else if (aNode.type !== "classification_value") {
8838
- errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Expected classification_value, found "${String(aNode.type)}" at ${aRef}` });
8839
- }
8840
- if (!bNode) {
8841
- errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Unknown node id: ${bRef}` });
8842
- } else if (bNode.type !== "classification_value") {
8843
- errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Expected classification_value, found "${String(bNode.type)}" at ${bRef}` });
8844
- }
8845
- if (aNode && bNode && aNode.type === "classification_value" && bNode.type === "classification_value") {
8846
- const aAxis = valueToAxis.get(aRef);
8847
- const bAxis = valueToAxis.get(bRef);
8848
- if (aAxis && bAxis && aAxis === bAxis) {
8849
- errors.push({
8850
- path: cpath,
8851
- message: `empty_cells references two classification_values from the same axis (${aAxis}); axis_a and axis_b must come from distinct classification_axis parents.`
8852
- });
8853
- }
8854
- }
8855
- const kind = c.rationale_kind;
8856
- if (kind !== void 0 && kind !== "structural" && kind !== "ideological" && kind !== "opportunity") {
8857
- errors.push({
8858
- path: `${cpath}.rationale_kind`,
8859
- message: `rationale_kind must be one of 'structural' | 'ideological' | 'opportunity'; got "${String(kind)}".`
8860
- });
8861
- }
8862
- });
8863
- });
8864
- }
8865
- }
8866
- return { valid: errors.length === 0, errors, warnings };
8867
- }
8868
- function isUPGDocument(doc) {
8869
- return validateUPGDocument(doc).valid;
8870
- }
8871
-
8872
8670
  // src/properties/property-schema.ts
8873
8671
  var UPG_PROPERTY_SCHEMA = {
8874
8672
  // A11yAnnotationProperties: Accessibility annotation.
@@ -12027,59 +11825,346 @@ function getPropertySchema(entityType) {
12027
11825
  return UPG_PROPERTY_SCHEMA[entityType];
12028
11826
  }
12029
11827
 
12030
- // src/frameworks/canonical.ts
12031
- var UPG_FRAMEWORKS = [
12032
- {
12033
- "id": "opportunity-solution-tree",
12034
- "approach_ids": [
12035
- "trace"
12036
- ],
12037
- "name": "Opportunity Solution Tree",
12038
- "version": "1.0.0",
12039
- "description": "Map desired outcomes to opportunities, then branch into solutions and experiments. Ensures every solution traces back to a real user need.",
12040
- "category": "discovery",
12041
- "origin": {
12042
- "type": "practitioner",
12043
- "attribution": "Teresa Torres",
12044
- "description": "Introduced in Continuous Discovery Habits. Maps outcomes to opportunities, solutions, and experiments.",
12045
- "url": "https://www.producttalk.org/opportunity-solution-tree/",
12046
- "year": 2021,
12047
- "license": "published_methodology"
12048
- },
12049
- "tags": [
12050
- "discovery",
12051
- "tree"
12052
- ],
12053
- "slots": [
12054
- {
12055
- "label": "Root Outcome",
12056
- "entityTypeId": "outcome",
12057
- "description": "The desired business or user outcome"
12058
- },
12059
- {
12060
- "label": "Opportunities",
12061
- "entityTypeId": "opportunity",
12062
- "description": "User needs, pain points, or desires"
12063
- },
12064
- {
12065
- "label": "Solutions",
12066
- "entityTypeId": "solution",
12067
- "description": "Ideas to address each opportunity"
12068
- },
12069
- {
12070
- "label": "Experiments",
12071
- "entityTypeId": "experiment_run",
12072
- "description": "Tests to validate each solution"
12073
- }
12074
- ],
12075
- "data": {
12076
- "entity_types": [
12077
- {
12078
- "type": "outcome",
12079
- "role": "root"
12080
- },
12081
- {
12082
- "type": "opportunity",
11828
+ // src/grammar/validate.ts
11829
+ var crossProductEdgeTypeSet = new Set(UPG_CROSS_EDGE_TYPES);
11830
+ var CONTENT_DEPTH_WARNING_RULES = /* @__PURE__ */ new Set([
11831
+ "property-type",
11832
+ "property-enum",
11833
+ "self-loop"
11834
+ ]);
11835
+ function validateUPGDocument(doc) {
11836
+ const errors = [];
11837
+ const warnings = [];
11838
+ if (!doc || typeof doc !== "object") {
11839
+ return { valid: false, errors: [{ path: "$", message: "Document must be an object" }], warnings };
11840
+ }
11841
+ const d = doc;
11842
+ if (!d.upg_version || typeof d.upg_version !== "string") {
11843
+ errors.push({ path: "$.upg_version", message: "upg_version is required and must be a string" });
11844
+ }
11845
+ if (!d.exported_at || typeof d.exported_at !== "string") {
11846
+ errors.push({ path: "$.exported_at", message: "exported_at is required and must be an ISO 8601 string" });
11847
+ }
11848
+ if (!d.source || typeof d.source !== "object") {
11849
+ errors.push({ path: "$.source", message: "source is required and must be an object" });
11850
+ } else {
11851
+ const source = d.source;
11852
+ if (!source.tool || typeof source.tool !== "string") {
11853
+ errors.push({ path: "$.source.tool", message: "source.tool is required and must be a string" });
11854
+ }
11855
+ }
11856
+ if (!d.product || typeof d.product !== "object") {
11857
+ errors.push({ path: "$.product", message: "product is required and must be an object" });
11858
+ } else {
11859
+ const product = d.product;
11860
+ if (!product.id || typeof product.id !== "string") {
11861
+ errors.push({ path: "$.product.id", message: "product.id is required and must be a string" });
11862
+ }
11863
+ if (!product.title || typeof product.title !== "string") {
11864
+ errors.push({ path: "$.product.title", message: "product.title is required and must be a string" });
11865
+ }
11866
+ }
11867
+ const allKnownTypes = new Set(getTypes());
11868
+ const allKnownEdgeTypes = new Set(Object.keys(UPG_EDGE_CATALOG));
11869
+ if (!Array.isArray(d.nodes)) {
11870
+ errors.push({ path: "$.nodes", message: "nodes is required and must be an array" });
11871
+ } else {
11872
+ const nodeIds = /* @__PURE__ */ new Set();
11873
+ const nodesById = /* @__PURE__ */ new Map();
11874
+ d.nodes.forEach((node, i) => {
11875
+ const path = `$.nodes[${i}]`;
11876
+ if (!node || typeof node !== "object") {
11877
+ errors.push({ path, message: "Each node must be an object" });
11878
+ return;
11879
+ }
11880
+ const n = node;
11881
+ if (!n.id || typeof n.id !== "string") {
11882
+ errors.push({ path: `${path}.id`, message: "Node id is required and must be a string" });
11883
+ } else {
11884
+ if (nodeIds.has(n.id)) {
11885
+ errors.push({ path: `${path}.id`, message: `Duplicate node id: ${n.id}` });
11886
+ }
11887
+ nodeIds.add(n.id);
11888
+ nodesById.set(n.id, n);
11889
+ }
11890
+ if (!n.type || typeof n.type !== "string") {
11891
+ errors.push({ path: `${path}.type`, message: "Node type is required and must be a string" });
11892
+ } else if (!allKnownTypes.has(n.type)) {
11893
+ warnings.push({ path: `${path}.type`, message: `Unknown UPG type: "${n.type}". Node will be preserved with its original type.` });
11894
+ }
11895
+ if (!n.title || typeof n.title !== "string") {
11896
+ errors.push({ path: `${path}.title`, message: "Node title is required and must be a string" });
11897
+ } else if (n.title.trim().length === 0) {
11898
+ errors.push({ path: `${path}.title`, message: "Node title must not be blank (whitespace-only)" });
11899
+ }
11900
+ if (typeof n.type === "string") {
11901
+ validateNodeProperties(n, `${path}`, warnings);
11902
+ }
11903
+ });
11904
+ if (!Array.isArray(d.edges)) {
11905
+ errors.push({ path: "$.edges", message: "edges is required and must be an array" });
11906
+ } else {
11907
+ d.edges.forEach((edge, i) => {
11908
+ const path = `$.edges[${i}]`;
11909
+ if (!edge || typeof edge !== "object") {
11910
+ errors.push({ path, message: "Each edge must be an object" });
11911
+ return;
11912
+ }
11913
+ const e = edge;
11914
+ if (!e.id || typeof e.id !== "string") {
11915
+ errors.push({ path: `${path}.id`, message: "Edge id is required and must be a string" });
11916
+ }
11917
+ if (typeof e.type === "string" && crossProductEdgeTypeSet.has(e.type)) {
11918
+ errors.push({
11919
+ path: `${path}.type`,
11920
+ message: `Cross-product edge type "${e.type}" must live in portfolio.cross_edges[], not product edges[].`
11921
+ });
11922
+ return;
11923
+ }
11924
+ if (!e.source || typeof e.source !== "string") {
11925
+ errors.push({ path: `${path}.source`, message: "Edge source is required and must be a string" });
11926
+ } else if (!nodeIds.has(e.source)) {
11927
+ errors.push({ path: `${path}.source`, message: `Edge source references unknown node id: ${e.source}` });
11928
+ }
11929
+ if (!e.target || typeof e.target !== "string") {
11930
+ errors.push({ path: `${path}.target`, message: "Edge target is required and must be a string" });
11931
+ } else if (!nodeIds.has(e.target)) {
11932
+ errors.push({ path: `${path}.target`, message: `Edge target references unknown node id: ${e.target}` });
11933
+ }
11934
+ if (typeof e.source === "string" && typeof e.target === "string" && e.source === e.target) {
11935
+ warnings.push({
11936
+ path: `${path}`,
11937
+ message: `Self-loop edge: source and target are the same node ("${e.source}"). A node related to itself is almost always an error.`,
11938
+ rule: "self-loop"
11939
+ });
11940
+ }
11941
+ if (!e.type || typeof e.type !== "string") {
11942
+ errors.push({ path: `${path}.type`, message: "Edge type is required and must be a string" });
11943
+ } else if (!allKnownEdgeTypes.has(e.type)) {
11944
+ warnings.push({
11945
+ path: `${path}.type`,
11946
+ message: `Unknown edge type: "${e.type}". Edge preserved but not in the edge registry.`
11947
+ });
11948
+ }
11949
+ });
11950
+ const valueToAxis = /* @__PURE__ */ new Map();
11951
+ const edgesArr = d.edges;
11952
+ for (const edge of edgesArr) {
11953
+ if (!edge || typeof edge !== "object") continue;
11954
+ const e = edge;
11955
+ if (e.type === "classification_axis_includes_classification_value" && typeof e.source === "string" && typeof e.target === "string") {
11956
+ valueToAxis.set(e.target, e.source);
11957
+ }
11958
+ }
11959
+ for (const [id, n] of nodesById.entries()) {
11960
+ if (n.type !== "classification_value") continue;
11961
+ if (valueToAxis.has(id)) continue;
11962
+ const pid = n.parent_id;
11963
+ if (typeof pid === "string") {
11964
+ const parent = nodesById.get(pid);
11965
+ if (parent && parent.type === "classification_axis") {
11966
+ valueToAxis.set(id, pid);
11967
+ }
11968
+ }
11969
+ }
11970
+ const axesPerAnalysis = /* @__PURE__ */ new Map();
11971
+ for (const edge of edgesArr) {
11972
+ if (!edge || typeof edge !== "object") continue;
11973
+ const e = edge;
11974
+ if (e.type === "competitive_analysis_dimensioned_by_classification_axis" && typeof e.source === "string") {
11975
+ axesPerAnalysis.set(e.source, (axesPerAnalysis.get(e.source) ?? 0) + 1);
11976
+ }
11977
+ }
11978
+ d.nodes.forEach((node, i) => {
11979
+ if (!node || typeof node !== "object") return;
11980
+ const n = node;
11981
+ if (n.type !== "competitive_analysis") return;
11982
+ const props = n.properties;
11983
+ if (!props || typeof props !== "object") return;
11984
+ const empty = props.empty_cells;
11985
+ if (!Array.isArray(empty)) return;
11986
+ const axisCount = axesPerAnalysis.get(n.id ?? "") ?? 0;
11987
+ if (axisCount !== 2) {
11988
+ warnings.push({
11989
+ path: `$.nodes[${i}].properties.empty_cells`,
11990
+ message: `empty_cells is meaningful only on a 2-axis competitive_analysis; this node is dimensioned by ${axisCount} classification_axis ${axisCount === 1 ? "child" : "children"}.`
11991
+ });
11992
+ }
11993
+ empty.forEach((cell, j) => {
11994
+ const cpath = `$.nodes[${i}].properties.empty_cells[${j}]`;
11995
+ if (!cell || typeof cell !== "object") {
11996
+ errors.push({ path: cpath, message: "empty_cells entry must be an object" });
11997
+ return;
11998
+ }
11999
+ const c = cell;
12000
+ const aRef = c.axis_a_value_ref;
12001
+ const bRef = c.axis_b_value_ref;
12002
+ if (typeof aRef !== "string") {
12003
+ errors.push({ path: `${cpath}.axis_a_value_ref`, message: "axis_a_value_ref is required and must be a string" });
12004
+ }
12005
+ if (typeof bRef !== "string") {
12006
+ errors.push({ path: `${cpath}.axis_b_value_ref`, message: "axis_b_value_ref is required and must be a string" });
12007
+ }
12008
+ if (typeof aRef !== "string" || typeof bRef !== "string") return;
12009
+ const aNode = nodesById.get(aRef);
12010
+ const bNode = nodesById.get(bRef);
12011
+ if (!aNode) {
12012
+ errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Unknown node id: ${aRef}` });
12013
+ } else if (aNode.type !== "classification_value") {
12014
+ errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Expected classification_value, found "${String(aNode.type)}" at ${aRef}` });
12015
+ }
12016
+ if (!bNode) {
12017
+ errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Unknown node id: ${bRef}` });
12018
+ } else if (bNode.type !== "classification_value") {
12019
+ errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Expected classification_value, found "${String(bNode.type)}" at ${bRef}` });
12020
+ }
12021
+ if (aNode && bNode && aNode.type === "classification_value" && bNode.type === "classification_value") {
12022
+ const aAxis = valueToAxis.get(aRef);
12023
+ const bAxis = valueToAxis.get(bRef);
12024
+ if (aAxis && bAxis && aAxis === bAxis) {
12025
+ errors.push({
12026
+ path: cpath,
12027
+ message: `empty_cells references two classification_values from the same axis (${aAxis}); axis_a and axis_b must come from distinct classification_axis parents.`
12028
+ });
12029
+ }
12030
+ }
12031
+ const kind = c.rationale_kind;
12032
+ if (kind !== void 0 && kind !== "structural" && kind !== "ideological" && kind !== "opportunity") {
12033
+ errors.push({
12034
+ path: `${cpath}.rationale_kind`,
12035
+ message: `rationale_kind must be one of 'structural' | 'ideological' | 'opportunity'; got "${String(kind)}".`
12036
+ });
12037
+ }
12038
+ });
12039
+ });
12040
+ }
12041
+ }
12042
+ return { valid: errors.length === 0, errors, warnings };
12043
+ }
12044
+ function runtimeKind(val) {
12045
+ if (val === null || val === void 0) return null;
12046
+ if (Array.isArray(val)) return "string[]";
12047
+ switch (typeof val) {
12048
+ case "string":
12049
+ return "string";
12050
+ case "number":
12051
+ return "number";
12052
+ case "boolean":
12053
+ return "boolean";
12054
+ case "object":
12055
+ return "object";
12056
+ default:
12057
+ return null;
12058
+ }
12059
+ }
12060
+ function valueMatchesType(defType, val) {
12061
+ const kind = runtimeKind(val);
12062
+ if (kind === null) return true;
12063
+ switch (defType) {
12064
+ case "string":
12065
+ return kind === "string";
12066
+ case "number":
12067
+ return kind === "number";
12068
+ case "boolean":
12069
+ return kind === "boolean";
12070
+ case "string[]":
12071
+ return kind === "string[]";
12072
+ case "object":
12073
+ case "assessment":
12074
+ return kind === "object";
12075
+ default:
12076
+ return true;
12077
+ }
12078
+ }
12079
+ function describeKind(val) {
12080
+ if (Array.isArray(val)) return "array";
12081
+ return typeof val;
12082
+ }
12083
+ function validateNodeProperties(node, basePath, warnings) {
12084
+ const type = node.type;
12085
+ if (typeof type !== "string") return;
12086
+ const schema = getPropertySchema(type);
12087
+ if (!schema) return;
12088
+ const props = node.properties;
12089
+ if (!props || typeof props !== "object" || Array.isArray(props)) return;
12090
+ for (const [key, value] of Object.entries(props)) {
12091
+ const def = schema[key];
12092
+ if (!def) continue;
12093
+ if (value === null || value === void 0) continue;
12094
+ if (!valueMatchesType(def.type, value)) {
12095
+ warnings.push({
12096
+ path: `${basePath}.properties.${key}`,
12097
+ message: `Property "${key}" on ${type} should be ${def.type}, got ${describeKind(value)}.`,
12098
+ rule: "property-type"
12099
+ });
12100
+ continue;
12101
+ }
12102
+ if (def.enum && typeof value === "string" && !def.enum.includes(value)) {
12103
+ warnings.push({
12104
+ path: `${basePath}.properties.${key}`,
12105
+ message: `Property "${key}" on ${type} has value "${value}" outside its allowed set: ${def.enum.join(", ")}.`,
12106
+ rule: "property-enum"
12107
+ });
12108
+ }
12109
+ }
12110
+ }
12111
+ function isUPGDocument(doc) {
12112
+ return validateUPGDocument(doc).valid;
12113
+ }
12114
+
12115
+ // src/frameworks/canonical.ts
12116
+ var UPG_FRAMEWORKS = [
12117
+ {
12118
+ "id": "opportunity-solution-tree",
12119
+ "approach_ids": [
12120
+ "trace"
12121
+ ],
12122
+ "name": "Opportunity Solution Tree",
12123
+ "version": "1.0.0",
12124
+ "description": "Map desired outcomes to opportunities, then branch into solutions and experiments. Ensures every solution traces back to a real user need.",
12125
+ "category": "discovery",
12126
+ "origin": {
12127
+ "type": "practitioner",
12128
+ "attribution": "Teresa Torres",
12129
+ "description": "Introduced in Continuous Discovery Habits. Maps outcomes to opportunities, solutions, and experiments.",
12130
+ "url": "https://www.producttalk.org/opportunity-solution-tree/",
12131
+ "year": 2021,
12132
+ "license": "published_methodology"
12133
+ },
12134
+ "tags": [
12135
+ "discovery",
12136
+ "tree"
12137
+ ],
12138
+ "slots": [
12139
+ {
12140
+ "label": "Root Outcome",
12141
+ "entityTypeId": "outcome",
12142
+ "description": "The desired business or user outcome"
12143
+ },
12144
+ {
12145
+ "label": "Opportunities",
12146
+ "entityTypeId": "opportunity",
12147
+ "description": "User needs, pain points, or desires"
12148
+ },
12149
+ {
12150
+ "label": "Solutions",
12151
+ "entityTypeId": "solution",
12152
+ "description": "Ideas to address each opportunity"
12153
+ },
12154
+ {
12155
+ "label": "Experiments",
12156
+ "entityTypeId": "experiment_run",
12157
+ "description": "Tests to validate each solution"
12158
+ }
12159
+ ],
12160
+ "data": {
12161
+ "entity_types": [
12162
+ {
12163
+ "type": "outcome",
12164
+ "role": "root"
12165
+ },
12166
+ {
12167
+ "type": "opportunity",
12083
12168
  "role": "item"
12084
12169
  },
12085
12170
  {
@@ -13263,7 +13348,7 @@ var UPG_FRAMEWORKS = [
13263
13348
  ],
13264
13349
  "name": "RICE Scoring",
13265
13350
  "version": "1.0.0",
13266
- "description": "Score features and opportunities by Reach, Impact, Confidence, and Effort to produce a ranked priority list.",
13351
+ "description": "Score features, opportunities, and needs by Reach, Impact, Confidence, and Effort to produce a ranked priority list.",
13267
13352
  "category": "prioritization",
13268
13353
  "origin": {
13269
13354
  "type": "practitioner",
@@ -13281,7 +13366,17 @@ var UPG_FRAMEWORKS = [
13281
13366
  {
13282
13367
  "label": "Items to score",
13283
13368
  "entityTypeId": "feature",
13284
- "description": "Features, opportunities, or solutions being evaluated"
13369
+ "description": "Features, opportunities, or needs being evaluated"
13370
+ },
13371
+ {
13372
+ "label": "Opportunities to score",
13373
+ "entityTypeId": "opportunity",
13374
+ "description": "Opportunities scored on the same RICE Scoring inputs as features."
13375
+ },
13376
+ {
13377
+ "label": "Needs to score",
13378
+ "entityTypeId": "need",
13379
+ "description": "Needs scored on the same RICE Scoring inputs as features."
13285
13380
  }
13286
13381
  ],
13287
13382
  "data": {
@@ -13289,6 +13384,14 @@ var UPG_FRAMEWORKS = [
13289
13384
  {
13290
13385
  "type": "feature",
13291
13386
  "role": "scored_item"
13387
+ },
13388
+ {
13389
+ "type": "opportunity",
13390
+ "role": "scored_item"
13391
+ },
13392
+ {
13393
+ "type": "need",
13394
+ "role": "scored_item"
13292
13395
  }
13293
13396
  ],
13294
13397
  "required_properties": {
@@ -13329,6 +13432,82 @@ var UPG_FRAMEWORKS = [
13329
13432
  "label": "Effort",
13330
13433
  "description": "How much work is required to build and ship this, on the effort scale?"
13331
13434
  }
13435
+ ],
13436
+ "opportunity": [
13437
+ {
13438
+ "property": "reach",
13439
+ "type": "assessment",
13440
+ "scale_id": "reach_5",
13441
+ "required": true,
13442
+ "scope": "framework",
13443
+ "label": "Reach",
13444
+ "description": "How many users will this impact per quarter?"
13445
+ },
13446
+ {
13447
+ "property": "impact",
13448
+ "type": "assessment",
13449
+ "scale_id": "impact_5",
13450
+ "required": true,
13451
+ "scope": "framework",
13452
+ "label": "Impact",
13453
+ "description": "How much will this impact each user, on the impact scale?"
13454
+ },
13455
+ {
13456
+ "property": "confidence",
13457
+ "type": "assessment",
13458
+ "scale_id": "confidence_5",
13459
+ "required": true,
13460
+ "scope": "framework",
13461
+ "label": "Confidence",
13462
+ "description": "How confident are you in the reach, impact, and effort estimates?"
13463
+ },
13464
+ {
13465
+ "property": "effort",
13466
+ "type": "assessment",
13467
+ "scale_id": "effort_5",
13468
+ "required": true,
13469
+ "scope": "framework",
13470
+ "label": "Effort",
13471
+ "description": "How much work is required to build and ship this, on the effort scale?"
13472
+ }
13473
+ ],
13474
+ "need": [
13475
+ {
13476
+ "property": "reach",
13477
+ "type": "assessment",
13478
+ "scale_id": "reach_5",
13479
+ "required": true,
13480
+ "scope": "framework",
13481
+ "label": "Reach",
13482
+ "description": "How many users will this impact per quarter?"
13483
+ },
13484
+ {
13485
+ "property": "impact",
13486
+ "type": "assessment",
13487
+ "scale_id": "impact_5",
13488
+ "required": true,
13489
+ "scope": "framework",
13490
+ "label": "Impact",
13491
+ "description": "How much will this impact each user, on the impact scale?"
13492
+ },
13493
+ {
13494
+ "property": "confidence",
13495
+ "type": "assessment",
13496
+ "scale_id": "confidence_5",
13497
+ "required": true,
13498
+ "scope": "framework",
13499
+ "label": "Confidence",
13500
+ "description": "How confident are you in the reach, impact, and effort estimates?"
13501
+ },
13502
+ {
13503
+ "property": "effort",
13504
+ "type": "assessment",
13505
+ "scale_id": "effort_5",
13506
+ "required": true,
13507
+ "scope": "framework",
13508
+ "label": "Effort",
13509
+ "description": "How much work is required to build and ship this, on the effort scale?"
13510
+ }
13332
13511
  ]
13333
13512
  },
13334
13513
  "computed_properties": [
@@ -13338,8 +13517,75 @@ var UPG_FRAMEWORKS = [
13338
13517
  "entity_type": "feature",
13339
13518
  "label": "RICE Score",
13340
13519
  "format": "number"
13520
+ },
13521
+ {
13522
+ "property": "rice_score",
13523
+ "expression": "(reach * impact * confidence) / effort",
13524
+ "entity_type": "opportunity",
13525
+ "label": "RICE Score",
13526
+ "format": "number"
13527
+ },
13528
+ {
13529
+ "property": "rice_score",
13530
+ "expression": "(reach * impact * confidence) / effort",
13531
+ "entity_type": "need",
13532
+ "label": "RICE Score",
13533
+ "format": "number"
13341
13534
  }
13342
- ]
13535
+ ],
13536
+ "scoring_lens": {
13537
+ "applies_to": [
13538
+ "feature",
13539
+ "opportunity",
13540
+ "need"
13541
+ ],
13542
+ "inputs": [
13543
+ {
13544
+ "property": "reach",
13545
+ "type": "assessment",
13546
+ "scale_id": "reach_5",
13547
+ "required": true,
13548
+ "scope": "framework",
13549
+ "label": "Reach",
13550
+ "description": "How many users will this impact per quarter?"
13551
+ },
13552
+ {
13553
+ "property": "impact",
13554
+ "type": "assessment",
13555
+ "scale_id": "impact_5",
13556
+ "required": true,
13557
+ "scope": "framework",
13558
+ "label": "Impact",
13559
+ "description": "How much will this impact each user, on the impact scale?"
13560
+ },
13561
+ {
13562
+ "property": "confidence",
13563
+ "type": "assessment",
13564
+ "scale_id": "confidence_5",
13565
+ "required": true,
13566
+ "scope": "framework",
13567
+ "label": "Confidence",
13568
+ "description": "How confident are you in the reach, impact, and effort estimates?"
13569
+ },
13570
+ {
13571
+ "property": "effort",
13572
+ "type": "assessment",
13573
+ "scale_id": "effort_5",
13574
+ "required": true,
13575
+ "scope": "framework",
13576
+ "label": "Effort",
13577
+ "description": "How much work is required to build and ship this, on the effort scale?"
13578
+ }
13579
+ ],
13580
+ "computed": [
13581
+ {
13582
+ "property": "rice_score",
13583
+ "expression": "(reach * impact * confidence) / effort",
13584
+ "label": "RICE Score",
13585
+ "format": "number"
13586
+ }
13587
+ ]
13588
+ }
13343
13589
  },
13344
13590
  "structure": {
13345
13591
  "pattern": "table"
@@ -15464,7 +15710,7 @@ var UPG_FRAMEWORKS = [
15464
15710
  "entity_types": [
15465
15711
  {
15466
15712
  "type": "team",
15467
- "role": "scored_item"
15713
+ "role": "item"
15468
15714
  },
15469
15715
  {
15470
15716
  "type": "retrospective",
@@ -15721,6 +15967,16 @@ var UPG_FRAMEWORKS = [
15721
15967
  "label": "Ease",
15722
15968
  "entityTypeId": "feature",
15723
15969
  "description": "How easy is this to implement?"
15970
+ },
15971
+ {
15972
+ "label": "Opportunities to score",
15973
+ "entityTypeId": "opportunity",
15974
+ "description": "Opportunities scored on the same ICE Scoring inputs as features."
15975
+ },
15976
+ {
15977
+ "label": "Needs to score",
15978
+ "entityTypeId": "need",
15979
+ "description": "Needs scored on the same ICE Scoring inputs as features."
15724
15980
  }
15725
15981
  ],
15726
15982
  "data": {
@@ -15736,6 +15992,14 @@ var UPG_FRAMEWORKS = [
15736
15992
  {
15737
15993
  "type": "assumption",
15738
15994
  "role": "item"
15995
+ },
15996
+ {
15997
+ "type": "opportunity",
15998
+ "role": "scored_item"
15999
+ },
16000
+ {
16001
+ "type": "need",
16002
+ "role": "scored_item"
15739
16003
  }
15740
16004
  ],
15741
16005
  "required_properties": {
@@ -15764,6 +16028,58 @@ var UPG_FRAMEWORKS = [
15764
16028
  "label": "Ease",
15765
16029
  "description": "Ease of implementation (1-10)"
15766
16030
  }
16031
+ ],
16032
+ "opportunity": [
16033
+ {
16034
+ "property": "impact",
16035
+ "type": "number",
16036
+ "required": true,
16037
+ "scope": "framework",
16038
+ "label": "Impact",
16039
+ "description": "Expected impact on the target metric (1-10)"
16040
+ },
16041
+ {
16042
+ "property": "confidence",
16043
+ "type": "number",
16044
+ "required": true,
16045
+ "scope": "framework",
16046
+ "label": "Confidence",
16047
+ "description": "Confidence in the impact estimate (1-10)"
16048
+ },
16049
+ {
16050
+ "property": "ease",
16051
+ "type": "number",
16052
+ "required": true,
16053
+ "scope": "framework",
16054
+ "label": "Ease",
16055
+ "description": "Ease of implementation (1-10)"
16056
+ }
16057
+ ],
16058
+ "need": [
16059
+ {
16060
+ "property": "impact",
16061
+ "type": "number",
16062
+ "required": true,
16063
+ "scope": "framework",
16064
+ "label": "Impact",
16065
+ "description": "Expected impact on the target metric (1-10)"
16066
+ },
16067
+ {
16068
+ "property": "confidence",
16069
+ "type": "number",
16070
+ "required": true,
16071
+ "scope": "framework",
16072
+ "label": "Confidence",
16073
+ "description": "Confidence in the impact estimate (1-10)"
16074
+ },
16075
+ {
16076
+ "property": "ease",
16077
+ "type": "number",
16078
+ "required": true,
16079
+ "scope": "framework",
16080
+ "label": "Ease",
16081
+ "description": "Ease of implementation (1-10)"
16082
+ }
15767
16083
  ]
15768
16084
  },
15769
16085
  "computed_properties": [
@@ -15773,8 +16089,63 @@ var UPG_FRAMEWORKS = [
15773
16089
  "entity_type": "feature",
15774
16090
  "label": "ICE Score",
15775
16091
  "format": "number"
16092
+ },
16093
+ {
16094
+ "property": "ice_score",
16095
+ "expression": "impact * confidence * ease",
16096
+ "entity_type": "opportunity",
16097
+ "label": "ICE Score",
16098
+ "format": "number"
16099
+ },
16100
+ {
16101
+ "property": "ice_score",
16102
+ "expression": "impact * confidence * ease",
16103
+ "entity_type": "need",
16104
+ "label": "ICE Score",
16105
+ "format": "number"
15776
16106
  }
15777
- ]
16107
+ ],
16108
+ "scoring_lens": {
16109
+ "applies_to": [
16110
+ "feature",
16111
+ "opportunity",
16112
+ "need"
16113
+ ],
16114
+ "inputs": [
16115
+ {
16116
+ "property": "impact",
16117
+ "type": "number",
16118
+ "required": true,
16119
+ "scope": "framework",
16120
+ "label": "Impact",
16121
+ "description": "Expected impact on the target metric (1-10)"
16122
+ },
16123
+ {
16124
+ "property": "confidence",
16125
+ "type": "number",
16126
+ "required": true,
16127
+ "scope": "framework",
16128
+ "label": "Confidence",
16129
+ "description": "Confidence in the impact estimate (1-10)"
16130
+ },
16131
+ {
16132
+ "property": "ease",
16133
+ "type": "number",
16134
+ "required": true,
16135
+ "scope": "framework",
16136
+ "label": "Ease",
16137
+ "description": "Ease of implementation (1-10)"
16138
+ }
16139
+ ],
16140
+ "computed": [
16141
+ {
16142
+ "property": "ice_score",
16143
+ "expression": "impact * confidence * ease",
16144
+ "label": "ICE Score",
16145
+ "format": "number"
16146
+ }
16147
+ ]
16148
+ }
15778
16149
  },
15779
16150
  "structure": {
15780
16151
  "pattern": "table"
@@ -15876,6 +16247,11 @@ var UPG_FRAMEWORKS = [
15876
16247
  "label": "Job Size",
15877
16248
  "entityTypeId": "metric",
15878
16249
  "description": "Estimated effort (story points, t-shirt size, or person-weeks)"
16250
+ },
16251
+ {
16252
+ "label": "Opportunities to score",
16253
+ "entityTypeId": "opportunity",
16254
+ "description": "Opportunities scored on the same WSJF (Weighted Shortest Job First) inputs as features."
15879
16255
  }
15880
16256
  ],
15881
16257
  "data": {
@@ -15887,6 +16263,10 @@ var UPG_FRAMEWORKS = [
15887
16263
  {
15888
16264
  "type": "metric",
15889
16265
  "role": "item"
16266
+ },
16267
+ {
16268
+ "type": "opportunity",
16269
+ "role": "scored_item"
15890
16270
  }
15891
16271
  ],
15892
16272
  "required_properties": {
@@ -15923,6 +16303,40 @@ var UPG_FRAMEWORKS = [
15923
16303
  "label": "Job Size",
15924
16304
  "description": "Estimated effort (story points, t-shirt size, or person-weeks)"
15925
16305
  }
16306
+ ],
16307
+ "opportunity": [
16308
+ {
16309
+ "property": "user_value",
16310
+ "type": "number",
16311
+ "required": true,
16312
+ "scope": "framework",
16313
+ "label": "User/Business Value",
16314
+ "description": "Relative value to users and the business if delivered"
16315
+ },
16316
+ {
16317
+ "property": "time_criticality",
16318
+ "type": "number",
16319
+ "required": true,
16320
+ "scope": "framework",
16321
+ "label": "Time Criticality",
16322
+ "description": "How much value decays if delivery is delayed (deadlines, competition, seasonal windows)"
16323
+ },
16324
+ {
16325
+ "property": "risk_reduction",
16326
+ "type": "number",
16327
+ "required": true,
16328
+ "scope": "framework",
16329
+ "label": "Risk Reduction / Opportunity Enablement",
16330
+ "description": "Value from reducing risk or enabling future opportunities"
16331
+ },
16332
+ {
16333
+ "property": "job_size",
16334
+ "type": "number",
16335
+ "required": true,
16336
+ "scope": "framework",
16337
+ "label": "Job Size",
16338
+ "description": "Estimated effort (story points, t-shirt size, or person-weeks)"
16339
+ }
15926
16340
  ]
15927
16341
  },
15928
16342
  "computed_properties": [
@@ -15932,8 +16346,63 @@ var UPG_FRAMEWORKS = [
15932
16346
  "entity_type": "feature",
15933
16347
  "label": "WSJF Score",
15934
16348
  "format": "number"
16349
+ },
16350
+ {
16351
+ "property": "wsjf_score",
16352
+ "expression": "(user_value + time_criticality + risk_reduction) / job_size",
16353
+ "entity_type": "opportunity",
16354
+ "label": "WSJF Score",
16355
+ "format": "number"
15935
16356
  }
15936
- ]
16357
+ ],
16358
+ "scoring_lens": {
16359
+ "applies_to": [
16360
+ "feature",
16361
+ "opportunity"
16362
+ ],
16363
+ "inputs": [
16364
+ {
16365
+ "property": "user_value",
16366
+ "type": "number",
16367
+ "required": true,
16368
+ "scope": "framework",
16369
+ "label": "User/Business Value",
16370
+ "description": "Relative value to users and the business if delivered"
16371
+ },
16372
+ {
16373
+ "property": "time_criticality",
16374
+ "type": "number",
16375
+ "required": true,
16376
+ "scope": "framework",
16377
+ "label": "Time Criticality",
16378
+ "description": "How much value decays if delivery is delayed (deadlines, competition, seasonal windows)"
16379
+ },
16380
+ {
16381
+ "property": "risk_reduction",
16382
+ "type": "number",
16383
+ "required": true,
16384
+ "scope": "framework",
16385
+ "label": "Risk Reduction / Opportunity Enablement",
16386
+ "description": "Value from reducing risk or enabling future opportunities"
16387
+ },
16388
+ {
16389
+ "property": "job_size",
16390
+ "type": "number",
16391
+ "required": true,
16392
+ "scope": "framework",
16393
+ "label": "Job Size",
16394
+ "description": "Estimated effort (story points, t-shirt size, or person-weeks)"
16395
+ }
16396
+ ],
16397
+ "computed": [
16398
+ {
16399
+ "property": "wsjf_score",
16400
+ "expression": "(user_value + time_criticality + risk_reduction) / job_size",
16401
+ "label": "WSJF Score",
16402
+ "format": "number"
16403
+ }
16404
+ ]
16405
+ }
15937
16406
  },
15938
16407
  "structure": {
15939
16408
  "pattern": "table"
@@ -16006,7 +16475,7 @@ var UPG_FRAMEWORKS = [
16006
16475
  ],
16007
16476
  "name": "Cost of Delay",
16008
16477
  "version": "1.0.0",
16009
- "description": "Quantify the economic cost of not shipping a feature to drive priority decisions. Combines urgency with value.",
16478
+ "description": "Quantify the economic cost of not shipping a feature or opportunity to drive priority decisions. Combines urgency with value.",
16010
16479
  "category": "prioritization",
16011
16480
  "origin": {
16012
16481
  "type": "practitioner",
@@ -16039,6 +16508,11 @@ var UPG_FRAMEWORKS = [
16039
16508
  "label": "Risk Reduction",
16040
16509
  "entityTypeId": "risk",
16041
16510
  "description": "What risk does this mitigate?"
16511
+ },
16512
+ {
16513
+ "label": "Opportunities to score",
16514
+ "entityTypeId": "opportunity",
16515
+ "description": "Opportunities scored on the same Cost of Delay inputs as features."
16042
16516
  }
16043
16517
  ],
16044
16518
  "data": {
@@ -16058,6 +16532,10 @@ var UPG_FRAMEWORKS = [
16058
16532
  {
16059
16533
  "type": "risk",
16060
16534
  "role": "item"
16535
+ },
16536
+ {
16537
+ "type": "opportunity",
16538
+ "role": "scored_item"
16061
16539
  }
16062
16540
  ],
16063
16541
  "required_properties": {
@@ -16078,6 +16556,24 @@ var UPG_FRAMEWORKS = [
16078
16556
  "label": "Job Size",
16079
16557
  "description": "Weeks of development effort"
16080
16558
  }
16559
+ ],
16560
+ "opportunity": [
16561
+ {
16562
+ "property": "cost_of_delay",
16563
+ "type": "number",
16564
+ "required": true,
16565
+ "scope": "framework",
16566
+ "label": "Cost of Delay",
16567
+ "description": "Weekly revenue impact of not shipping"
16568
+ },
16569
+ {
16570
+ "property": "job_size",
16571
+ "type": "number",
16572
+ "required": true,
16573
+ "scope": "framework",
16574
+ "label": "Job Size",
16575
+ "description": "Weeks of development effort"
16576
+ }
16081
16577
  ]
16082
16578
  },
16083
16579
  "computed_properties": [
@@ -16087,8 +16583,47 @@ var UPG_FRAMEWORKS = [
16087
16583
  "entity_type": "feature",
16088
16584
  "label": "WSJF Score",
16089
16585
  "format": "number"
16586
+ },
16587
+ {
16588
+ "property": "wsjf_score",
16589
+ "expression": "cost_of_delay / job_size",
16590
+ "entity_type": "opportunity",
16591
+ "label": "WSJF Score",
16592
+ "format": "number"
16090
16593
  }
16091
- ]
16594
+ ],
16595
+ "scoring_lens": {
16596
+ "applies_to": [
16597
+ "feature",
16598
+ "opportunity"
16599
+ ],
16600
+ "inputs": [
16601
+ {
16602
+ "property": "cost_of_delay",
16603
+ "type": "number",
16604
+ "required": true,
16605
+ "scope": "framework",
16606
+ "label": "Cost of Delay",
16607
+ "description": "Weekly revenue impact of not shipping"
16608
+ },
16609
+ {
16610
+ "property": "job_size",
16611
+ "type": "number",
16612
+ "required": true,
16613
+ "scope": "framework",
16614
+ "label": "Job Size",
16615
+ "description": "Weeks of development effort"
16616
+ }
16617
+ ],
16618
+ "computed": [
16619
+ {
16620
+ "property": "wsjf_score",
16621
+ "expression": "cost_of_delay / job_size",
16622
+ "label": "WSJF Score",
16623
+ "format": "number"
16624
+ }
16625
+ ]
16626
+ }
16092
16627
  },
16093
16628
  "structure": {
16094
16629
  "pattern": "table"
@@ -24989,7 +25524,7 @@ function isCanonical(text) {
24989
25524
  }
24990
25525
 
24991
25526
  // src/index.ts
24992
- var UPG_VERSION = "0.8.4";
25527
+ var UPG_VERSION = "0.8.7";
24993
25528
  var UPG_FORMAT_VERSION = "0.4.0";
24994
25529
  var MARKDOWN_FORMAT_VERSION = "0.1";
24995
25530
  var UPG_TYPES = getTypes();
@@ -25058,6 +25593,7 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
25058
25593
  BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK,
25059
25594
  BUSINESS_GTM_GROWTH_PLAYBOOK,
25060
25595
  BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK,
25596
+ CONTENT_DEPTH_WARNING_RULES,
25061
25597
  DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK,
25062
25598
  ENGINEERING_PLATFORM_PLAYBOOK,
25063
25599
  EXPERIENCE_DESIGN_BRAND_PLAYBOOK,