@unified-product-graph/core 0.8.7 → 0.8.9

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
@@ -11825,293 +11825,6 @@ function getPropertySchema(entityType) {
11825
11825
  return UPG_PROPERTY_SCHEMA[entityType];
11826
11826
  }
11827
11827
 
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
11828
  // src/frameworks/canonical.ts
12116
11829
  var UPG_FRAMEWORKS = [
12117
11830
  {
@@ -12330,31 +12043,37 @@ var UPG_FRAMEWORKS = [
12330
12043
  "slots": [
12331
12044
  {
12332
12045
  "label": "Customer Jobs",
12046
+ "role": "customer_job",
12333
12047
  "entityTypeId": "job",
12334
12048
  "description": "Place job entities in the Customer Jobs position of the matrix"
12335
12049
  },
12336
12050
  {
12337
12051
  "label": "Pains",
12052
+ "role": "pain",
12338
12053
  "entityTypeId": "need",
12339
12054
  "description": "Place need entities in the Pains position of the matrix"
12340
12055
  },
12341
12056
  {
12342
12057
  "label": "Gains",
12058
+ "role": "gain",
12343
12059
  "entityTypeId": "desired_outcome",
12344
12060
  "description": "Place desired outcome entities in the Gains position of the matrix"
12345
12061
  },
12346
12062
  {
12347
12063
  "label": "Products & Services",
12064
+ "role": "product_or_service",
12348
12065
  "entityTypeId": "feature",
12349
12066
  "description": "Place feature entities in the Products & Services position of the matrix"
12350
12067
  },
12351
12068
  {
12352
12069
  "label": "Pain Relievers",
12070
+ "role": "pain_reliever",
12353
12071
  "entityTypeId": "feature",
12354
12072
  "description": "Place feature entities in the Pain Relievers position of the matrix"
12355
12073
  },
12356
12074
  {
12357
12075
  "label": "Gain Creators",
12076
+ "role": "gain_creator",
12358
12077
  "entityTypeId": "feature",
12359
12078
  "description": "Place feature entities in the Gain Creators position of the matrix"
12360
12079
  }
@@ -12983,26 +12702,31 @@ var UPG_FRAMEWORKS = [
12983
12702
  "slots": [
12984
12703
  {
12985
12704
  "label": "Competitive Rivalry",
12705
+ "role": "competitive_rivalry",
12986
12706
  "entityTypeId": "competitor",
12987
12707
  "description": "Intensity of competition among existing players"
12988
12708
  },
12989
12709
  {
12990
12710
  "label": "Threat of New Entrants",
12711
+ "role": "new_entrants",
12991
12712
  "entityTypeId": "competitor",
12992
12713
  "description": "How easy is it for new competitors to enter?"
12993
12714
  },
12994
12715
  {
12995
12716
  "label": "Threat of Substitutes",
12717
+ "role": "substitutes",
12996
12718
  "entityTypeId": "competitor",
12997
12719
  "description": "Can customers switch to alternatives?"
12998
12720
  },
12999
12721
  {
13000
12722
  "label": "Buyer Power",
12723
+ "role": "buyer_power",
13001
12724
  "entityTypeId": "persona",
13002
12725
  "description": "How much leverage do buyers have?"
13003
12726
  },
13004
12727
  {
13005
12728
  "label": "Supplier Power",
12729
+ "role": "supplier_power",
13006
12730
  "entityTypeId": "persona",
13007
12731
  "description": "How much leverage do suppliers have?"
13008
12732
  }
@@ -13170,31 +12894,37 @@ var UPG_FRAMEWORKS = [
13170
12894
  "slots": [
13171
12895
  {
13172
12896
  "label": "Inbound Logistics",
12897
+ "role": "inbound_logistics",
13173
12898
  "entityTypeId": "key_activity",
13174
12899
  "description": "Inbound Logistics phase: key activity entities move through this stage"
13175
12900
  },
13176
12901
  {
13177
12902
  "label": "Operations",
12903
+ "role": "operations",
13178
12904
  "entityTypeId": "key_activity",
13179
12905
  "description": "Operations phase: key activity entities move through this stage"
13180
12906
  },
13181
12907
  {
13182
12908
  "label": "Outbound Logistics",
12909
+ "role": "outbound_logistics",
13183
12910
  "entityTypeId": "key_activity",
13184
12911
  "description": "Outbound Logistics phase: key activity entities move through this stage"
13185
12912
  },
13186
12913
  {
13187
12914
  "label": "Marketing & Sales",
12915
+ "role": "marketing_and_sales",
13188
12916
  "entityTypeId": "key_activity",
13189
12917
  "description": "Marketing & Sales phase: key activity entities move through this stage"
13190
12918
  },
13191
12919
  {
13192
12920
  "label": "Service",
12921
+ "role": "service",
13193
12922
  "entityTypeId": "key_activity",
13194
12923
  "description": "Service phase: key activity entities move through this stage"
13195
12924
  },
13196
12925
  {
13197
12926
  "label": "Support Activities",
12927
+ "role": "support_activity",
13198
12928
  "entityTypeId": "capability",
13199
12929
  "description": "Support Activities phase: capability entities move through this stage"
13200
12930
  }
@@ -13265,26 +12995,31 @@ var UPG_FRAMEWORKS = [
13265
12995
  "slots": [
13266
12996
  {
13267
12997
  "label": "Portfolio",
12998
+ "role": "portfolio",
13268
12999
  "entityTypeId": "portfolio",
13269
13000
  "description": "Innovation portfolio balanced across three horizons: core performance, emerging growth, and future creation"
13270
13001
  },
13271
13002
  {
13272
13003
  "label": "Product",
13004
+ "role": "product",
13273
13005
  "entityTypeId": "product",
13274
13006
  "description": "Product or business unit assigned to a horizon: H1 (defend and extend), H2 (build), or H3 (seed and explore)"
13275
13007
  },
13276
13008
  {
13277
13009
  "label": "Initiative",
13010
+ "role": "initiative",
13278
13011
  "entityTypeId": "portfolio",
13279
13012
  "description": "Growth initiative positioned in a horizon, with appropriate funding model, governance, and success metrics"
13280
13013
  },
13281
13014
  {
13282
13015
  "label": "Product Area",
13016
+ "role": "product_area",
13283
13017
  "entityTypeId": "product_area",
13284
13018
  "description": "Product area representing a horizon: core products (H1), adjacent expansions (H2), or experimental ventures (H3)"
13285
13019
  },
13286
13020
  {
13287
13021
  "label": "Capability",
13022
+ "role": "capability",
13288
13023
  "entityTypeId": "capability",
13289
13024
  "description": "Capability needed to execute across horizons: H1 needs efficiency, H2 needs scaling, H3 needs discovery"
13290
13025
  }
@@ -13533,7 +13268,7 @@ var UPG_FRAMEWORKS = [
13533
13268
  "format": "number"
13534
13269
  }
13535
13270
  ],
13536
- "scoring_lens": {
13271
+ "scoring_method": {
13537
13272
  "applies_to": [
13538
13273
  "feature",
13539
13274
  "opportunity",
@@ -13764,21 +13499,25 @@ var UPG_FRAMEWORKS = [
13764
13499
  "slots": [
13765
13500
  {
13766
13501
  "label": "Must-haves",
13502
+ "role": "must_have",
13767
13503
  "entityTypeId": "feature",
13768
13504
  "description": "Expected features: absence causes dissatisfaction"
13769
13505
  },
13770
13506
  {
13771
13507
  "label": "Performance",
13508
+ "role": "performance",
13772
13509
  "entityTypeId": "feature",
13773
13510
  "description": "More is better: linear satisfaction increase"
13774
13511
  },
13775
13512
  {
13776
13513
  "label": "Delighters",
13514
+ "role": "delighter",
13777
13515
  "entityTypeId": "feature",
13778
13516
  "description": "Unexpected features: presence creates delight"
13779
13517
  },
13780
13518
  {
13781
13519
  "label": "Indifferent",
13520
+ "role": "indifferent",
13782
13521
  "entityTypeId": "feature",
13783
13522
  "description": "Features users don't care about either way"
13784
13523
  }
@@ -13786,12 +13525,97 @@ var UPG_FRAMEWORKS = [
13786
13525
  "data": {
13787
13526
  "entity_types": [
13788
13527
  {
13789
- "type": "feature",
13790
- "role": "item"
13528
+ "type": "feature",
13529
+ "role": "scored_item"
13530
+ }
13531
+ ],
13532
+ "required_properties": {
13533
+ "feature": [
13534
+ {
13535
+ "property": "functional_response",
13536
+ "type": "enum",
13537
+ "required": true,
13538
+ "scope": "framework",
13539
+ "label": "Functional Response",
13540
+ "description": "How users feel when the feature IS present",
13541
+ "enum_values": [
13542
+ "i_like_it",
13543
+ "i_expect_it",
13544
+ "i_am_neutral",
13545
+ "i_can_tolerate_it",
13546
+ "i_dislike_it"
13547
+ ]
13548
+ },
13549
+ {
13550
+ "property": "dysfunctional_response",
13551
+ "type": "enum",
13552
+ "required": true,
13553
+ "scope": "framework",
13554
+ "label": "Dysfunctional Response",
13555
+ "description": "How users feel when the feature IS NOT present",
13556
+ "enum_values": [
13557
+ "i_like_it",
13558
+ "i_expect_it",
13559
+ "i_am_neutral",
13560
+ "i_can_tolerate_it",
13561
+ "i_dislike_it"
13562
+ ]
13563
+ },
13564
+ {
13565
+ "property": "delighter_count",
13566
+ "type": "number",
13567
+ "required": false,
13568
+ "scope": "framework",
13569
+ "label": "Delighter classifications",
13570
+ "description": "Count of survey responses classifying this feature as a delighter (attractive)"
13571
+ },
13572
+ {
13573
+ "property": "performance_count",
13574
+ "type": "number",
13575
+ "required": false,
13576
+ "scope": "framework",
13577
+ "label": "Performance classifications",
13578
+ "description": "Count of survey responses classifying this feature as performance (one-dimensional)"
13579
+ },
13580
+ {
13581
+ "property": "must_be_count",
13582
+ "type": "number",
13583
+ "required": false,
13584
+ "scope": "framework",
13585
+ "label": "Must-be classifications",
13586
+ "description": "Count of survey responses classifying this feature as must-be (basic)"
13587
+ },
13588
+ {
13589
+ "property": "indifferent_count",
13590
+ "type": "number",
13591
+ "required": false,
13592
+ "scope": "framework",
13593
+ "label": "Indifferent classifications",
13594
+ "description": "Count of survey responses classifying this feature as indifferent"
13595
+ }
13596
+ ]
13597
+ },
13598
+ "computed_properties": [
13599
+ {
13600
+ "property": "satisfaction_coefficient",
13601
+ "expression": "(delighter_count + performance_count) / (delighter_count + performance_count + must_be_count + indifferent_count)",
13602
+ "entity_type": "feature",
13603
+ "label": "Satisfaction Coefficient",
13604
+ "format": "number"
13605
+ },
13606
+ {
13607
+ "property": "dissatisfaction_coefficient",
13608
+ "expression": "(must_be_count + performance_count) / (delighter_count + performance_count + must_be_count + indifferent_count) * -1",
13609
+ "entity_type": "feature",
13610
+ "label": "Dissatisfaction Coefficient",
13611
+ "format": "number"
13791
13612
  }
13792
13613
  ],
13793
- "required_properties": {
13794
- "feature": [
13614
+ "scoring_method": {
13615
+ "applies_to": [
13616
+ "feature"
13617
+ ],
13618
+ "inputs": [
13795
13619
  {
13796
13620
  "property": "functional_response",
13797
13621
  "type": "enum",
@@ -13854,24 +13678,22 @@ var UPG_FRAMEWORKS = [
13854
13678
  "label": "Indifferent classifications",
13855
13679
  "description": "Count of survey responses classifying this feature as indifferent"
13856
13680
  }
13681
+ ],
13682
+ "computed": [
13683
+ {
13684
+ "property": "satisfaction_coefficient",
13685
+ "expression": "(delighter_count + performance_count) / (delighter_count + performance_count + must_be_count + indifferent_count)",
13686
+ "label": "Satisfaction Coefficient",
13687
+ "format": "number"
13688
+ },
13689
+ {
13690
+ "property": "dissatisfaction_coefficient",
13691
+ "expression": "(must_be_count + performance_count) / (delighter_count + performance_count + must_be_count + indifferent_count) * -1",
13692
+ "label": "Dissatisfaction Coefficient",
13693
+ "format": "number"
13694
+ }
13857
13695
  ]
13858
- },
13859
- "computed_properties": [
13860
- {
13861
- "property": "satisfaction_coefficient",
13862
- "expression": "(delighter_count + performance_count) / (delighter_count + performance_count + must_be_count + indifferent_count)",
13863
- "entity_type": "feature",
13864
- "label": "Satisfaction Coefficient",
13865
- "format": "number"
13866
- },
13867
- {
13868
- "property": "dissatisfaction_coefficient",
13869
- "expression": "(must_be_count + performance_count) / (delighter_count + performance_count + must_be_count + indifferent_count) * -1",
13870
- "entity_type": "feature",
13871
- "label": "Dissatisfaction Coefficient",
13872
- "format": "number"
13873
- }
13874
- ]
13696
+ }
13875
13697
  },
13876
13698
  "structure": {
13877
13699
  "pattern": "matrix"
@@ -13926,16 +13748,19 @@ var UPG_FRAMEWORKS = [
13926
13748
  "slots": [
13927
13749
  {
13928
13750
  "label": "Now",
13751
+ "role": "now",
13929
13752
  "entityTypeId": "feature",
13930
13753
  "description": "Committed work in progress"
13931
13754
  },
13932
13755
  {
13933
13756
  "label": "Next",
13757
+ "role": "next",
13934
13758
  "entityTypeId": "feature",
13935
13759
  "description": "High-confidence upcoming work"
13936
13760
  },
13937
13761
  {
13938
13762
  "label": "Later",
13763
+ "role": "later",
13939
13764
  "entityTypeId": "initiative",
13940
13765
  "description": "Exploratory, needs more discovery"
13941
13766
  }
@@ -14451,26 +14276,31 @@ var UPG_FRAMEWORKS = [
14451
14276
  "slots": [
14452
14277
  {
14453
14278
  "label": "Atoms",
14279
+ "role": "atom",
14454
14280
  "entityTypeId": "design_component",
14455
14281
  "description": "Atoms: design component entities for this dimension of the framework"
14456
14282
  },
14457
14283
  {
14458
14284
  "label": "Molecules",
14285
+ "role": "molecule",
14459
14286
  "entityTypeId": "design_component",
14460
14287
  "description": "Molecules: design token entities for this dimension of the framework"
14461
14288
  },
14462
14289
  {
14463
14290
  "label": "Organisms",
14291
+ "role": "organism",
14464
14292
  "entityTypeId": "design_pattern",
14465
14293
  "description": "Organisms: design pattern entities for this dimension of the framework"
14466
14294
  },
14467
14295
  {
14468
14296
  "label": "Templates",
14297
+ "role": "template",
14469
14298
  "entityTypeId": "wireframe",
14470
14299
  "description": "Templates: design system entities for this dimension of the framework"
14471
14300
  },
14472
14301
  {
14473
14302
  "label": "Pages",
14303
+ "role": "page",
14474
14304
  "entityTypeId": "screen",
14475
14305
  "description": "Pages: screen entities for this dimension of the framework"
14476
14306
  }
@@ -15135,26 +14965,31 @@ var UPG_FRAMEWORKS = [
15135
14965
  "slots": [
15136
14966
  {
15137
14967
  "label": "Outer Ring",
14968
+ "role": "outer_ring",
15138
14969
  "entityTypeId": "acquisition_channel",
15139
14970
  "description": "All 19 traction channels brainstormed"
15140
14971
  },
15141
14972
  {
15142
14973
  "label": "Middle Ring",
14974
+ "role": "middle_ring",
15143
14975
  "entityTypeId": "acquisition_channel",
15144
14976
  "description": "Top 6 channels worth testing"
15145
14977
  },
15146
14978
  {
15147
14979
  "label": "Inner Ring",
14980
+ "role": "inner_ring",
15148
14981
  "entityTypeId": "acquisition_channel",
15149
14982
  "description": "Top 3 channels to focus on"
15150
14983
  },
15151
14984
  {
15152
14985
  "label": "Traction Test",
14986
+ "role": "traction_test",
15153
14987
  "entityTypeId": "growth_campaign",
15154
14988
  "description": "Cheap test for each channel"
15155
14989
  },
15156
14990
  {
15157
14991
  "label": "Bullseye Channel",
14992
+ "role": "bullseye_channel",
15158
14993
  "entityTypeId": "acquisition_channel",
15159
14994
  "description": "The single best-performing channel"
15160
14995
  }
@@ -15224,26 +15059,31 @@ var UPG_FRAMEWORKS = [
15224
15059
  "slots": [
15225
15060
  {
15226
15061
  "label": "Free Entry",
15062
+ "role": "free_entry",
15227
15063
  "entityTypeId": "gtm_strategy",
15228
15064
  "description": "How users access the product for free"
15229
15065
  },
15230
15066
  {
15231
15067
  "label": "Aha Moment",
15068
+ "role": "aha_moment",
15232
15069
  "entityTypeId": "gtm_strategy",
15233
15070
  "description": "First experience of core value"
15234
15071
  },
15235
15072
  {
15236
15073
  "label": "Expansion Motion",
15074
+ "role": "expansion_motion",
15237
15075
  "entityTypeId": "sales_motion",
15238
15076
  "description": "How usage spreads within accounts"
15239
15077
  },
15240
15078
  {
15241
15079
  "label": "Monetisation Trigger",
15080
+ "role": "monetisation_trigger",
15242
15081
  "entityTypeId": "gtm_strategy",
15243
15082
  "description": "When and how to convert to paid"
15244
15083
  },
15245
15084
  {
15246
15085
  "label": "Metric",
15086
+ "role": "metric",
15247
15087
  "entityTypeId": "metric",
15248
15088
  "description": "Key metric driving the PLG motion"
15249
15089
  }
@@ -15318,21 +15158,25 @@ var UPG_FRAMEWORKS = [
15318
15158
  "slots": [
15319
15159
  {
15320
15160
  "label": "Objective",
15161
+ "role": "objective",
15321
15162
  "entityTypeId": "objective",
15322
15163
  "description": "Qualitative, inspiring goal"
15323
15164
  },
15324
15165
  {
15325
15166
  "label": "Key Result 1",
15167
+ "role": "key_result",
15326
15168
  "entityTypeId": "key_result",
15327
15169
  "description": "Measurable outcome proving progress"
15328
15170
  },
15329
15171
  {
15330
15172
  "label": "Key Result 2",
15173
+ "role": "key_result",
15331
15174
  "entityTypeId": "key_result",
15332
15175
  "description": "Measurable outcome proving progress"
15333
15176
  },
15334
15177
  {
15335
15178
  "label": "Initiatives",
15179
+ "role": "initiative",
15336
15180
  "entityTypeId": "initiative",
15337
15181
  "description": "Work streams that drive key results"
15338
15182
  }
@@ -15406,26 +15250,31 @@ var UPG_FRAMEWORKS = [
15406
15250
  "slots": [
15407
15251
  {
15408
15252
  "label": "Activity",
15253
+ "role": "activity",
15409
15254
  "entityTypeId": "key_activity",
15410
15255
  "description": "Place persona entities in the Activity position of the matrix"
15411
15256
  },
15412
15257
  {
15413
15258
  "label": "Responsible",
15259
+ "role": "responsible",
15414
15260
  "entityTypeId": "role",
15415
15261
  "description": "Does the work"
15416
15262
  },
15417
15263
  {
15418
15264
  "label": "Accountable",
15265
+ "role": "accountable",
15419
15266
  "entityTypeId": "role",
15420
15267
  "description": "Makes the final call"
15421
15268
  },
15422
15269
  {
15423
15270
  "label": "Consulted",
15271
+ "role": "consulted",
15424
15272
  "entityTypeId": "role",
15425
15273
  "description": "Gives input"
15426
15274
  },
15427
15275
  {
15428
15276
  "label": "Informed",
15277
+ "role": "informed",
15429
15278
  "entityTypeId": "role",
15430
15279
  "description": "Kept in the loop"
15431
15280
  }
@@ -15499,21 +15348,25 @@ var UPG_FRAMEWORKS = [
15499
15348
  "slots": [
15500
15349
  {
15501
15350
  "label": "What Went Well",
15351
+ "role": "went_well",
15502
15352
  "entityTypeId": "outcome",
15503
15353
  "description": "Practices to continue"
15504
15354
  },
15505
15355
  {
15506
15356
  "label": "What Didn't Go Well",
15357
+ "role": "went_poorly",
15507
15358
  "entityTypeId": "need",
15508
15359
  "description": "Issues to address"
15509
15360
  },
15510
15361
  {
15511
15362
  "label": "Action Items",
15363
+ "role": "action_item",
15512
15364
  "entityTypeId": "learning",
15513
15365
  "description": "Changes for next iteration"
15514
15366
  },
15515
15367
  {
15516
15368
  "label": "Learnings",
15369
+ "role": "learning",
15517
15370
  "entityTypeId": "learning",
15518
15371
  "description": "Insights to carry forward"
15519
15372
  }
@@ -15596,53 +15449,346 @@ var UPG_FRAMEWORKS = [
15596
15449
  "description": "A measurable indicator at any level of the tree: north star at the root, driver and input metrics as children"
15597
15450
  },
15598
15451
  {
15599
- "label": "Outcome",
15600
- "entityTypeId": "outcome",
15601
- "description": "The business or product outcome the north-star metric is designed to represent"
15452
+ "label": "Outcome",
15453
+ "entityTypeId": "outcome",
15454
+ "description": "The business or product outcome the north-star metric is designed to represent"
15455
+ },
15456
+ {
15457
+ "label": "Dashboard",
15458
+ "entityTypeId": "dashboard",
15459
+ "description": "Dashboard visualising the metrics tree and showing real-time progress across levels"
15460
+ },
15461
+ {
15462
+ "label": "Report",
15463
+ "entityTypeId": "report",
15464
+ "description": "Periodic report analysing movement across the tree and attributing changes to specific inputs"
15465
+ }
15466
+ ],
15467
+ "data": {
15468
+ "entity_types": [
15469
+ {
15470
+ "type": "metric",
15471
+ "role": "item"
15472
+ },
15473
+ {
15474
+ "type": "outcome",
15475
+ "role": "root"
15476
+ },
15477
+ {
15478
+ "type": "dashboard",
15479
+ "role": "item"
15480
+ },
15481
+ {
15482
+ "type": "report",
15483
+ "role": "item"
15484
+ }
15485
+ ],
15486
+ "required_properties": {}
15487
+ },
15488
+ "structure": {
15489
+ "pattern": "tree"
15490
+ },
15491
+ "presentation": {
15492
+ "layout": {
15493
+ "type": "tree",
15494
+ "direction": "TB",
15495
+ "engine": "dagre"
15496
+ },
15497
+ "colour_by": "type",
15498
+ "collapsible": true,
15499
+ "card_fields": [
15500
+ "title",
15501
+ "description",
15502
+ "status"
15503
+ ]
15504
+ },
15505
+ "education": {
15506
+ "purpose": "Decompose a top-level business metric into its component parts and drivers, creating a tree that shows exactly which levers move the number.",
15507
+ "core_question": "What are the component metrics that drive our north star, and which lever has the most headroom for improvement?",
15508
+ "when_to_use": [
15509
+ "You need to structure your data architecture or analytics practice",
15510
+ "Data quality, governance, or accessibility is a problem",
15511
+ "You want to move from ad-hoc analysis to systematic data practices"
15512
+ ],
15513
+ "when_not_to_use": [
15514
+ "The product generates minimal data that does not warrant formal practices",
15515
+ "You are in very early stage where data infrastructure is premature"
15516
+ ]
15517
+ }
15518
+ },
15519
+ {
15520
+ "id": "team-health-check",
15521
+ "approach_ids": [
15522
+ "inspect"
15523
+ ],
15524
+ "name": "Team Health Check",
15525
+ "version": "1.0.0",
15526
+ "description": "A facilitated team self-assessment across dimensions like mission, fun, learning, speed, and support, using traffic-light voting to surface strengths and improvement areas in a safe format.",
15527
+ "category": "team_process",
15528
+ "origin": {
15529
+ "type": "practitioner",
15530
+ "attribution": "Spotify / Kniberg",
15531
+ "description": "Popularised by Spotify and Henrik Kniberg's Squad Health Check model (2014). Teams self-assess health across dimensions like mission clarity, psychological safety, speed, and learning.",
15532
+ "url": "https://engineering.atspotify.com/2014/09/squad-health-check-model/",
15533
+ "year": 2014,
15534
+ "license": "open_attribution"
15535
+ },
15536
+ "tags": [
15537
+ "team_process",
15538
+ "table"
15539
+ ],
15540
+ "slots": [
15541
+ {
15542
+ "label": "Team",
15543
+ "entityTypeId": "team",
15544
+ "description": "Team conducting the health check; all members vote anonymously on each dimension"
15545
+ },
15546
+ {
15547
+ "label": "Retrospective",
15548
+ "entityTypeId": "retrospective",
15549
+ "description": "Health check session results feeding into the retrospective discussion and action items"
15550
+ },
15551
+ {
15552
+ "label": "Metric",
15553
+ "entityTypeId": "metric",
15554
+ "description": "Health dimension scored, e.g. delivering value, teamwork, fun, learning, mission clarity, speed"
15555
+ },
15556
+ {
15557
+ "label": "Team OKR",
15558
+ "entityTypeId": "team_okr",
15559
+ "description": "Team OKR or improvement goal derived from consistently red health check dimensions"
15560
+ }
15561
+ ],
15562
+ "data": {
15563
+ "entity_types": [
15564
+ {
15565
+ "type": "team",
15566
+ "role": "item"
15567
+ },
15568
+ {
15569
+ "type": "retrospective",
15570
+ "role": "item"
15571
+ },
15572
+ {
15573
+ "type": "metric",
15574
+ "role": "item"
15575
+ },
15576
+ {
15577
+ "type": "team_okr",
15578
+ "role": "item"
15579
+ }
15580
+ ],
15581
+ "required_properties": {}
15582
+ },
15583
+ "structure": {
15584
+ "pattern": "table"
15585
+ },
15586
+ "presentation": {
15587
+ "layout": {
15588
+ "type": "table",
15589
+ "columns": [
15590
+ {
15591
+ "property": "title",
15592
+ "label": "Team",
15593
+ "sortable": true
15594
+ },
15595
+ {
15596
+ "property": "title",
15597
+ "label": "Retrospective",
15598
+ "sortable": true
15599
+ },
15600
+ {
15601
+ "property": "description",
15602
+ "label": "Metric"
15603
+ },
15604
+ {
15605
+ "property": "title",
15606
+ "label": "Team OKR",
15607
+ "sortable": true
15608
+ }
15609
+ ]
15610
+ },
15611
+ "sort_by": {
15612
+ "property": "title",
15613
+ "direction": "asc"
15614
+ },
15615
+ "colour_by": "type",
15616
+ "card_fields": [
15617
+ "title",
15618
+ "description",
15619
+ "status"
15620
+ ]
15621
+ },
15622
+ "education": {
15623
+ "purpose": "Regularly assess team health across dimensions (psychological safety, autonomy, mission clarity, fun, speed, learning) to catch problems early and celebrate strengths.",
15624
+ "core_question": "How is the team really doing: where do they feel strong, where do they feel stuck, and what has changed since last check?",
15625
+ "when_to_use": [
15626
+ "You need to improve team collaboration, clarity, or effectiveness",
15627
+ "Roles and responsibilities are unclear or causing friction",
15628
+ "You want to establish or improve team processes and ceremonies"
15629
+ ],
15630
+ "when_not_to_use": [
15631
+ "The team is small and informal coordination works well",
15632
+ "Process overhead would slow down a team that needs speed"
15633
+ ]
15634
+ }
15635
+ },
15636
+ {
15637
+ "id": "raid-log",
15638
+ "approach_ids": [
15639
+ "inspect"
15640
+ ],
15641
+ "name": "RAID Log",
15642
+ "version": "1.0.0",
15643
+ "description": "A project management register tracking Risks, Assumptions, Issues, and Dependencies, the four categories most likely to derail a project if left unmanaged.",
15644
+ "category": "program_mgmt",
15645
+ "origin": {
15646
+ "type": "practitioner",
15647
+ "description": "Standard project management tool used across methodologies. The RAID log (Risks, Assumptions, Issues, Dependencies) provides a single living document for tracking all factors that could derail a programme.",
15648
+ "license": "cc_by"
15649
+ },
15650
+ "tags": [
15651
+ "program_mgmt",
15652
+ "table"
15653
+ ],
15654
+ "slots": [
15655
+ {
15656
+ "label": "Risk Register",
15657
+ "entityTypeId": "risk_register",
15658
+ "description": "Risk or assumption logged with likelihood, impact, owner, and mitigation strategy"
15659
+ },
15660
+ {
15661
+ "label": "Risk",
15662
+ "entityTypeId": "risk",
15663
+ "description": "Individual risk scored by probability and impact; severity = probability * impact"
15664
+ },
15665
+ {
15666
+ "label": "Dependency",
15667
+ "entityTypeId": "dependency",
15668
+ "description": "Dependency on another team, system, or deliverable that could block progress if not resolved"
15602
15669
  },
15603
15670
  {
15604
- "label": "Dashboard",
15605
- "entityTypeId": "dashboard",
15606
- "description": "Dashboard visualising the metrics tree and showing real-time progress across levels"
15671
+ "label": "Status Report",
15672
+ "entityTypeId": "status_report",
15673
+ "description": "Status update summarising RAID log changes: new risks, resolved issues, and dependency updates"
15607
15674
  },
15608
15675
  {
15609
- "label": "Report",
15610
- "entityTypeId": "report",
15611
- "description": "Periodic report analysing movement across the tree and attributing changes to specific inputs"
15676
+ "label": "Deliverable",
15677
+ "entityTypeId": "deliverable",
15678
+ "description": "Deliverable affected by logged risks, issues, or dependencies, linking RAID items to project scope"
15612
15679
  }
15613
15680
  ],
15614
15681
  "data": {
15615
15682
  "entity_types": [
15616
15683
  {
15617
- "type": "metric",
15684
+ "type": "risk_register",
15618
15685
  "role": "item"
15619
15686
  },
15620
15687
  {
15621
- "type": "outcome",
15622
- "role": "root"
15688
+ "type": "risk",
15689
+ "role": "scored_item"
15623
15690
  },
15624
15691
  {
15625
- "type": "dashboard",
15692
+ "type": "dependency",
15626
15693
  "role": "item"
15627
15694
  },
15628
15695
  {
15629
- "type": "report",
15696
+ "type": "status_report",
15697
+ "role": "item"
15698
+ },
15699
+ {
15700
+ "type": "deliverable",
15630
15701
  "role": "item"
15631
15702
  }
15632
15703
  ],
15633
- "required_properties": {}
15704
+ "required_properties": {
15705
+ "risk": [
15706
+ {
15707
+ "property": "probability",
15708
+ "type": "number",
15709
+ "required": true,
15710
+ "label": "Probability",
15711
+ "description": "Likelihood the risk materialises (risk.probability assessment)"
15712
+ },
15713
+ {
15714
+ "property": "impact",
15715
+ "type": "number",
15716
+ "required": true,
15717
+ "label": "Impact",
15718
+ "description": "Consequence severity if the risk materialises (risk.impact assessment)"
15719
+ }
15720
+ ]
15721
+ },
15722
+ "computed_properties": [
15723
+ {
15724
+ "property": "severity",
15725
+ "expression": "probability * impact",
15726
+ "entity_type": "risk",
15727
+ "label": "Severity",
15728
+ "format": "number"
15729
+ }
15730
+ ],
15731
+ "scoring_method": {
15732
+ "applies_to": [
15733
+ "risk"
15734
+ ],
15735
+ "inputs": [
15736
+ {
15737
+ "property": "probability",
15738
+ "type": "number",
15739
+ "required": true,
15740
+ "label": "Probability",
15741
+ "description": "Likelihood the risk materialises (risk.probability assessment)"
15742
+ },
15743
+ {
15744
+ "property": "impact",
15745
+ "type": "number",
15746
+ "required": true,
15747
+ "label": "Impact",
15748
+ "description": "Consequence severity if the risk materialises (risk.impact assessment)"
15749
+ }
15750
+ ],
15751
+ "computed": [
15752
+ {
15753
+ "property": "severity",
15754
+ "expression": "probability * impact",
15755
+ "label": "Severity",
15756
+ "format": "number"
15757
+ }
15758
+ ]
15759
+ }
15634
15760
  },
15635
15761
  "structure": {
15636
- "pattern": "tree"
15762
+ "pattern": "table"
15637
15763
  },
15638
15764
  "presentation": {
15639
15765
  "layout": {
15640
- "type": "tree",
15641
- "direction": "TB",
15642
- "engine": "dagre"
15766
+ "type": "table",
15767
+ "columns": [
15768
+ {
15769
+ "property": "title",
15770
+ "label": "Item",
15771
+ "sortable": true
15772
+ },
15773
+ {
15774
+ "property": "description",
15775
+ "label": "Category"
15776
+ },
15777
+ {
15778
+ "property": "status",
15779
+ "label": "Status"
15780
+ },
15781
+ {
15782
+ "property": "description",
15783
+ "label": "Owner / Action"
15784
+ }
15785
+ ]
15786
+ },
15787
+ "sort_by": {
15788
+ "property": "title",
15789
+ "direction": "asc"
15643
15790
  },
15644
15791
  "colour_by": "type",
15645
- "collapsible": true,
15646
15792
  "card_fields": [
15647
15793
  "title",
15648
15794
  "description",
@@ -15650,82 +15796,244 @@ var UPG_FRAMEWORKS = [
15650
15796
  ]
15651
15797
  },
15652
15798
  "education": {
15653
- "purpose": "Decompose a top-level business metric into its component parts and drivers, creating a tree that shows exactly which levers move the number.",
15654
- "core_question": "What are the component metrics that drive our north star, and which lever has the most headroom for improvement?",
15799
+ "purpose": "Track Risks, Assumptions, Issues, and Dependencies in a single living document so the project manager sees all potential blockers in one place.",
15800
+ "core_question": "What are the current risks, untested assumptions, open issues, and external dependencies that could derail this programme?",
15655
15801
  "when_to_use": [
15656
- "You need to structure your data architecture or analytics practice",
15657
- "Data quality, governance, or accessibility is a problem",
15658
- "You want to move from ad-hoc analysis to systematic data practices"
15802
+ "You are managing complex, multi-team initiatives with dependencies",
15803
+ "You need to track risks, decisions, and milestones across workstreams",
15804
+ "Stakeholders need visibility into programme-level progress"
15659
15805
  ],
15660
15806
  "when_not_to_use": [
15661
- "The product generates minimal data that does not warrant formal practices",
15662
- "You are in very early stage where data infrastructure is premature"
15807
+ "The project is small enough for a single team to manage",
15808
+ "Formal programme management would create overhead without value"
15663
15809
  ]
15664
15810
  }
15665
15811
  },
15666
15812
  {
15667
- "id": "team-health-check",
15813
+ "id": "ice-scoring",
15668
15814
  "approach_ids": [
15669
- "inspect"
15815
+ "prioritise"
15670
15816
  ],
15671
- "name": "Team Health Check",
15817
+ "name": "ICE Scoring",
15672
15818
  "version": "1.0.0",
15673
- "description": "A facilitated team self-assessment across dimensions like mission, fun, learning, speed, and support, using traffic-light voting to surface strengths and improvement areas in a safe format.",
15674
- "category": "team_process",
15819
+ "description": "Rate ideas by Impact, Confidence, and Ease on a 1-10 scale. Multiply for a composite score. Fast and lightweight.",
15820
+ "category": "prioritization",
15675
15821
  "origin": {
15676
15822
  "type": "practitioner",
15677
- "attribution": "Spotify / Kniberg",
15678
- "description": "Popularised by Spotify and Henrik Kniberg's Squad Health Check model (2014). Teams self-assess health across dimensions like mission clarity, psychological safety, speed, and learning.",
15679
- "url": "https://engineering.atspotify.com/2014/09/squad-health-check-model/",
15680
- "year": 2014,
15823
+ "attribution": "Sean Ellis",
15824
+ "description": "Created by Sean Ellis as a lightweight growth experiment scoring method. Widely adopted in growth teams.",
15825
+ "year": 2010,
15681
15826
  "license": "open_attribution"
15682
15827
  },
15683
15828
  "tags": [
15684
- "team_process",
15829
+ "prioritization",
15685
15830
  "table"
15686
15831
  ],
15687
15832
  "slots": [
15688
15833
  {
15689
- "label": "Team",
15690
- "entityTypeId": "team",
15691
- "description": "Team conducting the health check; all members vote anonymously on each dimension"
15834
+ "label": "Items to score",
15835
+ "role": "candidate",
15836
+ "entityTypeId": "feature",
15837
+ "description": "Features or experiments being evaluated"
15692
15838
  },
15693
15839
  {
15694
- "label": "Retrospective",
15695
- "entityTypeId": "retrospective",
15696
- "description": "Health check session results feeding into the retrospective discussion and action items"
15840
+ "label": "Impact",
15841
+ "role": "impact",
15842
+ "entityTypeId": "outcome",
15843
+ "description": "How much will this move the needle?"
15697
15844
  },
15698
15845
  {
15699
- "label": "Metric",
15700
- "entityTypeId": "metric",
15701
- "description": "Health dimension scored, e.g. delivering value, teamwork, fun, learning, mission clarity, speed"
15846
+ "label": "Confidence",
15847
+ "role": "confidence",
15848
+ "entityTypeId": "assumption",
15849
+ "description": "How sure are we about the impact?"
15850
+ },
15851
+ {
15852
+ "label": "Ease",
15853
+ "role": "ease",
15854
+ "entityTypeId": "feature",
15855
+ "description": "How easy is this to implement?"
15856
+ },
15857
+ {
15858
+ "label": "Opportunities to score",
15859
+ "role": "candidate",
15860
+ "entityTypeId": "opportunity",
15861
+ "description": "Opportunities scored on the same ICE Scoring inputs as features."
15862
+ },
15863
+ {
15864
+ "label": "Needs to score",
15865
+ "role": "candidate",
15866
+ "entityTypeId": "need",
15867
+ "description": "Needs scored on the same ICE Scoring inputs as features."
15868
+ }
15869
+ ],
15870
+ "data": {
15871
+ "entity_types": [
15872
+ {
15873
+ "type": "feature",
15874
+ "role": "scored_item"
15875
+ },
15876
+ {
15877
+ "type": "outcome",
15878
+ "role": "item"
15879
+ },
15880
+ {
15881
+ "type": "assumption",
15882
+ "role": "item"
15883
+ },
15884
+ {
15885
+ "type": "opportunity",
15886
+ "role": "scored_item"
15887
+ },
15888
+ {
15889
+ "type": "need",
15890
+ "role": "scored_item"
15891
+ }
15892
+ ],
15893
+ "required_properties": {
15894
+ "feature": [
15895
+ {
15896
+ "property": "impact",
15897
+ "type": "number",
15898
+ "required": true,
15899
+ "scope": "framework",
15900
+ "label": "Impact",
15901
+ "description": "Expected impact on the target metric (1-10)"
15902
+ },
15903
+ {
15904
+ "property": "confidence",
15905
+ "type": "number",
15906
+ "required": true,
15907
+ "scope": "framework",
15908
+ "label": "Confidence",
15909
+ "description": "Confidence in the impact estimate (1-10)"
15910
+ },
15911
+ {
15912
+ "property": "ease",
15913
+ "type": "number",
15914
+ "required": true,
15915
+ "scope": "framework",
15916
+ "label": "Ease",
15917
+ "description": "Ease of implementation (1-10)"
15918
+ }
15919
+ ],
15920
+ "opportunity": [
15921
+ {
15922
+ "property": "impact",
15923
+ "type": "number",
15924
+ "required": true,
15925
+ "scope": "framework",
15926
+ "label": "Impact",
15927
+ "description": "Expected impact on the target metric (1-10)"
15928
+ },
15929
+ {
15930
+ "property": "confidence",
15931
+ "type": "number",
15932
+ "required": true,
15933
+ "scope": "framework",
15934
+ "label": "Confidence",
15935
+ "description": "Confidence in the impact estimate (1-10)"
15936
+ },
15937
+ {
15938
+ "property": "ease",
15939
+ "type": "number",
15940
+ "required": true,
15941
+ "scope": "framework",
15942
+ "label": "Ease",
15943
+ "description": "Ease of implementation (1-10)"
15944
+ }
15945
+ ],
15946
+ "need": [
15947
+ {
15948
+ "property": "impact",
15949
+ "type": "number",
15950
+ "required": true,
15951
+ "scope": "framework",
15952
+ "label": "Impact",
15953
+ "description": "Expected impact on the target metric (1-10)"
15954
+ },
15955
+ {
15956
+ "property": "confidence",
15957
+ "type": "number",
15958
+ "required": true,
15959
+ "scope": "framework",
15960
+ "label": "Confidence",
15961
+ "description": "Confidence in the impact estimate (1-10)"
15962
+ },
15963
+ {
15964
+ "property": "ease",
15965
+ "type": "number",
15966
+ "required": true,
15967
+ "scope": "framework",
15968
+ "label": "Ease",
15969
+ "description": "Ease of implementation (1-10)"
15970
+ }
15971
+ ]
15702
15972
  },
15703
- {
15704
- "label": "Team OKR",
15705
- "entityTypeId": "team_okr",
15706
- "description": "Team OKR or improvement goal derived from consistently red health check dimensions"
15707
- }
15708
- ],
15709
- "data": {
15710
- "entity_types": [
15711
- {
15712
- "type": "team",
15713
- "role": "item"
15714
- },
15973
+ "computed_properties": [
15715
15974
  {
15716
- "type": "retrospective",
15717
- "role": "item"
15975
+ "property": "ice_score",
15976
+ "expression": "impact * confidence * ease",
15977
+ "entity_type": "feature",
15978
+ "label": "ICE Score",
15979
+ "format": "number"
15718
15980
  },
15719
15981
  {
15720
- "type": "metric",
15721
- "role": "item"
15982
+ "property": "ice_score",
15983
+ "expression": "impact * confidence * ease",
15984
+ "entity_type": "opportunity",
15985
+ "label": "ICE Score",
15986
+ "format": "number"
15722
15987
  },
15723
15988
  {
15724
- "type": "team_okr",
15725
- "role": "item"
15989
+ "property": "ice_score",
15990
+ "expression": "impact * confidence * ease",
15991
+ "entity_type": "need",
15992
+ "label": "ICE Score",
15993
+ "format": "number"
15726
15994
  }
15727
15995
  ],
15728
- "required_properties": {}
15996
+ "scoring_method": {
15997
+ "applies_to": [
15998
+ "feature",
15999
+ "opportunity",
16000
+ "need"
16001
+ ],
16002
+ "inputs": [
16003
+ {
16004
+ "property": "impact",
16005
+ "type": "number",
16006
+ "required": true,
16007
+ "scope": "framework",
16008
+ "label": "Impact",
16009
+ "description": "Expected impact on the target metric (1-10)"
16010
+ },
16011
+ {
16012
+ "property": "confidence",
16013
+ "type": "number",
16014
+ "required": true,
16015
+ "scope": "framework",
16016
+ "label": "Confidence",
16017
+ "description": "Confidence in the impact estimate (1-10)"
16018
+ },
16019
+ {
16020
+ "property": "ease",
16021
+ "type": "number",
16022
+ "required": true,
16023
+ "scope": "framework",
16024
+ "label": "Ease",
16025
+ "description": "Ease of implementation (1-10)"
16026
+ }
16027
+ ],
16028
+ "computed": [
16029
+ {
16030
+ "property": "ice_score",
16031
+ "expression": "impact * confidence * ease",
16032
+ "label": "ICE Score",
16033
+ "format": "number"
16034
+ }
16035
+ ]
16036
+ }
15729
16037
  },
15730
16038
  "structure": {
15731
16039
  "pattern": "table"
@@ -15736,21 +16044,22 @@ var UPG_FRAMEWORKS = [
15736
16044
  "columns": [
15737
16045
  {
15738
16046
  "property": "title",
15739
- "label": "Team",
16047
+ "label": "Items to score",
15740
16048
  "sortable": true
15741
16049
  },
15742
16050
  {
15743
- "property": "title",
15744
- "label": "Retrospective",
16051
+ "property": "impact",
16052
+ "label": "Impact",
15745
16053
  "sortable": true
15746
16054
  },
15747
16055
  {
15748
- "property": "description",
15749
- "label": "Metric"
16056
+ "property": "confidence",
16057
+ "label": "Confidence",
16058
+ "sortable": true
15750
16059
  },
15751
16060
  {
15752
- "property": "title",
15753
- "label": "Team OKR",
16061
+ "property": "ease",
16062
+ "label": "Ease",
15754
16063
  "sortable": true
15755
16064
  }
15756
16065
  ]
@@ -15767,114 +16076,227 @@ var UPG_FRAMEWORKS = [
15767
16076
  ]
15768
16077
  },
15769
16078
  "education": {
15770
- "purpose": "Regularly assess team health across dimensions (psychological safety, autonomy, mission clarity, fun, speed, learning) to catch problems early and celebrate strengths.",
15771
- "core_question": "How is the team really doing: where do they feel strong, where do they feel stuck, and what has changed since last check?",
16079
+ "purpose": "Provide a lightweight scoring model for early-stage ideas when detailed effort estimates are unavailable. Faster than RICE and useful for brainstorm triage.",
16080
+ "core_question": "Which ideas should we investigate further based on their potential impact, confidence in our assumptions, and implementation ease?",
15772
16081
  "when_to_use": [
15773
- "You need to improve team collaboration, clarity, or effectiveness",
15774
- "Roles and responsibilities are unclear or causing friction",
15775
- "You want to establish or improve team processes and ceremonies"
16082
+ "You have more ideas or features than capacity to build them",
16083
+ "Stakeholders disagree on what to build next",
16084
+ "You need a transparent, defensible prioritisation process"
15776
16085
  ],
15777
16086
  "when_not_to_use": [
15778
- "The team is small and informal coordination works well",
15779
- "Process overhead would slow down a team that needs speed"
16087
+ "You have a single obvious next step with no contention",
16088
+ "The backlog is small enough to sequence intuitively"
15780
16089
  ]
15781
16090
  }
15782
16091
  },
15783
16092
  {
15784
- "id": "raid-log",
16093
+ "id": "wsjf",
15785
16094
  "approach_ids": [
15786
- "inspect"
16095
+ "prioritise"
15787
16096
  ],
15788
- "name": "RAID Log",
16097
+ "name": "WSJF (Weighted Shortest Job First)",
15789
16098
  "version": "1.0.0",
15790
- "description": "A project management register tracking Risks, Assumptions, Issues, and Dependencies, the four categories most likely to derail a project if left unmanaged.",
15791
- "category": "program_mgmt",
16099
+ "description": "Prioritise work by dividing Cost of Delay (user value + time criticality + risk reduction) by job duration to maximise economic throughput.",
16100
+ "category": "planning",
15792
16101
  "origin": {
15793
16102
  "type": "practitioner",
15794
- "description": "Standard project management tool used across methodologies. The RAID log (Risks, Assumptions, Issues, Dependencies) provides a single living document for tracking all factors that could derail a programme.",
15795
- "license": "cc_by"
16103
+ "attribution": "Reinertsen / SAFe",
16104
+ "description": "Developed by Don Reinertsen and adopted as a core practice in the Scaled Agile Framework (SAFe). Combines urgency (Cost of Delay) with job size to produce an economic prioritisation sequence.",
16105
+ "url": "https://www.scaledagileframework.com/wsjf/",
16106
+ "year": 2011,
16107
+ "license": "open_attribution"
15796
16108
  },
15797
16109
  "tags": [
15798
- "program_mgmt",
16110
+ "planning",
15799
16111
  "table"
15800
16112
  ],
15801
16113
  "slots": [
15802
16114
  {
15803
- "label": "Risk Register",
15804
- "entityTypeId": "risk_register",
15805
- "description": "Risk or assumption logged with likelihood, impact, owner, and mitigation strategy"
16115
+ "label": "Backlog Items",
16116
+ "role": "candidate",
16117
+ "entityTypeId": "feature",
16118
+ "description": "Backlog Items: feature entries to evaluate"
15806
16119
  },
15807
16120
  {
15808
- "label": "Risk",
15809
- "entityTypeId": "risk",
15810
- "description": "Individual risk scored by probability and impact; severity = probability * impact"
16121
+ "label": "User/Business Value",
16122
+ "role": "user_business_value",
16123
+ "entityTypeId": "metric",
16124
+ "description": "User/Business Value: metric entries to evaluate"
15811
16125
  },
15812
16126
  {
15813
- "label": "Dependency",
15814
- "entityTypeId": "dependency",
15815
- "description": "Dependency on another team, system, or deliverable that could block progress if not resolved"
16127
+ "label": "Time Criticality",
16128
+ "role": "time_criticality",
16129
+ "entityTypeId": "metric",
16130
+ "description": "How much value decays if delivery is delayed (deadlines, competition, seasonal windows)"
15816
16131
  },
15817
16132
  {
15818
- "label": "Status Report",
15819
- "entityTypeId": "status_report",
15820
- "description": "Status update summarising RAID log changes: new risks, resolved issues, and dependency updates"
16133
+ "label": "Risk Reduction / Opportunity Enablement",
16134
+ "role": "risk_reduction",
16135
+ "entityTypeId": "metric",
16136
+ "description": "Risk Reduction / Opportunity Enablement: metric entries to evaluate"
16137
+ },
16138
+ {
16139
+ "label": "Job Size",
16140
+ "role": "job_size",
16141
+ "entityTypeId": "metric",
16142
+ "description": "Estimated effort (story points, t-shirt size, or person-weeks)"
16143
+ },
16144
+ {
16145
+ "label": "Opportunities to score",
16146
+ "role": "candidate",
16147
+ "entityTypeId": "opportunity",
16148
+ "description": "Opportunities scored on the same WSJF (Weighted Shortest Job First) inputs as features."
16149
+ }
16150
+ ],
16151
+ "data": {
16152
+ "entity_types": [
16153
+ {
16154
+ "type": "feature",
16155
+ "role": "scored_item"
16156
+ },
16157
+ {
16158
+ "type": "metric",
16159
+ "role": "item"
16160
+ },
16161
+ {
16162
+ "type": "opportunity",
16163
+ "role": "scored_item"
16164
+ }
16165
+ ],
16166
+ "required_properties": {
16167
+ "feature": [
16168
+ {
16169
+ "property": "user_value",
16170
+ "type": "number",
16171
+ "required": true,
16172
+ "scope": "framework",
16173
+ "label": "User/Business Value",
16174
+ "description": "Relative value to users and the business if delivered"
16175
+ },
16176
+ {
16177
+ "property": "time_criticality",
16178
+ "type": "number",
16179
+ "required": true,
16180
+ "scope": "framework",
16181
+ "label": "Time Criticality",
16182
+ "description": "How much value decays if delivery is delayed (deadlines, competition, seasonal windows)"
16183
+ },
16184
+ {
16185
+ "property": "risk_reduction",
16186
+ "type": "number",
16187
+ "required": true,
16188
+ "scope": "framework",
16189
+ "label": "Risk Reduction / Opportunity Enablement",
16190
+ "description": "Value from reducing risk or enabling future opportunities"
16191
+ },
16192
+ {
16193
+ "property": "job_size",
16194
+ "type": "number",
16195
+ "required": true,
16196
+ "scope": "framework",
16197
+ "label": "Job Size",
16198
+ "description": "Estimated effort (story points, t-shirt size, or person-weeks)"
16199
+ }
16200
+ ],
16201
+ "opportunity": [
16202
+ {
16203
+ "property": "user_value",
16204
+ "type": "number",
16205
+ "required": true,
16206
+ "scope": "framework",
16207
+ "label": "User/Business Value",
16208
+ "description": "Relative value to users and the business if delivered"
16209
+ },
16210
+ {
16211
+ "property": "time_criticality",
16212
+ "type": "number",
16213
+ "required": true,
16214
+ "scope": "framework",
16215
+ "label": "Time Criticality",
16216
+ "description": "How much value decays if delivery is delayed (deadlines, competition, seasonal windows)"
16217
+ },
16218
+ {
16219
+ "property": "risk_reduction",
16220
+ "type": "number",
16221
+ "required": true,
16222
+ "scope": "framework",
16223
+ "label": "Risk Reduction / Opportunity Enablement",
16224
+ "description": "Value from reducing risk or enabling future opportunities"
16225
+ },
16226
+ {
16227
+ "property": "job_size",
16228
+ "type": "number",
16229
+ "required": true,
16230
+ "scope": "framework",
16231
+ "label": "Job Size",
16232
+ "description": "Estimated effort (story points, t-shirt size, or person-weeks)"
16233
+ }
16234
+ ]
15821
16235
  },
15822
- {
15823
- "label": "Deliverable",
15824
- "entityTypeId": "deliverable",
15825
- "description": "Deliverable affected by logged risks, issues, or dependencies, linking RAID items to project scope"
15826
- }
15827
- ],
15828
- "data": {
15829
- "entity_types": [
15830
- {
15831
- "type": "risk_register",
15832
- "role": "item"
15833
- },
15834
- {
15835
- "type": "risk",
15836
- "role": "scored_item"
15837
- },
15838
- {
15839
- "type": "dependency",
15840
- "role": "item"
15841
- },
16236
+ "computed_properties": [
15842
16237
  {
15843
- "type": "status_report",
15844
- "role": "item"
16238
+ "property": "wsjf_score",
16239
+ "expression": "(user_value + time_criticality + risk_reduction) / job_size",
16240
+ "entity_type": "feature",
16241
+ "label": "WSJF Score",
16242
+ "format": "number"
15845
16243
  },
15846
16244
  {
15847
- "type": "deliverable",
15848
- "role": "item"
16245
+ "property": "wsjf_score",
16246
+ "expression": "(user_value + time_criticality + risk_reduction) / job_size",
16247
+ "entity_type": "opportunity",
16248
+ "label": "WSJF Score",
16249
+ "format": "number"
15849
16250
  }
15850
16251
  ],
15851
- "required_properties": {
15852
- "risk": [
16252
+ "scoring_method": {
16253
+ "applies_to": [
16254
+ "feature",
16255
+ "opportunity"
16256
+ ],
16257
+ "inputs": [
15853
16258
  {
15854
- "property": "probability",
16259
+ "property": "user_value",
15855
16260
  "type": "number",
15856
16261
  "required": true,
15857
- "label": "Probability",
15858
- "description": "Likelihood the risk materialises (risk.probability assessment)"
16262
+ "scope": "framework",
16263
+ "label": "User/Business Value",
16264
+ "description": "Relative value to users and the business if delivered"
15859
16265
  },
15860
16266
  {
15861
- "property": "impact",
16267
+ "property": "time_criticality",
15862
16268
  "type": "number",
15863
16269
  "required": true,
15864
- "label": "Impact",
15865
- "description": "Consequence severity if the risk materialises (risk.impact assessment)"
16270
+ "scope": "framework",
16271
+ "label": "Time Criticality",
16272
+ "description": "How much value decays if delivery is delayed (deadlines, competition, seasonal windows)"
16273
+ },
16274
+ {
16275
+ "property": "risk_reduction",
16276
+ "type": "number",
16277
+ "required": true,
16278
+ "scope": "framework",
16279
+ "label": "Risk Reduction / Opportunity Enablement",
16280
+ "description": "Value from reducing risk or enabling future opportunities"
16281
+ },
16282
+ {
16283
+ "property": "job_size",
16284
+ "type": "number",
16285
+ "required": true,
16286
+ "scope": "framework",
16287
+ "label": "Job Size",
16288
+ "description": "Estimated effort (story points, t-shirt size, or person-weeks)"
16289
+ }
16290
+ ],
16291
+ "computed": [
16292
+ {
16293
+ "property": "wsjf_score",
16294
+ "expression": "(user_value + time_criticality + risk_reduction) / job_size",
16295
+ "label": "WSJF Score",
16296
+ "format": "number"
15866
16297
  }
15867
16298
  ]
15868
- },
15869
- "computed_properties": [
15870
- {
15871
- "property": "severity",
15872
- "expression": "probability * impact",
15873
- "entity_type": "risk",
15874
- "label": "Severity",
15875
- "format": "number"
15876
- }
15877
- ]
16299
+ }
15878
16300
  },
15879
16301
  "structure": {
15880
16302
  "pattern": "table"
@@ -15885,20 +16307,33 @@ var UPG_FRAMEWORKS = [
15885
16307
  "columns": [
15886
16308
  {
15887
16309
  "property": "title",
15888
- "label": "Item",
16310
+ "label": "Backlog Items",
15889
16311
  "sortable": true
15890
16312
  },
15891
16313
  {
15892
- "property": "description",
15893
- "label": "Category"
16314
+ "property": "user_value",
16315
+ "label": "User/Business Value",
16316
+ "sortable": true
15894
16317
  },
15895
16318
  {
15896
- "property": "status",
15897
- "label": "Status"
16319
+ "property": "time_criticality",
16320
+ "label": "Time Criticality",
16321
+ "sortable": true
15898
16322
  },
15899
16323
  {
15900
- "property": "description",
15901
- "label": "Owner / Action"
16324
+ "property": "risk_reduction",
16325
+ "label": "Risk Reduction / Opportunity Enablement",
16326
+ "sortable": true
16327
+ },
16328
+ {
16329
+ "property": "job_size",
16330
+ "label": "Job Size",
16331
+ "sortable": true
16332
+ },
16333
+ {
16334
+ "property": "wsjf_score",
16335
+ "label": "WSJF Score",
16336
+ "sortable": true
15902
16337
  }
15903
16338
  ]
15904
16339
  },
@@ -15914,34 +16349,34 @@ var UPG_FRAMEWORKS = [
15914
16349
  ]
15915
16350
  },
15916
16351
  "education": {
15917
- "purpose": "Track Risks, Assumptions, Issues, and Dependencies in a single living document so the project manager sees all potential blockers in one place.",
15918
- "core_question": "What are the current risks, untested assumptions, open issues, and external dependencies that could derail this programme?",
16352
+ "purpose": "Prioritise work by dividing the Cost of Delay by job duration, ensuring the most time-sensitive, valuable items are done first.",
16353
+ "core_question": "Considering the cost of waiting, which items should we start now to maximise economic benefit?",
15919
16354
  "when_to_use": [
15920
- "You are managing complex, multi-team initiatives with dependencies",
15921
- "You need to track risks, decisions, and milestones across workstreams",
15922
- "Stakeholders need visibility into programme-level progress"
16355
+ "You need to coordinate work across multiple teams or time horizons",
16356
+ "Stakeholders need visibility into what is coming and when",
16357
+ "You want to balance commitments with flexibility"
15923
16358
  ],
15924
16359
  "when_not_to_use": [
15925
- "The project is small enough for a single team to manage",
15926
- "Formal programme management would create overhead without value"
16360
+ "The team is small enough that informal coordination works",
16361
+ "Plans would create false precision about uncertain outcomes"
15927
16362
  ]
15928
16363
  }
15929
16364
  },
15930
16365
  {
15931
- "id": "ice-scoring",
16366
+ "id": "cost-of-delay",
15932
16367
  "approach_ids": [
15933
16368
  "prioritise"
15934
16369
  ],
15935
- "name": "ICE Scoring",
16370
+ "name": "Cost of Delay",
15936
16371
  "version": "1.0.0",
15937
- "description": "Rate ideas by Impact, Confidence, and Ease on a 1-10 scale. Multiply for a composite score. Fast and lightweight.",
16372
+ "description": "Quantify the economic cost of not shipping a feature or opportunity to drive priority decisions. Combines urgency with value.",
15938
16373
  "category": "prioritization",
15939
16374
  "origin": {
15940
16375
  "type": "practitioner",
15941
- "attribution": "Sean Ellis",
15942
- "description": "Created by Sean Ellis as a lightweight growth experiment scoring method. Widely adopted in growth teams.",
15943
- "year": 2010,
15944
- "license": "open_attribution"
16376
+ "attribution": "Don Reinertsen",
16377
+ "description": "Formalised in The Principles of Product Development Flow (Celeritas Publishing). Foundational to lean product economics.",
16378
+ "year": 2009,
16379
+ "license": "public_domain"
15945
16380
  },
15946
16381
  "tags": [
15947
16382
  "prioritization",
@@ -15949,34 +16384,29 @@ var UPG_FRAMEWORKS = [
15949
16384
  ],
15950
16385
  "slots": [
15951
16386
  {
15952
- "label": "Items to score",
16387
+ "label": "Items to evaluate",
15953
16388
  "entityTypeId": "feature",
15954
- "description": "Features or experiments being evaluated"
16389
+ "description": "Features or initiatives being assessed"
15955
16390
  },
15956
16391
  {
15957
- "label": "Impact",
16392
+ "label": "User-Business Value",
15958
16393
  "entityTypeId": "outcome",
15959
- "description": "How much will this move the needle?"
16394
+ "description": "Revenue, retention, or strategic value"
15960
16395
  },
15961
16396
  {
15962
- "label": "Confidence",
15963
- "entityTypeId": "assumption",
15964
- "description": "How sure are we about the impact?"
16397
+ "label": "Time Criticality",
16398
+ "entityTypeId": "metric",
16399
+ "description": "How much value decays with delay"
15965
16400
  },
15966
16401
  {
15967
- "label": "Ease",
15968
- "entityTypeId": "feature",
15969
- "description": "How easy is this to implement?"
16402
+ "label": "Risk Reduction",
16403
+ "entityTypeId": "risk",
16404
+ "description": "What risk does this mitigate?"
15970
16405
  },
15971
16406
  {
15972
16407
  "label": "Opportunities to score",
15973
16408
  "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."
16409
+ "description": "Opportunities scored on the same Cost of Delay inputs as features."
15980
16410
  }
15981
16411
  ],
15982
16412
  "data": {
@@ -15986,743 +16416,567 @@ var UPG_FRAMEWORKS = [
15986
16416
  "role": "scored_item"
15987
16417
  },
15988
16418
  {
15989
- "type": "outcome",
16419
+ "type": "metric",
15990
16420
  "role": "item"
15991
16421
  },
15992
16422
  {
15993
- "type": "assumption",
16423
+ "type": "outcome",
15994
16424
  "role": "item"
15995
16425
  },
15996
16426
  {
15997
- "type": "opportunity",
15998
- "role": "scored_item"
16427
+ "type": "risk",
16428
+ "role": "item"
15999
16429
  },
16000
16430
  {
16001
- "type": "need",
16431
+ "type": "opportunity",
16002
16432
  "role": "scored_item"
16003
16433
  }
16004
16434
  ],
16005
16435
  "required_properties": {
16006
16436
  "feature": [
16007
16437
  {
16008
- "property": "impact",
16009
- "type": "number",
16010
- "required": true,
16011
- "scope": "framework",
16012
- "label": "Impact",
16013
- "description": "Expected impact on the target metric (1-10)"
16014
- },
16015
- {
16016
- "property": "confidence",
16017
- "type": "number",
16018
- "required": true,
16019
- "scope": "framework",
16020
- "label": "Confidence",
16021
- "description": "Confidence in the impact estimate (1-10)"
16022
- },
16023
- {
16024
- "property": "ease",
16025
- "type": "number",
16026
- "required": true,
16027
- "scope": "framework",
16028
- "label": "Ease",
16029
- "description": "Ease of implementation (1-10)"
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",
16438
+ "property": "cost_of_delay",
16043
16439
  "type": "number",
16044
16440
  "required": true,
16045
16441
  "scope": "framework",
16046
- "label": "Confidence",
16047
- "description": "Confidence in the impact estimate (1-10)"
16442
+ "label": "Cost of Delay",
16443
+ "description": "Weekly revenue impact of not shipping"
16048
16444
  },
16049
16445
  {
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",
16446
+ "property": "job_size",
16061
16447
  "type": "number",
16062
16448
  "required": true,
16063
16449
  "scope": "framework",
16064
- "label": "Impact",
16065
- "description": "Expected impact on the target metric (1-10)"
16066
- },
16450
+ "label": "Job Size",
16451
+ "description": "Weeks of development effort"
16452
+ }
16453
+ ],
16454
+ "opportunity": [
16067
16455
  {
16068
- "property": "confidence",
16456
+ "property": "cost_of_delay",
16069
16457
  "type": "number",
16070
16458
  "required": true,
16071
16459
  "scope": "framework",
16072
- "label": "Confidence",
16073
- "description": "Confidence in the impact estimate (1-10)"
16460
+ "label": "Cost of Delay",
16461
+ "description": "Weekly revenue impact of not shipping"
16074
16462
  },
16075
16463
  {
16076
- "property": "ease",
16464
+ "property": "job_size",
16077
16465
  "type": "number",
16078
16466
  "required": true,
16079
16467
  "scope": "framework",
16080
- "label": "Ease",
16081
- "description": "Ease of implementation (1-10)"
16468
+ "label": "Job Size",
16469
+ "description": "Weeks of development effort"
16082
16470
  }
16083
16471
  ]
16084
16472
  },
16085
16473
  "computed_properties": [
16086
16474
  {
16087
- "property": "ice_score",
16088
- "expression": "impact * confidence * ease",
16475
+ "property": "wsjf_score",
16476
+ "expression": "cost_of_delay / job_size",
16089
16477
  "entity_type": "feature",
16090
- "label": "ICE Score",
16478
+ "label": "WSJF Score",
16091
16479
  "format": "number"
16092
16480
  },
16093
16481
  {
16094
- "property": "ice_score",
16095
- "expression": "impact * confidence * ease",
16482
+ "property": "wsjf_score",
16483
+ "expression": "cost_of_delay / job_size",
16096
16484
  "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",
16485
+ "label": "WSJF Score",
16105
16486
  "format": "number"
16106
16487
  }
16107
16488
  ],
16108
- "scoring_lens": {
16489
+ "scoring_method": {
16109
16490
  "applies_to": [
16110
16491
  "feature",
16111
- "opportunity",
16112
- "need"
16492
+ "opportunity"
16113
16493
  ],
16114
16494
  "inputs": [
16115
16495
  {
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",
16496
+ "property": "cost_of_delay",
16125
16497
  "type": "number",
16126
16498
  "required": true,
16127
16499
  "scope": "framework",
16128
- "label": "Confidence",
16129
- "description": "Confidence in the impact estimate (1-10)"
16500
+ "label": "Cost of Delay",
16501
+ "description": "Weekly revenue impact of not shipping"
16130
16502
  },
16131
16503
  {
16132
- "property": "ease",
16504
+ "property": "job_size",
16133
16505
  "type": "number",
16134
16506
  "required": true,
16135
16507
  "scope": "framework",
16136
- "label": "Ease",
16137
- "description": "Ease of implementation (1-10)"
16508
+ "label": "Job Size",
16509
+ "description": "Weeks of development effort"
16138
16510
  }
16139
16511
  ],
16140
16512
  "computed": [
16141
16513
  {
16142
- "property": "ice_score",
16143
- "expression": "impact * confidence * ease",
16144
- "label": "ICE Score",
16514
+ "property": "wsjf_score",
16515
+ "expression": "cost_of_delay / job_size",
16516
+ "label": "WSJF Score",
16145
16517
  "format": "number"
16146
16518
  }
16147
16519
  ]
16148
16520
  }
16149
16521
  },
16150
16522
  "structure": {
16151
- "pattern": "table"
16523
+ "pattern": "table"
16524
+ },
16525
+ "presentation": {
16526
+ "layout": {
16527
+ "type": "table",
16528
+ "columns": [
16529
+ {
16530
+ "property": "title",
16531
+ "label": "Items to evaluate",
16532
+ "sortable": true
16533
+ },
16534
+ {
16535
+ "property": "cost_of_delay",
16536
+ "label": "User-Business Value",
16537
+ "sortable": true
16538
+ },
16539
+ {
16540
+ "property": "job_size",
16541
+ "label": "Job Size",
16542
+ "sortable": true
16543
+ },
16544
+ {
16545
+ "property": "wsjf_score",
16546
+ "label": "CoD Score",
16547
+ "sortable": true
16548
+ }
16549
+ ]
16550
+ },
16551
+ "sort_by": {
16552
+ "property": "title",
16553
+ "direction": "asc"
16554
+ },
16555
+ "colour_by": "type",
16556
+ "card_fields": [
16557
+ "title",
16558
+ "description",
16559
+ "status"
16560
+ ]
16561
+ },
16562
+ "education": {
16563
+ "purpose": "Quantify the economic impact of not delivering a feature by a given date, making urgency visible and enabling time-sensitive prioritisation.",
16564
+ "core_question": "How much value are we losing every week this feature is not in production, and does that urgency justify fast-tracking it?",
16565
+ "when_to_use": [
16566
+ "You have more ideas or features than capacity to build them",
16567
+ "Stakeholders disagree on what to build next",
16568
+ "You need a transparent, defensible prioritisation process"
16569
+ ],
16570
+ "when_not_to_use": [
16571
+ "You have a single obvious next step with no contention",
16572
+ "The backlog is small enough to sequence intuitively"
16573
+ ]
16574
+ }
16575
+ },
16576
+ {
16577
+ "id": "five-whys",
16578
+ "approach_ids": [
16579
+ "reflect",
16580
+ "inspect"
16581
+ ],
16582
+ "name": "Five Whys",
16583
+ "version": "1.0.0",
16584
+ "description": 'Iteratively ask "why?", typically five times, starting from a symptom; each answer becomes the subject of the next question. The chain of answers reveals the underlying root cause behind the surface problem.',
16585
+ "category": "team_process",
16586
+ "origin": {
16587
+ "type": "practitioner",
16588
+ "attribution": "Sakichi Toyoda / Toyota Production System",
16589
+ "description": "Developed within the Toyota Production System as a root-cause analysis technique. Popularised through Lean and Six Sigma practice; now a widely used incident-review and design-debug staple.",
16590
+ "year": 1930,
16591
+ "license": "public_domain"
16592
+ },
16593
+ "tags": [
16594
+ "team_process",
16595
+ "reflection",
16596
+ "root_cause",
16597
+ "tree"
16598
+ ],
16599
+ "slots": [
16600
+ {
16601
+ "label": "Symptom",
16602
+ "role": "symptom",
16603
+ "entityTypeId": "need",
16604
+ "description": "The observed problem the analysis starts from."
16605
+ },
16606
+ {
16607
+ "label": "Why chain",
16608
+ "role": "why",
16609
+ "entityTypeId": "insight",
16610
+ "description": 'Each "why?" answer along the chain, typically five iterations deep.'
16611
+ },
16612
+ {
16613
+ "label": "Root cause",
16614
+ "role": "root_cause",
16615
+ "entityTypeId": "insight",
16616
+ "description": "The terminal answer at the bottom of the chain: the underlying cause to address."
16617
+ }
16618
+ ],
16619
+ "data": {
16620
+ "entity_types": [
16621
+ {
16622
+ "type": "need",
16623
+ "role": "root"
16624
+ },
16625
+ {
16626
+ "type": "insight",
16627
+ "role": "branch"
16628
+ }
16629
+ ],
16630
+ "required_properties": {}
16631
+ },
16632
+ "structure": {
16633
+ "pattern": "tree"
16634
+ },
16635
+ "presentation": {
16636
+ "layout": {
16637
+ "type": "tree",
16638
+ "direction": "TB"
16639
+ },
16640
+ "sort_by": {
16641
+ "property": "title",
16642
+ "direction": "asc"
16643
+ },
16644
+ "card_fields": [
16645
+ "title",
16646
+ "description"
16647
+ ]
16648
+ },
16649
+ "education": {
16650
+ "purpose": 'Move past surface symptoms by chaining "why?" questions until the underlying root cause surfaces, so fixes target the real driver rather than a downstream effect.',
16651
+ "core_question": "Why is this happening, and why is THAT happening, until we reach a cause we can act on?",
16652
+ "when_to_use": [
16653
+ "A problem keeps recurring after surface fixes",
16654
+ "Post-incident review where the obvious cause feels too obvious",
16655
+ "Designing a fix and you want to confirm you understand the actual driver"
16656
+ ],
16657
+ "when_not_to_use": [
16658
+ "The problem has multiple independent root causes (use a fishbone or richer RCA tool)",
16659
+ "You need quantitative attribution rather than a single-thread narrative",
16660
+ 'Five linear "whys" oversimplify a systems problem with feedback loops'
16661
+ ]
16662
+ }
16663
+ },
16664
+ {
16665
+ "id": "pre-mortem",
16666
+ "approach_ids": [
16667
+ "reflect"
16668
+ ],
16669
+ "name": "Pre-mortem",
16670
+ "version": "1.0.0",
16671
+ "description": "Imagine the project has already failed; work backward listing the plausible causes of the failure. Produce a risk register and matching mitigations before the work starts.",
16672
+ "category": "team_process",
16673
+ "origin": {
16674
+ "type": "practitioner",
16675
+ "attribution": "Gary Klein",
16676
+ "description": "Popularised by Gary Klein in Harvard Business Review (2007) as a prospective-hindsight technique. Inverts the post-mortem: imagine failure first, then list causes, while there is still time to act.",
16677
+ "url": "https://hbr.org/2007/09/performing-a-project-premortem",
16678
+ "year": 2007,
16679
+ "license": "published_methodology"
16680
+ },
16681
+ "tags": [
16682
+ "team_process",
16683
+ "reflection",
16684
+ "risk",
16685
+ "collection"
16686
+ ],
16687
+ "slots": [
16688
+ {
16689
+ "label": "Imagined failures",
16690
+ "entityTypeId": "risk",
16691
+ "description": "Plausible failure modes named as if they had already occurred."
16692
+ },
16693
+ {
16694
+ "label": "Causes",
16695
+ "entityTypeId": "insight",
16696
+ "description": "For each imagined failure, the contributing causes the team can foresee."
16697
+ },
16698
+ {
16699
+ "label": "Mitigations",
16700
+ "entityTypeId": "initiative",
16701
+ "description": "Mitigation actions the team will take before failure can occur."
16702
+ }
16703
+ ],
16704
+ "data": {
16705
+ "entity_types": [
16706
+ {
16707
+ "type": "risk",
16708
+ "role": "bucket"
16709
+ },
16710
+ {
16711
+ "type": "insight",
16712
+ "role": "bucket"
16713
+ },
16714
+ {
16715
+ "type": "initiative",
16716
+ "role": "bucket"
16717
+ }
16718
+ ],
16719
+ "required_properties": {}
16720
+ },
16721
+ "structure": {
16722
+ "pattern": "collection"
16152
16723
  },
16153
16724
  "presentation": {
16154
16725
  "layout": {
16155
- "type": "table",
16156
- "columns": [
16157
- {
16158
- "property": "title",
16159
- "label": "Items to score",
16160
- "sortable": true
16161
- },
16162
- {
16163
- "property": "impact",
16164
- "label": "Impact",
16165
- "sortable": true
16166
- },
16167
- {
16168
- "property": "confidence",
16169
- "label": "Confidence",
16170
- "sortable": true
16171
- },
16172
- {
16173
- "property": "ease",
16174
- "label": "Ease",
16175
- "sortable": true
16176
- }
16177
- ]
16726
+ "type": "grid",
16727
+ "groupBy": "type"
16178
16728
  },
16179
16729
  "sort_by": {
16180
16730
  "property": "title",
16181
16731
  "direction": "asc"
16182
16732
  },
16183
- "colour_by": "type",
16733
+ "colour_by": "group",
16184
16734
  "card_fields": [
16185
16735
  "title",
16186
- "description",
16187
- "status"
16736
+ "description"
16188
16737
  ]
16189
16738
  },
16190
16739
  "education": {
16191
- "purpose": "Provide a lightweight scoring model for early-stage ideas when detailed effort estimates are unavailable. Faster than RICE and useful for brainstorm triage.",
16192
- "core_question": "Which ideas should we investigate further based on their potential impact, confidence in our assumptions, and implementation ease?",
16740
+ "purpose": "Surface project risks early by inverting hindsight: imagine the project has already failed and ask why, while there is still time to mitigate.",
16741
+ "core_question": "It is six months from now and the project has failed catastrophically. What happened, and why?",
16193
16742
  "when_to_use": [
16194
- "You have more ideas or features than capacity to build them",
16195
- "Stakeholders disagree on what to build next",
16196
- "You need a transparent, defensible prioritisation process"
16743
+ "Kicking off a project with significant downside or irreversible commitment",
16744
+ "A plan looks too clean and the team senses unspoken concerns",
16745
+ "Stakeholders disagree on risk; the exercise externalises and ranks them"
16197
16746
  ],
16198
16747
  "when_not_to_use": [
16199
- "You have a single obvious next step with no contention",
16200
- "The backlog is small enough to sequence intuitively"
16748
+ "The work is small, reversible, and cheap to course-correct",
16749
+ "The team is in execution mode and reflective ceremonies will derail momentum",
16750
+ "Risk surfacing has become performative: the team names risks but never mitigates them"
16201
16751
  ]
16202
16752
  }
16203
16753
  },
16204
16754
  {
16205
- "id": "wsjf",
16755
+ "id": "red-team",
16206
16756
  "approach_ids": [
16207
- "prioritise"
16757
+ "reflect",
16758
+ "inspect"
16208
16759
  ],
16209
- "name": "WSJF (Weighted Shortest Job First)",
16760
+ "name": "Red Team",
16210
16761
  "version": "1.0.0",
16211
- "description": "Prioritise work by dividing Cost of Delay (user value + time criticality + risk reduction) by job duration to maximise economic throughput.",
16212
- "category": "planning",
16762
+ "description": "Structured adversarial review. A designated group is assigned to attack a plan, design, or proposal from an outside-in stance, surfacing weaknesses the inside-out builders cannot see.",
16763
+ "category": "team_process",
16213
16764
  "origin": {
16214
16765
  "type": "practitioner",
16215
- "attribution": "Reinertsen / SAFe",
16216
- "description": "Developed by Don Reinertsen and adopted as a core practice in the Scaled Agile Framework (SAFe). Combines urgency (Cost of Delay) with job size to produce an economic prioritisation sequence.",
16217
- "url": "https://www.scaledagileframework.com/wsjf/",
16218
- "year": 2011,
16219
- "license": "open_attribution"
16766
+ "attribution": "US Department of Defense (Cold War era); broadened by security and intelligence practice",
16767
+ "description": `Originated in Cold War-era military strategic exercises ("red" team takes the adversary role against the "blue" team's defence). Adopted by cybersecurity, intelligence analysis, and product teams as a structured contrarian-review practice.`,
16768
+ "year": 1960,
16769
+ "license": "public_domain"
16220
16770
  },
16221
16771
  "tags": [
16222
- "planning",
16223
- "table"
16772
+ "team_process",
16773
+ "reflection",
16774
+ "adversarial",
16775
+ "collection"
16224
16776
  ],
16225
16777
  "slots": [
16226
16778
  {
16227
- "label": "Backlog Items",
16228
- "entityTypeId": "feature",
16229
- "description": "Backlog Items: feature entries to evaluate"
16230
- },
16231
- {
16232
- "label": "User/Business Value",
16233
- "entityTypeId": "metric",
16234
- "description": "User/Business Value: metric entries to evaluate"
16235
- },
16236
- {
16237
- "label": "Time Criticality",
16238
- "entityTypeId": "metric",
16239
- "description": "How much value decays if delivery is delayed (deadlines, competition, seasonal windows)"
16240
- },
16241
- {
16242
- "label": "Risk Reduction / Opportunity Enablement",
16243
- "entityTypeId": "metric",
16244
- "description": "Risk Reduction / Opportunity Enablement: metric entries to evaluate"
16779
+ "label": "Target",
16780
+ "entityTypeId": "initiative",
16781
+ "description": "The plan, design, or proposal under adversarial review."
16245
16782
  },
16246
16783
  {
16247
- "label": "Job Size",
16248
- "entityTypeId": "metric",
16249
- "description": "Estimated effort (story points, t-shirt size, or person-weeks)"
16784
+ "label": "Attack vectors",
16785
+ "entityTypeId": "risk",
16786
+ "description": "The angles the red team uses to probe weaknesses."
16250
16787
  },
16251
16788
  {
16252
- "label": "Opportunities to score",
16253
- "entityTypeId": "opportunity",
16254
- "description": "Opportunities scored on the same WSJF (Weighted Shortest Job First) inputs as features."
16789
+ "label": "Findings",
16790
+ "entityTypeId": "insight",
16791
+ "description": "Weaknesses, blind spots, or unstated assumptions surfaced by the review."
16255
16792
  }
16256
16793
  ],
16257
16794
  "data": {
16258
16795
  "entity_types": [
16259
- {
16260
- "type": "feature",
16261
- "role": "scored_item"
16262
- },
16263
- {
16264
- "type": "metric",
16265
- "role": "item"
16266
- },
16267
- {
16268
- "type": "opportunity",
16269
- "role": "scored_item"
16270
- }
16271
- ],
16272
- "required_properties": {
16273
- "feature": [
16274
- {
16275
- "property": "user_value",
16276
- "type": "number",
16277
- "required": true,
16278
- "scope": "framework",
16279
- "label": "User/Business Value",
16280
- "description": "Relative value to users and the business if delivered"
16281
- },
16282
- {
16283
- "property": "time_criticality",
16284
- "type": "number",
16285
- "required": true,
16286
- "scope": "framework",
16287
- "label": "Time Criticality",
16288
- "description": "How much value decays if delivery is delayed (deadlines, competition, seasonal windows)"
16289
- },
16290
- {
16291
- "property": "risk_reduction",
16292
- "type": "number",
16293
- "required": true,
16294
- "scope": "framework",
16295
- "label": "Risk Reduction / Opportunity Enablement",
16296
- "description": "Value from reducing risk or enabling future opportunities"
16297
- },
16298
- {
16299
- "property": "job_size",
16300
- "type": "number",
16301
- "required": true,
16302
- "scope": "framework",
16303
- "label": "Job Size",
16304
- "description": "Estimated effort (story points, t-shirt size, or person-weeks)"
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
- }
16340
- ]
16341
- },
16342
- "computed_properties": [
16343
- {
16344
- "property": "wsjf_score",
16345
- "expression": "(user_value + time_criticality + risk_reduction) / job_size",
16346
- "entity_type": "feature",
16347
- "label": "WSJF Score",
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"
16356
- }
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
- }
16796
+ {
16797
+ "type": "initiative",
16798
+ "role": "root"
16799
+ },
16800
+ {
16801
+ "type": "risk",
16802
+ "role": "bucket"
16803
+ },
16804
+ {
16805
+ "type": "insight",
16806
+ "role": "bucket"
16807
+ }
16808
+ ],
16809
+ "required_properties": {}
16406
16810
  },
16407
16811
  "structure": {
16408
- "pattern": "table"
16812
+ "pattern": "collection"
16409
16813
  },
16410
16814
  "presentation": {
16411
16815
  "layout": {
16412
- "type": "table",
16413
- "columns": [
16414
- {
16415
- "property": "title",
16416
- "label": "Backlog Items",
16417
- "sortable": true
16418
- },
16419
- {
16420
- "property": "user_value",
16421
- "label": "User/Business Value",
16422
- "sortable": true
16423
- },
16424
- {
16425
- "property": "time_criticality",
16426
- "label": "Time Criticality",
16427
- "sortable": true
16428
- },
16429
- {
16430
- "property": "risk_reduction",
16431
- "label": "Risk Reduction / Opportunity Enablement",
16432
- "sortable": true
16433
- },
16434
- {
16435
- "property": "job_size",
16436
- "label": "Job Size",
16437
- "sortable": true
16438
- },
16439
- {
16440
- "property": "wsjf_score",
16441
- "label": "WSJF Score",
16442
- "sortable": true
16443
- }
16444
- ]
16816
+ "type": "grid",
16817
+ "groupBy": "type"
16445
16818
  },
16446
16819
  "sort_by": {
16447
16820
  "property": "title",
16448
16821
  "direction": "asc"
16449
16822
  },
16450
- "colour_by": "type",
16823
+ "colour_by": "group",
16451
16824
  "card_fields": [
16452
16825
  "title",
16453
- "description",
16454
- "status"
16826
+ "description"
16455
16827
  ]
16456
16828
  },
16457
16829
  "education": {
16458
- "purpose": "Prioritise work by dividing the Cost of Delay by job duration, ensuring the most time-sensitive, valuable items are done first.",
16459
- "core_question": "Considering the cost of waiting, which items should we start now to maximise economic benefit?",
16830
+ "purpose": "Stress-test a plan against an explicit adversary by assigning reviewers to attack rather than agree, so weaknesses surface before reality finds them.",
16831
+ "core_question": "If a competent adversary wanted this to fail, where would they push first, and would we hold?",
16460
16832
  "when_to_use": [
16461
- "You need to coordinate work across multiple teams or time horizons",
16462
- "Stakeholders need visibility into what is coming and when",
16463
- "You want to balance commitments with flexibility"
16833
+ "A high-stakes decision, launch, or security posture needs hardening",
16834
+ "Inside-out thinking is dominant and dissent has gone quiet",
16835
+ "Risk register is suspiciously short for the size of the bet"
16464
16836
  ],
16465
16837
  "when_not_to_use": [
16466
- "The team is small enough that informal coordination works",
16467
- "Plans would create false precision about uncertain outcomes"
16838
+ "Early-stage exploration where adversarial framing would crush a fragile idea prematurely",
16839
+ "Team trust is too low: red-teaming will read as personal attack rather than role-play",
16840
+ "The work is small enough that a lightweight devil's-advocate pass is sufficient"
16468
16841
  ]
16469
16842
  }
16470
16843
  },
16471
16844
  {
16472
- "id": "cost-of-delay",
16845
+ "id": "devils-advocate",
16473
16846
  "approach_ids": [
16474
- "prioritise"
16847
+ "reflect"
16475
16848
  ],
16476
- "name": "Cost of Delay",
16849
+ "name": "Devil's Advocate",
16477
16850
  "version": "1.0.0",
16478
- "description": "Quantify the economic cost of not shipping a feature or opportunity to drive priority decisions. Combines urgency with value.",
16479
- "category": "prioritization",
16851
+ "description": "Designate one reviewer to formally take the opposing position regardless of personal view. The assigned-role contrarian defangs groupthink by making dissent legitimate and structured.",
16852
+ "category": "team_process",
16480
16853
  "origin": {
16481
16854
  "type": "practitioner",
16482
- "attribution": "Don Reinertsen",
16483
- "description": "Formalised in The Principles of Product Development Flow (Celeritas Publishing). Foundational to lean product economics.",
16484
- "year": 2009,
16855
+ "attribution": "Roman Catholic Church (advocatus diaboli); broadened by decision-quality practice",
16856
+ "description": "Originated in 16th-century canonisation proceedings as the advocatus diaboli, an official assigned to argue against canonising a candidate. Adopted by decision-science and product-team practice as a structured antidote to groupthink.",
16857
+ "year": 1587,
16485
16858
  "license": "public_domain"
16486
16859
  },
16487
16860
  "tags": [
16488
- "prioritization",
16489
- "table"
16861
+ "team_process",
16862
+ "reflection",
16863
+ "decision_quality",
16864
+ "collection"
16490
16865
  ],
16491
16866
  "slots": [
16492
16867
  {
16493
- "label": "Items to evaluate",
16494
- "entityTypeId": "feature",
16495
- "description": "Features or initiatives being assessed"
16496
- },
16497
- {
16498
- "label": "User-Business Value",
16499
- "entityTypeId": "outcome",
16500
- "description": "Revenue, retention, or strategic value"
16501
- },
16502
- {
16503
- "label": "Time Criticality",
16504
- "entityTypeId": "metric",
16505
- "description": "How much value decays with delay"
16868
+ "label": "Proposal",
16869
+ "entityTypeId": "initiative",
16870
+ "description": "The plan or recommendation under consideration."
16506
16871
  },
16507
16872
  {
16508
- "label": "Risk Reduction",
16509
- "entityTypeId": "risk",
16510
- "description": "What risk does this mitigate?"
16873
+ "label": "Opposing arguments",
16874
+ "entityTypeId": "insight",
16875
+ "description": "The case against the proposal, voiced by the assigned contrarian regardless of personal view."
16511
16876
  },
16512
16877
  {
16513
- "label": "Opportunities to score",
16514
- "entityTypeId": "opportunity",
16515
- "description": "Opportunities scored on the same Cost of Delay inputs as features."
16878
+ "label": "Counter-evidence",
16879
+ "entityTypeId": "evidence",
16880
+ "description": "Data points the contrarian raises that the proposal does not yet account for."
16516
16881
  }
16517
16882
  ],
16518
16883
  "data": {
16519
16884
  "entity_types": [
16520
16885
  {
16521
- "type": "feature",
16522
- "role": "scored_item"
16523
- },
16524
- {
16525
- "type": "metric",
16526
- "role": "item"
16527
- },
16528
- {
16529
- "type": "outcome",
16530
- "role": "item"
16531
- },
16532
- {
16533
- "type": "risk",
16534
- "role": "item"
16535
- },
16536
- {
16537
- "type": "opportunity",
16538
- "role": "scored_item"
16539
- }
16540
- ],
16541
- "required_properties": {
16542
- "feature": [
16543
- {
16544
- "property": "cost_of_delay",
16545
- "type": "number",
16546
- "required": true,
16547
- "scope": "framework",
16548
- "label": "Cost of Delay",
16549
- "description": "Weekly revenue impact of not shipping"
16550
- },
16551
- {
16552
- "property": "job_size",
16553
- "type": "number",
16554
- "required": true,
16555
- "scope": "framework",
16556
- "label": "Job Size",
16557
- "description": "Weeks of development effort"
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
- }
16577
- ]
16578
- },
16579
- "computed_properties": [
16580
- {
16581
- "property": "wsjf_score",
16582
- "expression": "cost_of_delay / job_size",
16583
- "entity_type": "feature",
16584
- "label": "WSJF Score",
16585
- "format": "number"
16886
+ "type": "initiative",
16887
+ "role": "root"
16586
16888
  },
16587
16889
  {
16588
- "property": "wsjf_score",
16589
- "expression": "cost_of_delay / job_size",
16590
- "entity_type": "opportunity",
16591
- "label": "WSJF Score",
16592
- "format": "number"
16593
- }
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
- }
16627
- },
16628
- "structure": {
16629
- "pattern": "table"
16630
- },
16631
- "presentation": {
16632
- "layout": {
16633
- "type": "table",
16634
- "columns": [
16635
- {
16636
- "property": "title",
16637
- "label": "Items to evaluate",
16638
- "sortable": true
16639
- },
16640
- {
16641
- "property": "cost_of_delay",
16642
- "label": "User-Business Value",
16643
- "sortable": true
16644
- },
16645
- {
16646
- "property": "job_size",
16647
- "label": "Job Size",
16648
- "sortable": true
16649
- },
16650
- {
16651
- "property": "wsjf_score",
16652
- "label": "CoD Score",
16653
- "sortable": true
16654
- }
16655
- ]
16890
+ "type": "insight",
16891
+ "role": "bucket"
16892
+ },
16893
+ {
16894
+ "type": "evidence",
16895
+ "role": "bucket"
16896
+ }
16897
+ ],
16898
+ "required_properties": {}
16899
+ },
16900
+ "structure": {
16901
+ "pattern": "collection"
16902
+ },
16903
+ "presentation": {
16904
+ "layout": {
16905
+ "type": "grid",
16906
+ "groupBy": "type"
16656
16907
  },
16657
16908
  "sort_by": {
16658
16909
  "property": "title",
16659
16910
  "direction": "asc"
16660
16911
  },
16661
- "colour_by": "type",
16912
+ "colour_by": "group",
16662
16913
  "card_fields": [
16663
16914
  "title",
16664
- "description",
16665
- "status"
16915
+ "description"
16666
16916
  ]
16667
16917
  },
16668
16918
  "education": {
16669
- "purpose": "Quantify the economic impact of not delivering a feature by a given date, making urgency visible and enabling time-sensitive prioritisation.",
16670
- "core_question": "How much value are we losing every week this feature is not in production, and does that urgency justify fast-tracking it?",
16919
+ "purpose": "Make dissent a legitimate role rather than a personal stance. Assigning one reviewer to argue against the proposal forces the team to confront the strongest counter-case.",
16920
+ "core_question": "If we had to argue against this proposal (not because we believe it, but because the role demands it) what is the strongest case?",
16671
16921
  "when_to_use": [
16672
- "You have more ideas or features than capacity to build them",
16673
- "Stakeholders disagree on what to build next",
16674
- "You need a transparent, defensible prioritisation process"
16922
+ "A decision is heading toward consensus and you suspect groupthink",
16923
+ "Stakes are high and the team has not heard a serious counter-argument",
16924
+ "Cultural norms make raw dissent costly; assigning the role lowers the social cost"
16675
16925
  ],
16676
16926
  "when_not_to_use": [
16677
- "You have a single obvious next step with no contention",
16678
- "The backlog is small enough to sequence intuitively"
16927
+ "Genuine disagreement already exists in the room (let it surface; do not theatricalise it)",
16928
+ "The decision is small enough that the ceremony costs more than the insight returned",
16929
+ "The assigned contrarian will be punished socially for the role; set the norms first or skip"
16679
16930
  ]
16680
16931
  }
16681
16932
  },
16682
16933
  {
16683
- "id": "five-whys",
16934
+ "id": "second-order-thinking",
16684
16935
  "approach_ids": [
16685
- "reflect",
16686
- "inspect"
16936
+ "reflect"
16687
16937
  ],
16688
- "name": "Five Whys",
16938
+ "name": "Second-order Thinking",
16689
16939
  "version": "1.0.0",
16690
- "description": 'Iteratively ask "why?", typically five times, starting from a symptom; each answer becomes the subject of the next question. The chain of answers reveals the underlying root cause behind the surface problem.',
16940
+ "description": 'After deciding a move, ask "and then what?" repeatedly. Trace second-, third-, and higher-order consequences to surface downstream effects that first-order reasoning misses.',
16691
16941
  "category": "team_process",
16692
16942
  "origin": {
16693
16943
  "type": "practitioner",
16694
- "attribution": "Sakichi Toyoda / Toyota Production System",
16695
- "description": "Developed within the Toyota Production System as a root-cause analysis technique. Popularised through Lean and Six Sigma practice; now a widely used incident-review and design-debug staple.",
16696
- "year": 1930,
16697
- "license": "public_domain"
16944
+ "attribution": "Howard Marks / Charlie Munger",
16945
+ "description": `Howard Marks distinguished first-order vs second-order thinking in The Most Important Thing (2011) as the essential discipline of consequential decision-making. Charlie Munger's "and then what?" framing is the practical heuristic.`,
16946
+ "url": "https://www.oaktreecapital.com/insights/memo/dare-to-be-great-ii",
16947
+ "year": 2011,
16948
+ "license": "published_methodology"
16698
16949
  },
16699
16950
  "tags": [
16700
16951
  "team_process",
16701
16952
  "reflection",
16702
- "root_cause",
16953
+ "consequences",
16703
16954
  "tree"
16704
16955
  ],
16705
16956
  "slots": [
16706
16957
  {
16707
- "label": "Symptom",
16708
- "entityTypeId": "need",
16709
- "description": "The observed problem the analysis starts from."
16958
+ "label": "First-order move",
16959
+ "role": "first_order",
16960
+ "entityTypeId": "decision",
16961
+ "description": "The decision or move under consideration."
16710
16962
  },
16711
16963
  {
16712
- "label": "Why chain",
16964
+ "label": "Second-order consequences",
16965
+ "role": "second_order",
16713
16966
  "entityTypeId": "insight",
16714
- "description": 'Each "why?" answer along the chain, typically five iterations deep.'
16967
+ "description": "Downstream effects that follow from the first-order move."
16715
16968
  },
16716
16969
  {
16717
- "label": "Root cause",
16970
+ "label": "Higher-order consequences",
16971
+ "role": "higher_order",
16718
16972
  "entityTypeId": "insight",
16719
- "description": "The terminal answer at the bottom of the chain: the underlying cause to address."
16973
+ "description": "Third-, fourth-, fifth-order ripples: second-order consequences of the second-order consequences."
16720
16974
  }
16721
16975
  ],
16722
16976
  "data": {
16723
16977
  "entity_types": [
16724
16978
  {
16725
- "type": "need",
16979
+ "type": "decision",
16726
16980
  "role": "root"
16727
16981
  },
16728
16982
  {
@@ -16750,382 +17004,461 @@ var UPG_FRAMEWORKS = [
16750
17004
  ]
16751
17005
  },
16752
17006
  "education": {
16753
- "purpose": 'Move past surface symptoms by chaining "why?" questions until the underlying root cause surfaces, so fixes target the real driver rather than a downstream effect.',
16754
- "core_question": "Why is this happening, and why is THAT happening, until we reach a cause we can act on?",
17007
+ "purpose": 'Resist first-order reasoning by chaining "and then what?" until non-obvious downstream consequences come into view.',
17008
+ "core_question": "If we make this move and it works, what does the world look like next, and is that the world we want?",
16755
17009
  "when_to_use": [
16756
- "A problem keeps recurring after surface fixes",
16757
- "Post-incident review where the obvious cause feels too obvious",
16758
- "Designing a fix and you want to confirm you understand the actual driver"
17010
+ "A decision has feedback loops, market reactions, or behavioural ripples",
17011
+ "The first-order case is compelling, which is exactly when downstream effects bite",
17012
+ "Considering an irreversible or large-scale commitment"
16759
17013
  ],
16760
17014
  "when_not_to_use": [
16761
- "The problem has multiple independent root causes (use a fishbone or richer RCA tool)",
16762
- "You need quantitative attribution rather than a single-thread narrative",
16763
- 'Five linear "whys" oversimplify a systems problem with feedback loops'
17015
+ "Routine, reversible, low-blast-radius decisions where deliberation costs more than mistakes",
17016
+ "Higher-order branches diverge into pure speculation with no anchor in evidence",
17017
+ "Time pressure makes a deeper trace expensive and the first-order call is good enough"
16764
17018
  ]
16765
17019
  }
16766
- },
16767
- {
16768
- "id": "pre-mortem",
16769
- "approach_ids": [
16770
- "reflect"
16771
- ],
16772
- "name": "Pre-mortem",
16773
- "version": "1.0.0",
16774
- "description": "Imagine the project has already failed; work backward listing the plausible causes of the failure. Produce a risk register and matching mitigations before the work starts.",
16775
- "category": "team_process",
16776
- "origin": {
16777
- "type": "practitioner",
16778
- "attribution": "Gary Klein",
16779
- "description": "Popularised by Gary Klein in Harvard Business Review (2007) as a prospective-hindsight technique. Inverts the post-mortem: imagine failure first, then list causes, while there is still time to act.",
16780
- "url": "https://hbr.org/2007/09/performing-a-project-premortem",
16781
- "year": 2007,
16782
- "license": "published_methodology"
16783
- },
16784
- "tags": [
16785
- "team_process",
16786
- "reflection",
16787
- "risk",
16788
- "collection"
16789
- ],
16790
- "slots": [
16791
- {
16792
- "label": "Imagined failures",
16793
- "entityTypeId": "risk",
16794
- "description": "Plausible failure modes named as if they had already occurred."
16795
- },
16796
- {
16797
- "label": "Causes",
16798
- "entityTypeId": "insight",
16799
- "description": "For each imagined failure, the contributing causes the team can foresee."
16800
- },
16801
- {
16802
- "label": "Mitigations",
16803
- "entityTypeId": "initiative",
16804
- "description": "Mitigation actions the team will take before failure can occur."
17020
+ }
17021
+ ];
17022
+ var UPG_FRAMEWORKS_BY_ID = Object.fromEntries(
17023
+ UPG_FRAMEWORKS.map((fw) => [fw.id, fw])
17024
+ );
17025
+ var UPG_FRAMEWORKS_BY_CATEGORY = {};
17026
+ for (const fw of UPG_FRAMEWORKS) {
17027
+ if (!UPG_FRAMEWORKS_BY_CATEGORY[fw.category]) UPG_FRAMEWORKS_BY_CATEGORY[fw.category] = [];
17028
+ UPG_FRAMEWORKS_BY_CATEGORY[fw.category].push(fw);
17029
+ }
17030
+
17031
+ // src/grammar/validate.ts
17032
+ var crossProductEdgeTypeSet = new Set(UPG_CROSS_EDGE_TYPES);
17033
+ var CONTENT_DEPTH_WARNING_RULES = /* @__PURE__ */ new Set([
17034
+ "property-type",
17035
+ "property-enum",
17036
+ "self-loop",
17037
+ "framework-score"
17038
+ ]);
17039
+ function validateUPGDocument(doc) {
17040
+ const errors = [];
17041
+ const warnings = [];
17042
+ if (!doc || typeof doc !== "object") {
17043
+ return { valid: false, errors: [{ path: "$", message: "Document must be an object" }], warnings };
17044
+ }
17045
+ const d = doc;
17046
+ if (!d.upg_version || typeof d.upg_version !== "string") {
17047
+ errors.push({ path: "$.upg_version", message: "upg_version is required and must be a string" });
17048
+ }
17049
+ if (!d.exported_at || typeof d.exported_at !== "string") {
17050
+ errors.push({ path: "$.exported_at", message: "exported_at is required and must be an ISO 8601 string" });
17051
+ }
17052
+ if (!d.source || typeof d.source !== "object") {
17053
+ errors.push({ path: "$.source", message: "source is required and must be an object" });
17054
+ } else {
17055
+ const source = d.source;
17056
+ if (!source.tool || typeof source.tool !== "string") {
17057
+ errors.push({ path: "$.source.tool", message: "source.tool is required and must be a string" });
17058
+ }
17059
+ }
17060
+ if (!d.product || typeof d.product !== "object") {
17061
+ errors.push({ path: "$.product", message: "product is required and must be an object" });
17062
+ } else {
17063
+ const product = d.product;
17064
+ if (!product.id || typeof product.id !== "string") {
17065
+ errors.push({ path: "$.product.id", message: "product.id is required and must be a string" });
17066
+ }
17067
+ if (!product.title || typeof product.title !== "string") {
17068
+ errors.push({ path: "$.product.title", message: "product.title is required and must be a string" });
17069
+ }
17070
+ }
17071
+ const allKnownTypes = new Set(getTypes());
17072
+ const allKnownEdgeTypes = new Set(Object.keys(UPG_EDGE_CATALOG));
17073
+ if (!Array.isArray(d.nodes)) {
17074
+ errors.push({
17075
+ path: "$.nodes",
17076
+ message: d.nodes === void 0 ? "nodes is required and must be an array" : `nodes must be an array, got ${describeKind(d.nodes)}`
17077
+ });
17078
+ } else {
17079
+ const nodeIds = /* @__PURE__ */ new Set();
17080
+ const nodesById = /* @__PURE__ */ new Map();
17081
+ d.nodes.forEach((node, i) => {
17082
+ const path = `$.nodes[${i}]`;
17083
+ if (!node || typeof node !== "object") {
17084
+ errors.push({ path, message: "Each node must be an object" });
17085
+ return;
17086
+ }
17087
+ const n = node;
17088
+ if (!n.id || typeof n.id !== "string") {
17089
+ errors.push({ path: `${path}.id`, message: "Node id is required and must be a string" });
17090
+ } else {
17091
+ if (nodeIds.has(n.id)) {
17092
+ errors.push({ path: `${path}.id`, message: `Duplicate node id: ${n.id}` });
17093
+ }
17094
+ nodeIds.add(n.id);
17095
+ nodesById.set(n.id, n);
17096
+ }
17097
+ if (!n.type || typeof n.type !== "string") {
17098
+ errors.push({ path: `${path}.type`, message: "Node type is required and must be a string" });
17099
+ } else if (!allKnownTypes.has(n.type)) {
17100
+ warnings.push({ path: `${path}.type`, message: `Unknown UPG type: "${n.type}". Node will be preserved with its original type.` });
17101
+ }
17102
+ if (!n.title || typeof n.title !== "string") {
17103
+ errors.push({ path: `${path}.title`, message: "Node title is required and must be a string" });
17104
+ } else if (n.title.trim().length === 0) {
17105
+ errors.push({ path: `${path}.title`, message: "Node title must not be blank (whitespace-only)" });
17106
+ }
17107
+ if (n.properties !== void 0 && n.properties !== null) {
17108
+ if (typeof n.properties !== "object" || Array.isArray(n.properties)) {
17109
+ errors.push({
17110
+ path: `${path}.properties`,
17111
+ message: `Node properties must be a plain object when present, got ${describeKind(n.properties)}.`
17112
+ });
17113
+ }
17114
+ }
17115
+ if (typeof n.type === "string") {
17116
+ validateNodeProperties(n, `${path}`, warnings);
17117
+ }
17118
+ });
17119
+ if (!Array.isArray(d.edges)) {
17120
+ errors.push({
17121
+ path: "$.edges",
17122
+ message: d.edges === void 0 ? "edges is required and must be an array" : `edges must be an array, got ${describeKind(d.edges)}`
17123
+ });
17124
+ } else {
17125
+ d.edges.forEach((edge, i) => {
17126
+ const path = `$.edges[${i}]`;
17127
+ if (!edge || typeof edge !== "object") {
17128
+ errors.push({ path, message: "Each edge must be an object" });
17129
+ return;
17130
+ }
17131
+ const e = edge;
17132
+ if (!e.id || typeof e.id !== "string") {
17133
+ errors.push({ path: `${path}.id`, message: "Edge id is required and must be a string" });
17134
+ }
17135
+ if (typeof e.type === "string" && crossProductEdgeTypeSet.has(e.type)) {
17136
+ errors.push({
17137
+ path: `${path}.type`,
17138
+ message: `Cross-product edge type "${e.type}" must live in portfolio.cross_edges[], not product edges[].`
17139
+ });
17140
+ return;
17141
+ }
17142
+ if (!e.source || typeof e.source !== "string") {
17143
+ errors.push({ path: `${path}.source`, message: "Edge source is required and must be a string" });
17144
+ } else if (!nodeIds.has(e.source)) {
17145
+ errors.push({ path: `${path}.source`, message: `Edge source references unknown node id: ${e.source}` });
17146
+ }
17147
+ if (!e.target || typeof e.target !== "string") {
17148
+ errors.push({ path: `${path}.target`, message: "Edge target is required and must be a string" });
17149
+ } else if (!nodeIds.has(e.target)) {
17150
+ errors.push({ path: `${path}.target`, message: `Edge target references unknown node id: ${e.target}` });
17151
+ }
17152
+ if (typeof e.source === "string" && typeof e.target === "string" && e.source === e.target) {
17153
+ warnings.push({
17154
+ path: `${path}`,
17155
+ message: `Self-loop edge: source and target are the same node ("${e.source}"). A node related to itself is almost always an error.`,
17156
+ rule: "self-loop"
17157
+ });
17158
+ }
17159
+ if (!e.type || typeof e.type !== "string") {
17160
+ errors.push({ path: `${path}.type`, message: "Edge type is required and must be a string" });
17161
+ } else if (!allKnownEdgeTypes.has(e.type)) {
17162
+ warnings.push({
17163
+ path: `${path}.type`,
17164
+ message: `Unknown edge type: "${e.type}". Edge preserved but not in the edge registry.`
17165
+ });
17166
+ }
17167
+ });
17168
+ const valueToAxis = /* @__PURE__ */ new Map();
17169
+ const edgesArr = d.edges;
17170
+ for (const edge of edgesArr) {
17171
+ if (!edge || typeof edge !== "object") continue;
17172
+ const e = edge;
17173
+ if (e.type === "classification_axis_includes_classification_value" && typeof e.source === "string" && typeof e.target === "string") {
17174
+ valueToAxis.set(e.target, e.source);
17175
+ }
16805
17176
  }
16806
- ],
16807
- "data": {
16808
- "entity_types": [
16809
- {
16810
- "type": "risk",
16811
- "role": "bucket"
16812
- },
16813
- {
16814
- "type": "insight",
16815
- "role": "bucket"
16816
- },
16817
- {
16818
- "type": "initiative",
16819
- "role": "bucket"
17177
+ for (const [id, n] of nodesById.entries()) {
17178
+ if (n.type !== "classification_value") continue;
17179
+ if (valueToAxis.has(id)) continue;
17180
+ const pid = n.parent_id;
17181
+ if (typeof pid === "string") {
17182
+ const parent = nodesById.get(pid);
17183
+ if (parent && parent.type === "classification_axis") {
17184
+ valueToAxis.set(id, pid);
17185
+ }
16820
17186
  }
16821
- ],
16822
- "required_properties": {}
16823
- },
16824
- "structure": {
16825
- "pattern": "collection"
16826
- },
16827
- "presentation": {
16828
- "layout": {
16829
- "type": "grid",
16830
- "groupBy": "type"
16831
- },
16832
- "sort_by": {
16833
- "property": "title",
16834
- "direction": "asc"
16835
- },
16836
- "colour_by": "group",
16837
- "card_fields": [
16838
- "title",
16839
- "description"
16840
- ]
16841
- },
16842
- "education": {
16843
- "purpose": "Surface project risks early by inverting hindsight: imagine the project has already failed and ask why, while there is still time to mitigate.",
16844
- "core_question": "It is six months from now and the project has failed catastrophically. What happened, and why?",
16845
- "when_to_use": [
16846
- "Kicking off a project with significant downside or irreversible commitment",
16847
- "A plan looks too clean and the team senses unspoken concerns",
16848
- "Stakeholders disagree on risk; the exercise externalises and ranks them"
16849
- ],
16850
- "when_not_to_use": [
16851
- "The work is small, reversible, and cheap to course-correct",
16852
- "The team is in execution mode and reflective ceremonies will derail momentum",
16853
- "Risk surfacing has become performative: the team names risks but never mitigates them"
16854
- ]
16855
- }
16856
- },
16857
- {
16858
- "id": "red-team",
16859
- "approach_ids": [
16860
- "reflect",
16861
- "inspect"
16862
- ],
16863
- "name": "Red Team",
16864
- "version": "1.0.0",
16865
- "description": "Structured adversarial review. A designated group is assigned to attack a plan, design, or proposal from an outside-in stance, surfacing weaknesses the inside-out builders cannot see.",
16866
- "category": "team_process",
16867
- "origin": {
16868
- "type": "practitioner",
16869
- "attribution": "US Department of Defense (Cold War era); broadened by security and intelligence practice",
16870
- "description": `Originated in Cold War-era military strategic exercises ("red" team takes the adversary role against the "blue" team's defence). Adopted by cybersecurity, intelligence analysis, and product teams as a structured contrarian-review practice.`,
16871
- "year": 1960,
16872
- "license": "public_domain"
16873
- },
16874
- "tags": [
16875
- "team_process",
16876
- "reflection",
16877
- "adversarial",
16878
- "collection"
16879
- ],
16880
- "slots": [
16881
- {
16882
- "label": "Target",
16883
- "entityTypeId": "initiative",
16884
- "description": "The plan, design, or proposal under adversarial review."
16885
- },
16886
- {
16887
- "label": "Attack vectors",
16888
- "entityTypeId": "risk",
16889
- "description": "The angles the red team uses to probe weaknesses."
16890
- },
16891
- {
16892
- "label": "Findings",
16893
- "entityTypeId": "insight",
16894
- "description": "Weaknesses, blind spots, or unstated assumptions surfaced by the review."
16895
17187
  }
16896
- ],
16897
- "data": {
16898
- "entity_types": [
16899
- {
16900
- "type": "initiative",
16901
- "role": "root"
16902
- },
16903
- {
16904
- "type": "risk",
16905
- "role": "bucket"
16906
- },
16907
- {
16908
- "type": "insight",
16909
- "role": "bucket"
17188
+ const axesPerAnalysis = /* @__PURE__ */ new Map();
17189
+ for (const edge of edgesArr) {
17190
+ if (!edge || typeof edge !== "object") continue;
17191
+ const e = edge;
17192
+ if (e.type === "competitive_analysis_dimensioned_by_classification_axis" && typeof e.source === "string") {
17193
+ axesPerAnalysis.set(e.source, (axesPerAnalysis.get(e.source) ?? 0) + 1);
16910
17194
  }
16911
- ],
16912
- "required_properties": {}
16913
- },
16914
- "structure": {
16915
- "pattern": "collection"
16916
- },
16917
- "presentation": {
16918
- "layout": {
16919
- "type": "grid",
16920
- "groupBy": "type"
16921
- },
16922
- "sort_by": {
16923
- "property": "title",
16924
- "direction": "asc"
16925
- },
16926
- "colour_by": "group",
16927
- "card_fields": [
16928
- "title",
16929
- "description"
16930
- ]
16931
- },
16932
- "education": {
16933
- "purpose": "Stress-test a plan against an explicit adversary by assigning reviewers to attack rather than agree, so weaknesses surface before reality finds them.",
16934
- "core_question": "If a competent adversary wanted this to fail, where would they push first, and would we hold?",
16935
- "when_to_use": [
16936
- "A high-stakes decision, launch, or security posture needs hardening",
16937
- "Inside-out thinking is dominant and dissent has gone quiet",
16938
- "Risk register is suspiciously short for the size of the bet"
16939
- ],
16940
- "when_not_to_use": [
16941
- "Early-stage exploration where adversarial framing would crush a fragile idea prematurely",
16942
- "Team trust is too low: red-teaming will read as personal attack rather than role-play",
16943
- "The work is small enough that a lightweight devil's-advocate pass is sufficient"
16944
- ]
16945
- }
16946
- },
16947
- {
16948
- "id": "devils-advocate",
16949
- "approach_ids": [
16950
- "reflect"
16951
- ],
16952
- "name": "Devil's Advocate",
16953
- "version": "1.0.0",
16954
- "description": "Designate one reviewer to formally take the opposing position regardless of personal view. The assigned-role contrarian defangs groupthink by making dissent legitimate and structured.",
16955
- "category": "team_process",
16956
- "origin": {
16957
- "type": "practitioner",
16958
- "attribution": "Roman Catholic Church (advocatus diaboli); broadened by decision-quality practice",
16959
- "description": "Originated in 16th-century canonisation proceedings as the advocatus diaboli, an official assigned to argue against canonising a candidate. Adopted by decision-science and product-team practice as a structured antidote to groupthink.",
16960
- "year": 1587,
16961
- "license": "public_domain"
16962
- },
16963
- "tags": [
16964
- "team_process",
16965
- "reflection",
16966
- "decision_quality",
16967
- "collection"
16968
- ],
16969
- "slots": [
16970
- {
16971
- "label": "Proposal",
16972
- "entityTypeId": "initiative",
16973
- "description": "The plan or recommendation under consideration."
16974
- },
16975
- {
16976
- "label": "Opposing arguments",
16977
- "entityTypeId": "insight",
16978
- "description": "The case against the proposal, voiced by the assigned contrarian regardless of personal view."
16979
- },
16980
- {
16981
- "label": "Counter-evidence",
16982
- "entityTypeId": "evidence",
16983
- "description": "Data points the contrarian raises that the proposal does not yet account for."
16984
17195
  }
16985
- ],
16986
- "data": {
16987
- "entity_types": [
16988
- {
16989
- "type": "initiative",
16990
- "role": "root"
16991
- },
16992
- {
16993
- "type": "insight",
16994
- "role": "bucket"
16995
- },
16996
- {
16997
- "type": "evidence",
16998
- "role": "bucket"
16999
- }
17000
- ],
17001
- "required_properties": {}
17002
- },
17003
- "structure": {
17004
- "pattern": "collection"
17005
- },
17006
- "presentation": {
17007
- "layout": {
17008
- "type": "grid",
17009
- "groupBy": "type"
17010
- },
17011
- "sort_by": {
17012
- "property": "title",
17013
- "direction": "asc"
17014
- },
17015
- "colour_by": "group",
17016
- "card_fields": [
17017
- "title",
17018
- "description"
17019
- ]
17020
- },
17021
- "education": {
17022
- "purpose": "Make dissent a legitimate role rather than a personal stance. Assigning one reviewer to argue against the proposal forces the team to confront the strongest counter-case.",
17023
- "core_question": "If we had to argue against this proposal (not because we believe it, but because the role demands it) what is the strongest case?",
17024
- "when_to_use": [
17025
- "A decision is heading toward consensus and you suspect groupthink",
17026
- "Stakes are high and the team has not heard a serious counter-argument",
17027
- "Cultural norms make raw dissent costly; assigning the role lowers the social cost"
17028
- ],
17029
- "when_not_to_use": [
17030
- "Genuine disagreement already exists in the room (let it surface; do not theatricalise it)",
17031
- "The decision is small enough that the ceremony costs more than the insight returned",
17032
- "The assigned contrarian will be punished socially for the role; set the norms first or skip"
17033
- ]
17196
+ d.nodes.forEach((node, i) => {
17197
+ if (!node || typeof node !== "object") return;
17198
+ const n = node;
17199
+ if (n.type !== "competitive_analysis") return;
17200
+ const props = n.properties;
17201
+ if (!props || typeof props !== "object") return;
17202
+ const empty = props.empty_cells;
17203
+ if (!Array.isArray(empty)) return;
17204
+ const axisCount = axesPerAnalysis.get(n.id ?? "") ?? 0;
17205
+ if (axisCount !== 2) {
17206
+ warnings.push({
17207
+ path: `$.nodes[${i}].properties.empty_cells`,
17208
+ message: `empty_cells is meaningful only on a 2-axis competitive_analysis; this node is dimensioned by ${axisCount} classification_axis ${axisCount === 1 ? "child" : "children"}.`
17209
+ });
17210
+ }
17211
+ empty.forEach((cell, j) => {
17212
+ const cpath = `$.nodes[${i}].properties.empty_cells[${j}]`;
17213
+ if (!cell || typeof cell !== "object") {
17214
+ errors.push({ path: cpath, message: "empty_cells entry must be an object" });
17215
+ return;
17216
+ }
17217
+ const c = cell;
17218
+ const aRef = c.axis_a_value_ref;
17219
+ const bRef = c.axis_b_value_ref;
17220
+ if (typeof aRef !== "string") {
17221
+ errors.push({ path: `${cpath}.axis_a_value_ref`, message: "axis_a_value_ref is required and must be a string" });
17222
+ }
17223
+ if (typeof bRef !== "string") {
17224
+ errors.push({ path: `${cpath}.axis_b_value_ref`, message: "axis_b_value_ref is required and must be a string" });
17225
+ }
17226
+ if (typeof aRef !== "string" || typeof bRef !== "string") return;
17227
+ const aNode = nodesById.get(aRef);
17228
+ const bNode = nodesById.get(bRef);
17229
+ if (!aNode) {
17230
+ errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Unknown node id: ${aRef}` });
17231
+ } else if (aNode.type !== "classification_value") {
17232
+ errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Expected classification_value, found "${String(aNode.type)}" at ${aRef}` });
17233
+ }
17234
+ if (!bNode) {
17235
+ errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Unknown node id: ${bRef}` });
17236
+ } else if (bNode.type !== "classification_value") {
17237
+ errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Expected classification_value, found "${String(bNode.type)}" at ${bRef}` });
17238
+ }
17239
+ if (aNode && bNode && aNode.type === "classification_value" && bNode.type === "classification_value") {
17240
+ const aAxis = valueToAxis.get(aRef);
17241
+ const bAxis = valueToAxis.get(bRef);
17242
+ if (aAxis && bAxis && aAxis === bAxis) {
17243
+ errors.push({
17244
+ path: cpath,
17245
+ message: `empty_cells references two classification_values from the same axis (${aAxis}); axis_a and axis_b must come from distinct classification_axis parents.`
17246
+ });
17247
+ }
17248
+ }
17249
+ const kind = c.rationale_kind;
17250
+ if (kind !== void 0 && kind !== "structural" && kind !== "ideological" && kind !== "opportunity") {
17251
+ errors.push({
17252
+ path: `${cpath}.rationale_kind`,
17253
+ message: `rationale_kind must be one of 'structural' | 'ideological' | 'opportunity'; got "${String(kind)}".`
17254
+ });
17255
+ }
17256
+ });
17257
+ });
17258
+ validateFrameworkScores(d.nodes, d.edges, warnings);
17259
+ }
17260
+ }
17261
+ return { valid: errors.length === 0, errors, warnings };
17262
+ }
17263
+ function validateFrameworkScores(nodes, edges, warnings) {
17264
+ const nodeById = /* @__PURE__ */ new Map();
17265
+ const exercises = [];
17266
+ for (const node of nodes) {
17267
+ if (!node || typeof node !== "object") continue;
17268
+ const n = node;
17269
+ if (typeof n.id === "string") nodeById.set(n.id, n);
17270
+ if (n.type !== "framework_exercise") continue;
17271
+ const props = n.properties;
17272
+ if (!props || typeof props !== "object" || Array.isArray(props)) continue;
17273
+ const frameworkId = props.framework_id;
17274
+ if (typeof frameworkId !== "string") continue;
17275
+ const framework = UPG_FRAMEWORKS_BY_ID[frameworkId];
17276
+ if (!framework) continue;
17277
+ if (typeof n.id === "string") exercises.push({ id: n.id, framework });
17278
+ }
17279
+ if (exercises.length === 0) return;
17280
+ const exerciseById = new Map(exercises.map((e) => [e.id, e.framework]));
17281
+ edges.forEach((edge, i) => {
17282
+ if (!edge || typeof edge !== "object") return;
17283
+ const e = edge;
17284
+ if (e.type !== "framework_exercise_includes_node") return;
17285
+ if (typeof e.source !== "string" || typeof e.target !== "string") return;
17286
+ const framework = exerciseById.get(e.source);
17287
+ if (!framework) return;
17288
+ const score = e.properties;
17289
+ if (!score || typeof score !== "object" || Array.isArray(score)) return;
17290
+ const targetNode = nodeById.get(e.target);
17291
+ const targetType = typeof targetNode?.type === "string" ? targetNode.type : void 0;
17292
+ if (!targetType) return;
17293
+ const requirements = framework.data?.required_properties?.[targetType];
17294
+ if (!requirements || requirements.length === 0) return;
17295
+ const divisors = divisorInputs(framework);
17296
+ const path = `$.edges[${i}].properties`;
17297
+ const scoreObj = score;
17298
+ for (const req of requirements) {
17299
+ const value = scoreObj[req.property];
17300
+ if (value === void 0 || value === null) continue;
17301
+ const finding = checkScoreValue(req, value, divisors.has(req.property), framework.id);
17302
+ if (finding) {
17303
+ warnings.push({
17304
+ path: `${path}.${req.property}`,
17305
+ message: finding,
17306
+ rule: "framework-score"
17307
+ });
17308
+ }
17034
17309
  }
17035
- },
17036
- {
17037
- "id": "second-order-thinking",
17038
- "approach_ids": [
17039
- "reflect"
17040
- ],
17041
- "name": "Second-order Thinking",
17042
- "version": "1.0.0",
17043
- "description": 'After deciding a move, ask "and then what?" repeatedly. Trace second-, third-, and higher-order consequences to surface downstream effects that first-order reasoning misses.',
17044
- "category": "team_process",
17045
- "origin": {
17046
- "type": "practitioner",
17047
- "attribution": "Howard Marks / Charlie Munger",
17048
- "description": `Howard Marks distinguished first-order vs second-order thinking in The Most Important Thing (2011) as the essential discipline of consequential decision-making. Charlie Munger's "and then what?" framing is the practical heuristic.`,
17049
- "url": "https://www.oaktreecapital.com/insights/memo/dare-to-be-great-ii",
17050
- "year": 2011,
17051
- "license": "published_methodology"
17052
- },
17053
- "tags": [
17054
- "team_process",
17055
- "reflection",
17056
- "consequences",
17057
- "tree"
17058
- ],
17059
- "slots": [
17060
- {
17061
- "label": "First-order move",
17062
- "entityTypeId": "decision",
17063
- "description": "The decision or move under consideration."
17064
- },
17065
- {
17066
- "label": "Second-order consequences",
17067
- "entityTypeId": "insight",
17068
- "description": "Downstream effects that follow from the first-order move."
17069
- },
17070
- {
17071
- "label": "Higher-order consequences",
17072
- "entityTypeId": "insight",
17073
- "description": "Third-, fourth-, fifth-order ripples: second-order consequences of the second-order consequences."
17310
+ });
17311
+ }
17312
+ function divisorInputs(framework) {
17313
+ const expr = framework.data?.computed_properties?.[0]?.expression;
17314
+ if (typeof expr !== "string" || expr.length === 0) return EMPTY_STRING_SET;
17315
+ const out = /* @__PURE__ */ new Set();
17316
+ const re = /\/\s*\(?\s*([A-Za-z_][A-Za-z0-9_]*)/g;
17317
+ let m;
17318
+ while ((m = re.exec(expr)) !== null) out.add(m[1]);
17319
+ return out;
17320
+ }
17321
+ var EMPTY_STRING_SET = /* @__PURE__ */ new Set();
17322
+ function checkScoreValue(req, value, isDivisor, frameworkId) {
17323
+ switch (req.type) {
17324
+ case "enum": {
17325
+ const allowed = req.enum_values ?? [];
17326
+ if (typeof value !== "string") {
17327
+ return `${frameworkId} score "${req.property}" must be one of ${allowed.join(", ")}; got ${describeKind(value)}.`;
17074
17328
  }
17075
- ],
17076
- "data": {
17077
- "entity_types": [
17078
- {
17079
- "type": "decision",
17080
- "role": "root"
17081
- },
17082
- {
17083
- "type": "insight",
17084
- "role": "branch"
17329
+ if (allowed.length > 0 && !allowed.includes(value)) {
17330
+ return `${frameworkId} score "${req.property}" has value "${value}" outside its allowed set: ${allowed.join(", ")}.`;
17331
+ }
17332
+ return null;
17333
+ }
17334
+ case "assessment": {
17335
+ const num = numericScore(value);
17336
+ if (num === null) {
17337
+ return `${frameworkId} score "${req.property}" must be a number; got ${describeKind(value)}.`;
17338
+ }
17339
+ const scale = req.scale_id ? getScale(req.scale_id) : void 0;
17340
+ if (scale) {
17341
+ if (num < scale.min || num > scale.max) {
17342
+ return `${frameworkId} score "${req.property}" is ${num}, outside the ${req.scale_id} scale range [${scale.min}, ${scale.max}].`;
17085
17343
  }
17086
- ],
17087
- "required_properties": {}
17088
- },
17089
- "structure": {
17090
- "pattern": "tree"
17091
- },
17092
- "presentation": {
17093
- "layout": {
17094
- "type": "tree",
17095
- "direction": "TB"
17096
- },
17097
- "sort_by": {
17098
- "property": "title",
17099
- "direction": "asc"
17100
- },
17101
- "card_fields": [
17102
- "title",
17103
- "description"
17104
- ]
17105
- },
17106
- "education": {
17107
- "purpose": 'Resist first-order reasoning by chaining "and then what?" until non-obvious downstream consequences come into view.',
17108
- "core_question": "If we make this move and it works, what does the world look like next, and is that the world we want?",
17109
- "when_to_use": [
17110
- "A decision has feedback loops, market reactions, or behavioural ripples",
17111
- "The first-order case is compelling, which is exactly when downstream effects bite",
17112
- "Considering an irreversible or large-scale commitment"
17113
- ],
17114
- "when_not_to_use": [
17115
- "Routine, reversible, low-blast-radius decisions where deliberation costs more than mistakes",
17116
- "Higher-order branches diverge into pure speculation with no anchor in evidence",
17117
- "Time pressure makes a deeper trace expensive and the first-order call is good enough"
17118
- ]
17344
+ } else if (num <= 0) {
17345
+ return `${frameworkId} score "${req.property}" must be greater than 0; got ${num}.`;
17346
+ }
17347
+ if (isDivisor && num === 0) {
17348
+ return `${frameworkId} score "${req.property}" must not be 0 (it is a divisor in the framework's formula).`;
17349
+ }
17350
+ return null;
17351
+ }
17352
+ case "number": {
17353
+ const num = numericScore(value);
17354
+ if (num === null) {
17355
+ return `${frameworkId} score "${req.property}" must be a number; got ${describeKind(value)}.`;
17356
+ }
17357
+ if (isDivisor && num === 0) {
17358
+ return `${frameworkId} score "${req.property}" must not be 0 (it is a divisor in the framework's formula).`;
17359
+ }
17360
+ if (num < 0) {
17361
+ return `${frameworkId} score "${req.property}" must not be negative; got ${num}.`;
17362
+ }
17363
+ return null;
17364
+ }
17365
+ case "boolean": {
17366
+ if (typeof value !== "boolean") {
17367
+ return `${frameworkId} score "${req.property}" must be a boolean; got ${describeKind(value)}.`;
17368
+ }
17369
+ return null;
17370
+ }
17371
+ case "string": {
17372
+ if (typeof value !== "string") {
17373
+ return `${frameworkId} score "${req.property}" must be a string; got ${describeKind(value)}.`;
17374
+ }
17375
+ return null;
17119
17376
  }
17377
+ default:
17378
+ return null;
17120
17379
  }
17121
- ];
17122
- var UPG_FRAMEWORKS_BY_ID = Object.fromEntries(
17123
- UPG_FRAMEWORKS.map((fw) => [fw.id, fw])
17124
- );
17125
- var UPG_FRAMEWORKS_BY_CATEGORY = {};
17126
- for (const fw of UPG_FRAMEWORKS) {
17127
- if (!UPG_FRAMEWORKS_BY_CATEGORY[fw.category]) UPG_FRAMEWORKS_BY_CATEGORY[fw.category] = [];
17128
- UPG_FRAMEWORKS_BY_CATEGORY[fw.category].push(fw);
17380
+ }
17381
+ function numericScore(value) {
17382
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
17383
+ if (typeof value === "string") {
17384
+ const n = Number.parseFloat(value);
17385
+ return Number.isFinite(n) ? n : null;
17386
+ }
17387
+ if (value && typeof value === "object" && !Array.isArray(value)) {
17388
+ const v = value.value;
17389
+ if (typeof v === "number") return Number.isFinite(v) ? v : null;
17390
+ }
17391
+ return null;
17392
+ }
17393
+ function runtimeKind(val) {
17394
+ if (val === null || val === void 0) return null;
17395
+ if (Array.isArray(val)) return "string[]";
17396
+ switch (typeof val) {
17397
+ case "string":
17398
+ return "string";
17399
+ case "number":
17400
+ return "number";
17401
+ case "boolean":
17402
+ return "boolean";
17403
+ case "object":
17404
+ return "object";
17405
+ default:
17406
+ return null;
17407
+ }
17408
+ }
17409
+ function valueMatchesType(defType, val) {
17410
+ const kind = runtimeKind(val);
17411
+ if (kind === null) return true;
17412
+ switch (defType) {
17413
+ case "string":
17414
+ return kind === "string";
17415
+ case "number":
17416
+ return kind === "number";
17417
+ case "boolean":
17418
+ return kind === "boolean";
17419
+ case "string[]":
17420
+ return kind === "string[]";
17421
+ case "object":
17422
+ case "assessment":
17423
+ return kind === "object";
17424
+ default:
17425
+ return true;
17426
+ }
17427
+ }
17428
+ function describeKind(val) {
17429
+ if (Array.isArray(val)) return "array";
17430
+ return typeof val;
17431
+ }
17432
+ function validateNodeProperties(node, basePath, warnings) {
17433
+ const type = node.type;
17434
+ if (typeof type !== "string") return;
17435
+ const schema = getPropertySchema(type);
17436
+ if (!schema) return;
17437
+ const props = node.properties;
17438
+ if (!props || typeof props !== "object" || Array.isArray(props)) return;
17439
+ for (const [key, value] of Object.entries(props)) {
17440
+ const def = schema[key];
17441
+ if (!def) continue;
17442
+ if (value === null || value === void 0) continue;
17443
+ if (!valueMatchesType(def.type, value)) {
17444
+ warnings.push({
17445
+ path: `${basePath}.properties.${key}`,
17446
+ message: `Property "${key}" on ${type} should be ${def.type}, got ${describeKind(value)}.`,
17447
+ rule: "property-type"
17448
+ });
17449
+ continue;
17450
+ }
17451
+ if (def.enum && typeof value === "string" && !def.enum.includes(value)) {
17452
+ warnings.push({
17453
+ path: `${basePath}.properties.${key}`,
17454
+ message: `Property "${key}" on ${type} has value "${value}" outside its allowed set: ${def.enum.join(", ")}.`,
17455
+ rule: "property-enum"
17456
+ });
17457
+ }
17458
+ }
17459
+ }
17460
+ function isUPGDocument(doc) {
17461
+ return validateUPGDocument(doc).valid;
17129
17462
  }
17130
17463
 
17131
17464
  // src/presentation/labels.ts
@@ -25524,7 +25857,7 @@ function isCanonical(text) {
25524
25857
  }
25525
25858
 
25526
25859
  // src/index.ts
25527
- var UPG_VERSION = "0.8.7";
25860
+ var UPG_VERSION = "0.8.9";
25528
25861
  var UPG_FORMAT_VERSION = "0.4.0";
25529
25862
  var MARKDOWN_FORMAT_VERSION = "0.1";
25530
25863
  var UPG_TYPES = getTypes();