@unified-product-graph/core 0.8.6 → 0.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -24,6 +24,7 @@ __export(index_exports, {
24
24
  BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK: () => BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK,
25
25
  BUSINESS_GTM_GROWTH_PLAYBOOK: () => BUSINESS_GTM_GROWTH_PLAYBOOK,
26
26
  BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK: () => BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK,
27
+ CONTENT_DEPTH_WARNING_RULES: () => CONTENT_DEPTH_WARNING_RULES,
27
28
  DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK: () => DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK,
28
29
  ENGINEERING_PLATFORM_PLAYBOOK: () => ENGINEERING_PLATFORM_PLAYBOOK,
29
30
  EXPERIENCE_DESIGN_BRAND_PLAYBOOK: () => EXPERIENCE_DESIGN_BRAND_PLAYBOOK,
@@ -8666,209 +8667,6 @@ function rotateSlug(node, next) {
8666
8667
  return node;
8667
8668
  }
8668
8669
 
8669
- // src/grammar/validate.ts
8670
- var crossProductEdgeTypeSet = new Set(UPG_CROSS_EDGE_TYPES);
8671
- function validateUPGDocument(doc) {
8672
- const errors = [];
8673
- const warnings = [];
8674
- if (!doc || typeof doc !== "object") {
8675
- return { valid: false, errors: [{ path: "$", message: "Document must be an object" }], warnings };
8676
- }
8677
- const d = doc;
8678
- if (!d.upg_version || typeof d.upg_version !== "string") {
8679
- errors.push({ path: "$.upg_version", message: "upg_version is required and must be a string" });
8680
- }
8681
- if (!d.exported_at || typeof d.exported_at !== "string") {
8682
- errors.push({ path: "$.exported_at", message: "exported_at is required and must be an ISO 8601 string" });
8683
- }
8684
- if (!d.source || typeof d.source !== "object") {
8685
- errors.push({ path: "$.source", message: "source is required and must be an object" });
8686
- } else {
8687
- const source = d.source;
8688
- if (!source.tool || typeof source.tool !== "string") {
8689
- errors.push({ path: "$.source.tool", message: "source.tool is required and must be a string" });
8690
- }
8691
- }
8692
- if (!d.product || typeof d.product !== "object") {
8693
- errors.push({ path: "$.product", message: "product is required and must be an object" });
8694
- } else {
8695
- const product = d.product;
8696
- if (!product.id || typeof product.id !== "string") {
8697
- errors.push({ path: "$.product.id", message: "product.id is required and must be a string" });
8698
- }
8699
- if (!product.title || typeof product.title !== "string") {
8700
- errors.push({ path: "$.product.title", message: "product.title is required and must be a string" });
8701
- }
8702
- }
8703
- const allKnownTypes = new Set(getTypes());
8704
- const allKnownEdgeTypes = new Set(Object.keys(UPG_EDGE_CATALOG));
8705
- if (!Array.isArray(d.nodes)) {
8706
- errors.push({ path: "$.nodes", message: "nodes is required and must be an array" });
8707
- } else {
8708
- const nodeIds = /* @__PURE__ */ new Set();
8709
- const nodesById = /* @__PURE__ */ new Map();
8710
- d.nodes.forEach((node, i) => {
8711
- const path = `$.nodes[${i}]`;
8712
- if (!node || typeof node !== "object") {
8713
- errors.push({ path, message: "Each node must be an object" });
8714
- return;
8715
- }
8716
- const n = node;
8717
- if (!n.id || typeof n.id !== "string") {
8718
- errors.push({ path: `${path}.id`, message: "Node id is required and must be a string" });
8719
- } else {
8720
- if (nodeIds.has(n.id)) {
8721
- errors.push({ path: `${path}.id`, message: `Duplicate node id: ${n.id}` });
8722
- }
8723
- nodeIds.add(n.id);
8724
- nodesById.set(n.id, n);
8725
- }
8726
- if (!n.type || typeof n.type !== "string") {
8727
- errors.push({ path: `${path}.type`, message: "Node type is required and must be a string" });
8728
- } else if (!allKnownTypes.has(n.type)) {
8729
- warnings.push({ path: `${path}.type`, message: `Unknown UPG type: "${n.type}". Node will be preserved with its original type.` });
8730
- }
8731
- if (!n.title || typeof n.title !== "string") {
8732
- errors.push({ path: `${path}.title`, message: "Node title is required and must be a string" });
8733
- }
8734
- });
8735
- if (!Array.isArray(d.edges)) {
8736
- errors.push({ path: "$.edges", message: "edges is required and must be an array" });
8737
- } else {
8738
- d.edges.forEach((edge, i) => {
8739
- const path = `$.edges[${i}]`;
8740
- if (!edge || typeof edge !== "object") {
8741
- errors.push({ path, message: "Each edge must be an object" });
8742
- return;
8743
- }
8744
- const e = edge;
8745
- if (!e.id || typeof e.id !== "string") {
8746
- errors.push({ path: `${path}.id`, message: "Edge id is required and must be a string" });
8747
- }
8748
- if (typeof e.type === "string" && crossProductEdgeTypeSet.has(e.type)) {
8749
- errors.push({
8750
- path: `${path}.type`,
8751
- message: `Cross-product edge type "${e.type}" must live in portfolio.cross_edges[], not product edges[].`
8752
- });
8753
- return;
8754
- }
8755
- if (!e.source || typeof e.source !== "string") {
8756
- errors.push({ path: `${path}.source`, message: "Edge source is required and must be a string" });
8757
- } else if (!nodeIds.has(e.source)) {
8758
- errors.push({ path: `${path}.source`, message: `Edge source references unknown node id: ${e.source}` });
8759
- }
8760
- if (!e.target || typeof e.target !== "string") {
8761
- errors.push({ path: `${path}.target`, message: "Edge target is required and must be a string" });
8762
- } else if (!nodeIds.has(e.target)) {
8763
- errors.push({ path: `${path}.target`, message: `Edge target references unknown node id: ${e.target}` });
8764
- }
8765
- if (!e.type || typeof e.type !== "string") {
8766
- errors.push({ path: `${path}.type`, message: "Edge type is required and must be a string" });
8767
- } else if (!allKnownEdgeTypes.has(e.type)) {
8768
- warnings.push({
8769
- path: `${path}.type`,
8770
- message: `Unknown edge type: "${e.type}". Edge preserved but not in the edge registry.`
8771
- });
8772
- }
8773
- });
8774
- const valueToAxis = /* @__PURE__ */ new Map();
8775
- const edgesArr = d.edges;
8776
- for (const edge of edgesArr) {
8777
- if (!edge || typeof edge !== "object") continue;
8778
- const e = edge;
8779
- if (e.type === "classification_axis_includes_classification_value" && typeof e.source === "string" && typeof e.target === "string") {
8780
- valueToAxis.set(e.target, e.source);
8781
- }
8782
- }
8783
- for (const [id, n] of nodesById.entries()) {
8784
- if (n.type !== "classification_value") continue;
8785
- if (valueToAxis.has(id)) continue;
8786
- const pid = n.parent_id;
8787
- if (typeof pid === "string") {
8788
- const parent = nodesById.get(pid);
8789
- if (parent && parent.type === "classification_axis") {
8790
- valueToAxis.set(id, pid);
8791
- }
8792
- }
8793
- }
8794
- const axesPerAnalysis = /* @__PURE__ */ new Map();
8795
- for (const edge of edgesArr) {
8796
- if (!edge || typeof edge !== "object") continue;
8797
- const e = edge;
8798
- if (e.type === "competitive_analysis_dimensioned_by_classification_axis" && typeof e.source === "string") {
8799
- axesPerAnalysis.set(e.source, (axesPerAnalysis.get(e.source) ?? 0) + 1);
8800
- }
8801
- }
8802
- d.nodes.forEach((node, i) => {
8803
- if (!node || typeof node !== "object") return;
8804
- const n = node;
8805
- if (n.type !== "competitive_analysis") return;
8806
- const props = n.properties;
8807
- if (!props || typeof props !== "object") return;
8808
- const empty = props.empty_cells;
8809
- if (!Array.isArray(empty)) return;
8810
- const axisCount = axesPerAnalysis.get(n.id ?? "") ?? 0;
8811
- if (axisCount !== 2) {
8812
- warnings.push({
8813
- path: `$.nodes[${i}].properties.empty_cells`,
8814
- message: `empty_cells is meaningful only on a 2-axis competitive_analysis; this node is dimensioned by ${axisCount} classification_axis ${axisCount === 1 ? "child" : "children"}.`
8815
- });
8816
- }
8817
- empty.forEach((cell, j) => {
8818
- const cpath = `$.nodes[${i}].properties.empty_cells[${j}]`;
8819
- if (!cell || typeof cell !== "object") {
8820
- errors.push({ path: cpath, message: "empty_cells entry must be an object" });
8821
- return;
8822
- }
8823
- const c = cell;
8824
- const aRef = c.axis_a_value_ref;
8825
- const bRef = c.axis_b_value_ref;
8826
- if (typeof aRef !== "string") {
8827
- errors.push({ path: `${cpath}.axis_a_value_ref`, message: "axis_a_value_ref is required and must be a string" });
8828
- }
8829
- if (typeof bRef !== "string") {
8830
- errors.push({ path: `${cpath}.axis_b_value_ref`, message: "axis_b_value_ref is required and must be a string" });
8831
- }
8832
- if (typeof aRef !== "string" || typeof bRef !== "string") return;
8833
- const aNode = nodesById.get(aRef);
8834
- const bNode = nodesById.get(bRef);
8835
- if (!aNode) {
8836
- errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Unknown node id: ${aRef}` });
8837
- } else if (aNode.type !== "classification_value") {
8838
- errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Expected classification_value, found "${String(aNode.type)}" at ${aRef}` });
8839
- }
8840
- if (!bNode) {
8841
- errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Unknown node id: ${bRef}` });
8842
- } else if (bNode.type !== "classification_value") {
8843
- errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Expected classification_value, found "${String(bNode.type)}" at ${bRef}` });
8844
- }
8845
- if (aNode && bNode && aNode.type === "classification_value" && bNode.type === "classification_value") {
8846
- const aAxis = valueToAxis.get(aRef);
8847
- const bAxis = valueToAxis.get(bRef);
8848
- if (aAxis && bAxis && aAxis === bAxis) {
8849
- errors.push({
8850
- path: cpath,
8851
- message: `empty_cells references two classification_values from the same axis (${aAxis}); axis_a and axis_b must come from distinct classification_axis parents.`
8852
- });
8853
- }
8854
- }
8855
- const kind = c.rationale_kind;
8856
- if (kind !== void 0 && kind !== "structural" && kind !== "ideological" && kind !== "opportunity") {
8857
- errors.push({
8858
- path: `${cpath}.rationale_kind`,
8859
- message: `rationale_kind must be one of 'structural' | 'ideological' | 'opportunity'; got "${String(kind)}".`
8860
- });
8861
- }
8862
- });
8863
- });
8864
- }
8865
- }
8866
- return { valid: errors.length === 0, errors, warnings };
8867
- }
8868
- function isUPGDocument(doc) {
8869
- return validateUPGDocument(doc).valid;
8870
- }
8871
-
8872
8670
  // src/properties/property-schema.ts
8873
8671
  var UPG_PROPERTY_SCHEMA = {
8874
8672
  // A11yAnnotationProperties: Accessibility annotation.
@@ -12022,9 +11820,296 @@ var UPG_PROPERTY_SCHEMA = {
12022
11820
  archived_at: { type: "string", description: "ISO timestamp archived. Pairs with `archived === true`." },
12023
11821
  icon: { type: "string", description: "Display icon (emoji or icon name)" }
12024
11822
  }
12025
- };
12026
- function getPropertySchema(entityType) {
12027
- return UPG_PROPERTY_SCHEMA[entityType];
11823
+ };
11824
+ function getPropertySchema(entityType) {
11825
+ return UPG_PROPERTY_SCHEMA[entityType];
11826
+ }
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;
12028
12113
  }
12029
12114
 
12030
12115
  // src/frameworks/canonical.ts
@@ -13263,7 +13348,7 @@ var UPG_FRAMEWORKS = [
13263
13348
  ],
13264
13349
  "name": "RICE Scoring",
13265
13350
  "version": "1.0.0",
13266
- "description": "Score features and opportunities by Reach, Impact, Confidence, and Effort to produce a ranked priority list.",
13351
+ "description": "Score features, opportunities, and needs by Reach, Impact, Confidence, and Effort to produce a ranked priority list.",
13267
13352
  "category": "prioritization",
13268
13353
  "origin": {
13269
13354
  "type": "practitioner",
@@ -13281,7 +13366,7 @@ var UPG_FRAMEWORKS = [
13281
13366
  {
13282
13367
  "label": "Items to score",
13283
13368
  "entityTypeId": "feature",
13284
- "description": "Features, opportunities, or solutions being evaluated"
13369
+ "description": "Features, opportunities, or needs being evaluated"
13285
13370
  },
13286
13371
  {
13287
13372
  "label": "Opportunities to score",
@@ -13447,7 +13532,60 @@ var UPG_FRAMEWORKS = [
13447
13532
  "label": "RICE Score",
13448
13533
  "format": "number"
13449
13534
  }
13450
- ]
13535
+ ],
13536
+ "scoring_lens": {
13537
+ "applies_to": [
13538
+ "feature",
13539
+ "opportunity",
13540
+ "need"
13541
+ ],
13542
+ "inputs": [
13543
+ {
13544
+ "property": "reach",
13545
+ "type": "assessment",
13546
+ "scale_id": "reach_5",
13547
+ "required": true,
13548
+ "scope": "framework",
13549
+ "label": "Reach",
13550
+ "description": "How many users will this impact per quarter?"
13551
+ },
13552
+ {
13553
+ "property": "impact",
13554
+ "type": "assessment",
13555
+ "scale_id": "impact_5",
13556
+ "required": true,
13557
+ "scope": "framework",
13558
+ "label": "Impact",
13559
+ "description": "How much will this impact each user, on the impact scale?"
13560
+ },
13561
+ {
13562
+ "property": "confidence",
13563
+ "type": "assessment",
13564
+ "scale_id": "confidence_5",
13565
+ "required": true,
13566
+ "scope": "framework",
13567
+ "label": "Confidence",
13568
+ "description": "How confident are you in the reach, impact, and effort estimates?"
13569
+ },
13570
+ {
13571
+ "property": "effort",
13572
+ "type": "assessment",
13573
+ "scale_id": "effort_5",
13574
+ "required": true,
13575
+ "scope": "framework",
13576
+ "label": "Effort",
13577
+ "description": "How much work is required to build and ship this, on the effort scale?"
13578
+ }
13579
+ ],
13580
+ "computed": [
13581
+ {
13582
+ "property": "rice_score",
13583
+ "expression": "(reach * impact * confidence) / effort",
13584
+ "label": "RICE Score",
13585
+ "format": "number"
13586
+ }
13587
+ ]
13588
+ }
13451
13589
  },
13452
13590
  "structure": {
13453
13591
  "pattern": "table"
@@ -15572,7 +15710,7 @@ var UPG_FRAMEWORKS = [
15572
15710
  "entity_types": [
15573
15711
  {
15574
15712
  "type": "team",
15575
- "role": "scored_item"
15713
+ "role": "item"
15576
15714
  },
15577
15715
  {
15578
15716
  "type": "retrospective",
@@ -15966,7 +16104,48 @@ var UPG_FRAMEWORKS = [
15966
16104
  "label": "ICE Score",
15967
16105
  "format": "number"
15968
16106
  }
15969
- ]
16107
+ ],
16108
+ "scoring_lens": {
16109
+ "applies_to": [
16110
+ "feature",
16111
+ "opportunity",
16112
+ "need"
16113
+ ],
16114
+ "inputs": [
16115
+ {
16116
+ "property": "impact",
16117
+ "type": "number",
16118
+ "required": true,
16119
+ "scope": "framework",
16120
+ "label": "Impact",
16121
+ "description": "Expected impact on the target metric (1-10)"
16122
+ },
16123
+ {
16124
+ "property": "confidence",
16125
+ "type": "number",
16126
+ "required": true,
16127
+ "scope": "framework",
16128
+ "label": "Confidence",
16129
+ "description": "Confidence in the impact estimate (1-10)"
16130
+ },
16131
+ {
16132
+ "property": "ease",
16133
+ "type": "number",
16134
+ "required": true,
16135
+ "scope": "framework",
16136
+ "label": "Ease",
16137
+ "description": "Ease of implementation (1-10)"
16138
+ }
16139
+ ],
16140
+ "computed": [
16141
+ {
16142
+ "property": "ice_score",
16143
+ "expression": "impact * confidence * ease",
16144
+ "label": "ICE Score",
16145
+ "format": "number"
16146
+ }
16147
+ ]
16148
+ }
15970
16149
  },
15971
16150
  "structure": {
15972
16151
  "pattern": "table"
@@ -16175,7 +16354,55 @@ var UPG_FRAMEWORKS = [
16175
16354
  "label": "WSJF Score",
16176
16355
  "format": "number"
16177
16356
  }
16178
- ]
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
+ }
16179
16406
  },
16180
16407
  "structure": {
16181
16408
  "pattern": "table"
@@ -16248,7 +16475,7 @@ var UPG_FRAMEWORKS = [
16248
16475
  ],
16249
16476
  "name": "Cost of Delay",
16250
16477
  "version": "1.0.0",
16251
- "description": "Quantify the economic cost of not shipping a feature to drive priority decisions. Combines urgency with value.",
16478
+ "description": "Quantify the economic cost of not shipping a feature or opportunity to drive priority decisions. Combines urgency with value.",
16252
16479
  "category": "prioritization",
16253
16480
  "origin": {
16254
16481
  "type": "practitioner",
@@ -16364,7 +16591,39 @@ var UPG_FRAMEWORKS = [
16364
16591
  "label": "WSJF Score",
16365
16592
  "format": "number"
16366
16593
  }
16367
- ]
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
+ }
16368
16627
  },
16369
16628
  "structure": {
16370
16629
  "pattern": "table"
@@ -25265,7 +25524,7 @@ function isCanonical(text) {
25265
25524
  }
25266
25525
 
25267
25526
  // src/index.ts
25268
- var UPG_VERSION = "0.8.6";
25527
+ var UPG_VERSION = "0.8.7";
25269
25528
  var UPG_FORMAT_VERSION = "0.4.0";
25270
25529
  var MARKDOWN_FORMAT_VERSION = "0.1";
25271
25530
  var UPG_TYPES = getTypes();
@@ -25334,6 +25593,7 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
25334
25593
  BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK,
25335
25594
  BUSINESS_GTM_GROWTH_PLAYBOOK,
25336
25595
  BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK,
25596
+ CONTENT_DEPTH_WARNING_RULES,
25337
25597
  DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK,
25338
25598
  ENGINEERING_PLATFORM_PLAYBOOK,
25339
25599
  EXPERIENCE_DESIGN_BRAND_PLAYBOOK,