@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/CHANGELOG.md +12 -0
- package/dist/index.cjs +475 -215
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -3
- package/dist/index.d.ts +71 -3
- package/dist/index.js +474 -215
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -8475,209 +8475,6 @@ function rotateSlug(node, next) {
|
|
|
8475
8475
|
return node;
|
|
8476
8476
|
}
|
|
8477
8477
|
|
|
8478
|
-
// src/grammar/validate.ts
|
|
8479
|
-
var crossProductEdgeTypeSet = new Set(UPG_CROSS_EDGE_TYPES);
|
|
8480
|
-
function validateUPGDocument(doc) {
|
|
8481
|
-
const errors = [];
|
|
8482
|
-
const warnings = [];
|
|
8483
|
-
if (!doc || typeof doc !== "object") {
|
|
8484
|
-
return { valid: false, errors: [{ path: "$", message: "Document must be an object" }], warnings };
|
|
8485
|
-
}
|
|
8486
|
-
const d = doc;
|
|
8487
|
-
if (!d.upg_version || typeof d.upg_version !== "string") {
|
|
8488
|
-
errors.push({ path: "$.upg_version", message: "upg_version is required and must be a string" });
|
|
8489
|
-
}
|
|
8490
|
-
if (!d.exported_at || typeof d.exported_at !== "string") {
|
|
8491
|
-
errors.push({ path: "$.exported_at", message: "exported_at is required and must be an ISO 8601 string" });
|
|
8492
|
-
}
|
|
8493
|
-
if (!d.source || typeof d.source !== "object") {
|
|
8494
|
-
errors.push({ path: "$.source", message: "source is required and must be an object" });
|
|
8495
|
-
} else {
|
|
8496
|
-
const source = d.source;
|
|
8497
|
-
if (!source.tool || typeof source.tool !== "string") {
|
|
8498
|
-
errors.push({ path: "$.source.tool", message: "source.tool is required and must be a string" });
|
|
8499
|
-
}
|
|
8500
|
-
}
|
|
8501
|
-
if (!d.product || typeof d.product !== "object") {
|
|
8502
|
-
errors.push({ path: "$.product", message: "product is required and must be an object" });
|
|
8503
|
-
} else {
|
|
8504
|
-
const product = d.product;
|
|
8505
|
-
if (!product.id || typeof product.id !== "string") {
|
|
8506
|
-
errors.push({ path: "$.product.id", message: "product.id is required and must be a string" });
|
|
8507
|
-
}
|
|
8508
|
-
if (!product.title || typeof product.title !== "string") {
|
|
8509
|
-
errors.push({ path: "$.product.title", message: "product.title is required and must be a string" });
|
|
8510
|
-
}
|
|
8511
|
-
}
|
|
8512
|
-
const allKnownTypes = new Set(getTypes());
|
|
8513
|
-
const allKnownEdgeTypes = new Set(Object.keys(UPG_EDGE_CATALOG));
|
|
8514
|
-
if (!Array.isArray(d.nodes)) {
|
|
8515
|
-
errors.push({ path: "$.nodes", message: "nodes is required and must be an array" });
|
|
8516
|
-
} else {
|
|
8517
|
-
const nodeIds = /* @__PURE__ */ new Set();
|
|
8518
|
-
const nodesById = /* @__PURE__ */ new Map();
|
|
8519
|
-
d.nodes.forEach((node, i) => {
|
|
8520
|
-
const path = `$.nodes[${i}]`;
|
|
8521
|
-
if (!node || typeof node !== "object") {
|
|
8522
|
-
errors.push({ path, message: "Each node must be an object" });
|
|
8523
|
-
return;
|
|
8524
|
-
}
|
|
8525
|
-
const n = node;
|
|
8526
|
-
if (!n.id || typeof n.id !== "string") {
|
|
8527
|
-
errors.push({ path: `${path}.id`, message: "Node id is required and must be a string" });
|
|
8528
|
-
} else {
|
|
8529
|
-
if (nodeIds.has(n.id)) {
|
|
8530
|
-
errors.push({ path: `${path}.id`, message: `Duplicate node id: ${n.id}` });
|
|
8531
|
-
}
|
|
8532
|
-
nodeIds.add(n.id);
|
|
8533
|
-
nodesById.set(n.id, n);
|
|
8534
|
-
}
|
|
8535
|
-
if (!n.type || typeof n.type !== "string") {
|
|
8536
|
-
errors.push({ path: `${path}.type`, message: "Node type is required and must be a string" });
|
|
8537
|
-
} else if (!allKnownTypes.has(n.type)) {
|
|
8538
|
-
warnings.push({ path: `${path}.type`, message: `Unknown UPG type: "${n.type}". Node will be preserved with its original type.` });
|
|
8539
|
-
}
|
|
8540
|
-
if (!n.title || typeof n.title !== "string") {
|
|
8541
|
-
errors.push({ path: `${path}.title`, message: "Node title is required and must be a string" });
|
|
8542
|
-
}
|
|
8543
|
-
});
|
|
8544
|
-
if (!Array.isArray(d.edges)) {
|
|
8545
|
-
errors.push({ path: "$.edges", message: "edges is required and must be an array" });
|
|
8546
|
-
} else {
|
|
8547
|
-
d.edges.forEach((edge, i) => {
|
|
8548
|
-
const path = `$.edges[${i}]`;
|
|
8549
|
-
if (!edge || typeof edge !== "object") {
|
|
8550
|
-
errors.push({ path, message: "Each edge must be an object" });
|
|
8551
|
-
return;
|
|
8552
|
-
}
|
|
8553
|
-
const e = edge;
|
|
8554
|
-
if (!e.id || typeof e.id !== "string") {
|
|
8555
|
-
errors.push({ path: `${path}.id`, message: "Edge id is required and must be a string" });
|
|
8556
|
-
}
|
|
8557
|
-
if (typeof e.type === "string" && crossProductEdgeTypeSet.has(e.type)) {
|
|
8558
|
-
errors.push({
|
|
8559
|
-
path: `${path}.type`,
|
|
8560
|
-
message: `Cross-product edge type "${e.type}" must live in portfolio.cross_edges[], not product edges[].`
|
|
8561
|
-
});
|
|
8562
|
-
return;
|
|
8563
|
-
}
|
|
8564
|
-
if (!e.source || typeof e.source !== "string") {
|
|
8565
|
-
errors.push({ path: `${path}.source`, message: "Edge source is required and must be a string" });
|
|
8566
|
-
} else if (!nodeIds.has(e.source)) {
|
|
8567
|
-
errors.push({ path: `${path}.source`, message: `Edge source references unknown node id: ${e.source}` });
|
|
8568
|
-
}
|
|
8569
|
-
if (!e.target || typeof e.target !== "string") {
|
|
8570
|
-
errors.push({ path: `${path}.target`, message: "Edge target is required and must be a string" });
|
|
8571
|
-
} else if (!nodeIds.has(e.target)) {
|
|
8572
|
-
errors.push({ path: `${path}.target`, message: `Edge target references unknown node id: ${e.target}` });
|
|
8573
|
-
}
|
|
8574
|
-
if (!e.type || typeof e.type !== "string") {
|
|
8575
|
-
errors.push({ path: `${path}.type`, message: "Edge type is required and must be a string" });
|
|
8576
|
-
} else if (!allKnownEdgeTypes.has(e.type)) {
|
|
8577
|
-
warnings.push({
|
|
8578
|
-
path: `${path}.type`,
|
|
8579
|
-
message: `Unknown edge type: "${e.type}". Edge preserved but not in the edge registry.`
|
|
8580
|
-
});
|
|
8581
|
-
}
|
|
8582
|
-
});
|
|
8583
|
-
const valueToAxis = /* @__PURE__ */ new Map();
|
|
8584
|
-
const edgesArr = d.edges;
|
|
8585
|
-
for (const edge of edgesArr) {
|
|
8586
|
-
if (!edge || typeof edge !== "object") continue;
|
|
8587
|
-
const e = edge;
|
|
8588
|
-
if (e.type === "classification_axis_includes_classification_value" && typeof e.source === "string" && typeof e.target === "string") {
|
|
8589
|
-
valueToAxis.set(e.target, e.source);
|
|
8590
|
-
}
|
|
8591
|
-
}
|
|
8592
|
-
for (const [id, n] of nodesById.entries()) {
|
|
8593
|
-
if (n.type !== "classification_value") continue;
|
|
8594
|
-
if (valueToAxis.has(id)) continue;
|
|
8595
|
-
const pid = n.parent_id;
|
|
8596
|
-
if (typeof pid === "string") {
|
|
8597
|
-
const parent = nodesById.get(pid);
|
|
8598
|
-
if (parent && parent.type === "classification_axis") {
|
|
8599
|
-
valueToAxis.set(id, pid);
|
|
8600
|
-
}
|
|
8601
|
-
}
|
|
8602
|
-
}
|
|
8603
|
-
const axesPerAnalysis = /* @__PURE__ */ new Map();
|
|
8604
|
-
for (const edge of edgesArr) {
|
|
8605
|
-
if (!edge || typeof edge !== "object") continue;
|
|
8606
|
-
const e = edge;
|
|
8607
|
-
if (e.type === "competitive_analysis_dimensioned_by_classification_axis" && typeof e.source === "string") {
|
|
8608
|
-
axesPerAnalysis.set(e.source, (axesPerAnalysis.get(e.source) ?? 0) + 1);
|
|
8609
|
-
}
|
|
8610
|
-
}
|
|
8611
|
-
d.nodes.forEach((node, i) => {
|
|
8612
|
-
if (!node || typeof node !== "object") return;
|
|
8613
|
-
const n = node;
|
|
8614
|
-
if (n.type !== "competitive_analysis") return;
|
|
8615
|
-
const props = n.properties;
|
|
8616
|
-
if (!props || typeof props !== "object") return;
|
|
8617
|
-
const empty = props.empty_cells;
|
|
8618
|
-
if (!Array.isArray(empty)) return;
|
|
8619
|
-
const axisCount = axesPerAnalysis.get(n.id ?? "") ?? 0;
|
|
8620
|
-
if (axisCount !== 2) {
|
|
8621
|
-
warnings.push({
|
|
8622
|
-
path: `$.nodes[${i}].properties.empty_cells`,
|
|
8623
|
-
message: `empty_cells is meaningful only on a 2-axis competitive_analysis; this node is dimensioned by ${axisCount} classification_axis ${axisCount === 1 ? "child" : "children"}.`
|
|
8624
|
-
});
|
|
8625
|
-
}
|
|
8626
|
-
empty.forEach((cell, j) => {
|
|
8627
|
-
const cpath = `$.nodes[${i}].properties.empty_cells[${j}]`;
|
|
8628
|
-
if (!cell || typeof cell !== "object") {
|
|
8629
|
-
errors.push({ path: cpath, message: "empty_cells entry must be an object" });
|
|
8630
|
-
return;
|
|
8631
|
-
}
|
|
8632
|
-
const c = cell;
|
|
8633
|
-
const aRef = c.axis_a_value_ref;
|
|
8634
|
-
const bRef = c.axis_b_value_ref;
|
|
8635
|
-
if (typeof aRef !== "string") {
|
|
8636
|
-
errors.push({ path: `${cpath}.axis_a_value_ref`, message: "axis_a_value_ref is required and must be a string" });
|
|
8637
|
-
}
|
|
8638
|
-
if (typeof bRef !== "string") {
|
|
8639
|
-
errors.push({ path: `${cpath}.axis_b_value_ref`, message: "axis_b_value_ref is required and must be a string" });
|
|
8640
|
-
}
|
|
8641
|
-
if (typeof aRef !== "string" || typeof bRef !== "string") return;
|
|
8642
|
-
const aNode = nodesById.get(aRef);
|
|
8643
|
-
const bNode = nodesById.get(bRef);
|
|
8644
|
-
if (!aNode) {
|
|
8645
|
-
errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Unknown node id: ${aRef}` });
|
|
8646
|
-
} else if (aNode.type !== "classification_value") {
|
|
8647
|
-
errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Expected classification_value, found "${String(aNode.type)}" at ${aRef}` });
|
|
8648
|
-
}
|
|
8649
|
-
if (!bNode) {
|
|
8650
|
-
errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Unknown node id: ${bRef}` });
|
|
8651
|
-
} else if (bNode.type !== "classification_value") {
|
|
8652
|
-
errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Expected classification_value, found "${String(bNode.type)}" at ${bRef}` });
|
|
8653
|
-
}
|
|
8654
|
-
if (aNode && bNode && aNode.type === "classification_value" && bNode.type === "classification_value") {
|
|
8655
|
-
const aAxis = valueToAxis.get(aRef);
|
|
8656
|
-
const bAxis = valueToAxis.get(bRef);
|
|
8657
|
-
if (aAxis && bAxis && aAxis === bAxis) {
|
|
8658
|
-
errors.push({
|
|
8659
|
-
path: cpath,
|
|
8660
|
-
message: `empty_cells references two classification_values from the same axis (${aAxis}); axis_a and axis_b must come from distinct classification_axis parents.`
|
|
8661
|
-
});
|
|
8662
|
-
}
|
|
8663
|
-
}
|
|
8664
|
-
const kind = c.rationale_kind;
|
|
8665
|
-
if (kind !== void 0 && kind !== "structural" && kind !== "ideological" && kind !== "opportunity") {
|
|
8666
|
-
errors.push({
|
|
8667
|
-
path: `${cpath}.rationale_kind`,
|
|
8668
|
-
message: `rationale_kind must be one of 'structural' | 'ideological' | 'opportunity'; got "${String(kind)}".`
|
|
8669
|
-
});
|
|
8670
|
-
}
|
|
8671
|
-
});
|
|
8672
|
-
});
|
|
8673
|
-
}
|
|
8674
|
-
}
|
|
8675
|
-
return { valid: errors.length === 0, errors, warnings };
|
|
8676
|
-
}
|
|
8677
|
-
function isUPGDocument(doc) {
|
|
8678
|
-
return validateUPGDocument(doc).valid;
|
|
8679
|
-
}
|
|
8680
|
-
|
|
8681
8478
|
// src/properties/property-schema.ts
|
|
8682
8479
|
var UPG_PROPERTY_SCHEMA = {
|
|
8683
8480
|
// A11yAnnotationProperties: Accessibility annotation.
|
|
@@ -11831,9 +11628,296 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11831
11628
|
archived_at: { type: "string", description: "ISO timestamp archived. Pairs with `archived === true`." },
|
|
11832
11629
|
icon: { type: "string", description: "Display icon (emoji or icon name)" }
|
|
11833
11630
|
}
|
|
11834
|
-
};
|
|
11835
|
-
function getPropertySchema(entityType) {
|
|
11836
|
-
return UPG_PROPERTY_SCHEMA[entityType];
|
|
11631
|
+
};
|
|
11632
|
+
function getPropertySchema(entityType) {
|
|
11633
|
+
return UPG_PROPERTY_SCHEMA[entityType];
|
|
11634
|
+
}
|
|
11635
|
+
|
|
11636
|
+
// src/grammar/validate.ts
|
|
11637
|
+
var crossProductEdgeTypeSet = new Set(UPG_CROSS_EDGE_TYPES);
|
|
11638
|
+
var CONTENT_DEPTH_WARNING_RULES = /* @__PURE__ */ new Set([
|
|
11639
|
+
"property-type",
|
|
11640
|
+
"property-enum",
|
|
11641
|
+
"self-loop"
|
|
11642
|
+
]);
|
|
11643
|
+
function validateUPGDocument(doc) {
|
|
11644
|
+
const errors = [];
|
|
11645
|
+
const warnings = [];
|
|
11646
|
+
if (!doc || typeof doc !== "object") {
|
|
11647
|
+
return { valid: false, errors: [{ path: "$", message: "Document must be an object" }], warnings };
|
|
11648
|
+
}
|
|
11649
|
+
const d = doc;
|
|
11650
|
+
if (!d.upg_version || typeof d.upg_version !== "string") {
|
|
11651
|
+
errors.push({ path: "$.upg_version", message: "upg_version is required and must be a string" });
|
|
11652
|
+
}
|
|
11653
|
+
if (!d.exported_at || typeof d.exported_at !== "string") {
|
|
11654
|
+
errors.push({ path: "$.exported_at", message: "exported_at is required and must be an ISO 8601 string" });
|
|
11655
|
+
}
|
|
11656
|
+
if (!d.source || typeof d.source !== "object") {
|
|
11657
|
+
errors.push({ path: "$.source", message: "source is required and must be an object" });
|
|
11658
|
+
} else {
|
|
11659
|
+
const source = d.source;
|
|
11660
|
+
if (!source.tool || typeof source.tool !== "string") {
|
|
11661
|
+
errors.push({ path: "$.source.tool", message: "source.tool is required and must be a string" });
|
|
11662
|
+
}
|
|
11663
|
+
}
|
|
11664
|
+
if (!d.product || typeof d.product !== "object") {
|
|
11665
|
+
errors.push({ path: "$.product", message: "product is required and must be an object" });
|
|
11666
|
+
} else {
|
|
11667
|
+
const product = d.product;
|
|
11668
|
+
if (!product.id || typeof product.id !== "string") {
|
|
11669
|
+
errors.push({ path: "$.product.id", message: "product.id is required and must be a string" });
|
|
11670
|
+
}
|
|
11671
|
+
if (!product.title || typeof product.title !== "string") {
|
|
11672
|
+
errors.push({ path: "$.product.title", message: "product.title is required and must be a string" });
|
|
11673
|
+
}
|
|
11674
|
+
}
|
|
11675
|
+
const allKnownTypes = new Set(getTypes());
|
|
11676
|
+
const allKnownEdgeTypes = new Set(Object.keys(UPG_EDGE_CATALOG));
|
|
11677
|
+
if (!Array.isArray(d.nodes)) {
|
|
11678
|
+
errors.push({ path: "$.nodes", message: "nodes is required and must be an array" });
|
|
11679
|
+
} else {
|
|
11680
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
11681
|
+
const nodesById = /* @__PURE__ */ new Map();
|
|
11682
|
+
d.nodes.forEach((node, i) => {
|
|
11683
|
+
const path = `$.nodes[${i}]`;
|
|
11684
|
+
if (!node || typeof node !== "object") {
|
|
11685
|
+
errors.push({ path, message: "Each node must be an object" });
|
|
11686
|
+
return;
|
|
11687
|
+
}
|
|
11688
|
+
const n = node;
|
|
11689
|
+
if (!n.id || typeof n.id !== "string") {
|
|
11690
|
+
errors.push({ path: `${path}.id`, message: "Node id is required and must be a string" });
|
|
11691
|
+
} else {
|
|
11692
|
+
if (nodeIds.has(n.id)) {
|
|
11693
|
+
errors.push({ path: `${path}.id`, message: `Duplicate node id: ${n.id}` });
|
|
11694
|
+
}
|
|
11695
|
+
nodeIds.add(n.id);
|
|
11696
|
+
nodesById.set(n.id, n);
|
|
11697
|
+
}
|
|
11698
|
+
if (!n.type || typeof n.type !== "string") {
|
|
11699
|
+
errors.push({ path: `${path}.type`, message: "Node type is required and must be a string" });
|
|
11700
|
+
} else if (!allKnownTypes.has(n.type)) {
|
|
11701
|
+
warnings.push({ path: `${path}.type`, message: `Unknown UPG type: "${n.type}". Node will be preserved with its original type.` });
|
|
11702
|
+
}
|
|
11703
|
+
if (!n.title || typeof n.title !== "string") {
|
|
11704
|
+
errors.push({ path: `${path}.title`, message: "Node title is required and must be a string" });
|
|
11705
|
+
} else if (n.title.trim().length === 0) {
|
|
11706
|
+
errors.push({ path: `${path}.title`, message: "Node title must not be blank (whitespace-only)" });
|
|
11707
|
+
}
|
|
11708
|
+
if (typeof n.type === "string") {
|
|
11709
|
+
validateNodeProperties(n, `${path}`, warnings);
|
|
11710
|
+
}
|
|
11711
|
+
});
|
|
11712
|
+
if (!Array.isArray(d.edges)) {
|
|
11713
|
+
errors.push({ path: "$.edges", message: "edges is required and must be an array" });
|
|
11714
|
+
} else {
|
|
11715
|
+
d.edges.forEach((edge, i) => {
|
|
11716
|
+
const path = `$.edges[${i}]`;
|
|
11717
|
+
if (!edge || typeof edge !== "object") {
|
|
11718
|
+
errors.push({ path, message: "Each edge must be an object" });
|
|
11719
|
+
return;
|
|
11720
|
+
}
|
|
11721
|
+
const e = edge;
|
|
11722
|
+
if (!e.id || typeof e.id !== "string") {
|
|
11723
|
+
errors.push({ path: `${path}.id`, message: "Edge id is required and must be a string" });
|
|
11724
|
+
}
|
|
11725
|
+
if (typeof e.type === "string" && crossProductEdgeTypeSet.has(e.type)) {
|
|
11726
|
+
errors.push({
|
|
11727
|
+
path: `${path}.type`,
|
|
11728
|
+
message: `Cross-product edge type "${e.type}" must live in portfolio.cross_edges[], not product edges[].`
|
|
11729
|
+
});
|
|
11730
|
+
return;
|
|
11731
|
+
}
|
|
11732
|
+
if (!e.source || typeof e.source !== "string") {
|
|
11733
|
+
errors.push({ path: `${path}.source`, message: "Edge source is required and must be a string" });
|
|
11734
|
+
} else if (!nodeIds.has(e.source)) {
|
|
11735
|
+
errors.push({ path: `${path}.source`, message: `Edge source references unknown node id: ${e.source}` });
|
|
11736
|
+
}
|
|
11737
|
+
if (!e.target || typeof e.target !== "string") {
|
|
11738
|
+
errors.push({ path: `${path}.target`, message: "Edge target is required and must be a string" });
|
|
11739
|
+
} else if (!nodeIds.has(e.target)) {
|
|
11740
|
+
errors.push({ path: `${path}.target`, message: `Edge target references unknown node id: ${e.target}` });
|
|
11741
|
+
}
|
|
11742
|
+
if (typeof e.source === "string" && typeof e.target === "string" && e.source === e.target) {
|
|
11743
|
+
warnings.push({
|
|
11744
|
+
path: `${path}`,
|
|
11745
|
+
message: `Self-loop edge: source and target are the same node ("${e.source}"). A node related to itself is almost always an error.`,
|
|
11746
|
+
rule: "self-loop"
|
|
11747
|
+
});
|
|
11748
|
+
}
|
|
11749
|
+
if (!e.type || typeof e.type !== "string") {
|
|
11750
|
+
errors.push({ path: `${path}.type`, message: "Edge type is required and must be a string" });
|
|
11751
|
+
} else if (!allKnownEdgeTypes.has(e.type)) {
|
|
11752
|
+
warnings.push({
|
|
11753
|
+
path: `${path}.type`,
|
|
11754
|
+
message: `Unknown edge type: "${e.type}". Edge preserved but not in the edge registry.`
|
|
11755
|
+
});
|
|
11756
|
+
}
|
|
11757
|
+
});
|
|
11758
|
+
const valueToAxis = /* @__PURE__ */ new Map();
|
|
11759
|
+
const edgesArr = d.edges;
|
|
11760
|
+
for (const edge of edgesArr) {
|
|
11761
|
+
if (!edge || typeof edge !== "object") continue;
|
|
11762
|
+
const e = edge;
|
|
11763
|
+
if (e.type === "classification_axis_includes_classification_value" && typeof e.source === "string" && typeof e.target === "string") {
|
|
11764
|
+
valueToAxis.set(e.target, e.source);
|
|
11765
|
+
}
|
|
11766
|
+
}
|
|
11767
|
+
for (const [id, n] of nodesById.entries()) {
|
|
11768
|
+
if (n.type !== "classification_value") continue;
|
|
11769
|
+
if (valueToAxis.has(id)) continue;
|
|
11770
|
+
const pid = n.parent_id;
|
|
11771
|
+
if (typeof pid === "string") {
|
|
11772
|
+
const parent = nodesById.get(pid);
|
|
11773
|
+
if (parent && parent.type === "classification_axis") {
|
|
11774
|
+
valueToAxis.set(id, pid);
|
|
11775
|
+
}
|
|
11776
|
+
}
|
|
11777
|
+
}
|
|
11778
|
+
const axesPerAnalysis = /* @__PURE__ */ new Map();
|
|
11779
|
+
for (const edge of edgesArr) {
|
|
11780
|
+
if (!edge || typeof edge !== "object") continue;
|
|
11781
|
+
const e = edge;
|
|
11782
|
+
if (e.type === "competitive_analysis_dimensioned_by_classification_axis" && typeof e.source === "string") {
|
|
11783
|
+
axesPerAnalysis.set(e.source, (axesPerAnalysis.get(e.source) ?? 0) + 1);
|
|
11784
|
+
}
|
|
11785
|
+
}
|
|
11786
|
+
d.nodes.forEach((node, i) => {
|
|
11787
|
+
if (!node || typeof node !== "object") return;
|
|
11788
|
+
const n = node;
|
|
11789
|
+
if (n.type !== "competitive_analysis") return;
|
|
11790
|
+
const props = n.properties;
|
|
11791
|
+
if (!props || typeof props !== "object") return;
|
|
11792
|
+
const empty = props.empty_cells;
|
|
11793
|
+
if (!Array.isArray(empty)) return;
|
|
11794
|
+
const axisCount = axesPerAnalysis.get(n.id ?? "") ?? 0;
|
|
11795
|
+
if (axisCount !== 2) {
|
|
11796
|
+
warnings.push({
|
|
11797
|
+
path: `$.nodes[${i}].properties.empty_cells`,
|
|
11798
|
+
message: `empty_cells is meaningful only on a 2-axis competitive_analysis; this node is dimensioned by ${axisCount} classification_axis ${axisCount === 1 ? "child" : "children"}.`
|
|
11799
|
+
});
|
|
11800
|
+
}
|
|
11801
|
+
empty.forEach((cell, j) => {
|
|
11802
|
+
const cpath = `$.nodes[${i}].properties.empty_cells[${j}]`;
|
|
11803
|
+
if (!cell || typeof cell !== "object") {
|
|
11804
|
+
errors.push({ path: cpath, message: "empty_cells entry must be an object" });
|
|
11805
|
+
return;
|
|
11806
|
+
}
|
|
11807
|
+
const c = cell;
|
|
11808
|
+
const aRef = c.axis_a_value_ref;
|
|
11809
|
+
const bRef = c.axis_b_value_ref;
|
|
11810
|
+
if (typeof aRef !== "string") {
|
|
11811
|
+
errors.push({ path: `${cpath}.axis_a_value_ref`, message: "axis_a_value_ref is required and must be a string" });
|
|
11812
|
+
}
|
|
11813
|
+
if (typeof bRef !== "string") {
|
|
11814
|
+
errors.push({ path: `${cpath}.axis_b_value_ref`, message: "axis_b_value_ref is required and must be a string" });
|
|
11815
|
+
}
|
|
11816
|
+
if (typeof aRef !== "string" || typeof bRef !== "string") return;
|
|
11817
|
+
const aNode = nodesById.get(aRef);
|
|
11818
|
+
const bNode = nodesById.get(bRef);
|
|
11819
|
+
if (!aNode) {
|
|
11820
|
+
errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Unknown node id: ${aRef}` });
|
|
11821
|
+
} else if (aNode.type !== "classification_value") {
|
|
11822
|
+
errors.push({ path: `${cpath}.axis_a_value_ref`, message: `Expected classification_value, found "${String(aNode.type)}" at ${aRef}` });
|
|
11823
|
+
}
|
|
11824
|
+
if (!bNode) {
|
|
11825
|
+
errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Unknown node id: ${bRef}` });
|
|
11826
|
+
} else if (bNode.type !== "classification_value") {
|
|
11827
|
+
errors.push({ path: `${cpath}.axis_b_value_ref`, message: `Expected classification_value, found "${String(bNode.type)}" at ${bRef}` });
|
|
11828
|
+
}
|
|
11829
|
+
if (aNode && bNode && aNode.type === "classification_value" && bNode.type === "classification_value") {
|
|
11830
|
+
const aAxis = valueToAxis.get(aRef);
|
|
11831
|
+
const bAxis = valueToAxis.get(bRef);
|
|
11832
|
+
if (aAxis && bAxis && aAxis === bAxis) {
|
|
11833
|
+
errors.push({
|
|
11834
|
+
path: cpath,
|
|
11835
|
+
message: `empty_cells references two classification_values from the same axis (${aAxis}); axis_a and axis_b must come from distinct classification_axis parents.`
|
|
11836
|
+
});
|
|
11837
|
+
}
|
|
11838
|
+
}
|
|
11839
|
+
const kind = c.rationale_kind;
|
|
11840
|
+
if (kind !== void 0 && kind !== "structural" && kind !== "ideological" && kind !== "opportunity") {
|
|
11841
|
+
errors.push({
|
|
11842
|
+
path: `${cpath}.rationale_kind`,
|
|
11843
|
+
message: `rationale_kind must be one of 'structural' | 'ideological' | 'opportunity'; got "${String(kind)}".`
|
|
11844
|
+
});
|
|
11845
|
+
}
|
|
11846
|
+
});
|
|
11847
|
+
});
|
|
11848
|
+
}
|
|
11849
|
+
}
|
|
11850
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
11851
|
+
}
|
|
11852
|
+
function runtimeKind(val) {
|
|
11853
|
+
if (val === null || val === void 0) return null;
|
|
11854
|
+
if (Array.isArray(val)) return "string[]";
|
|
11855
|
+
switch (typeof val) {
|
|
11856
|
+
case "string":
|
|
11857
|
+
return "string";
|
|
11858
|
+
case "number":
|
|
11859
|
+
return "number";
|
|
11860
|
+
case "boolean":
|
|
11861
|
+
return "boolean";
|
|
11862
|
+
case "object":
|
|
11863
|
+
return "object";
|
|
11864
|
+
default:
|
|
11865
|
+
return null;
|
|
11866
|
+
}
|
|
11867
|
+
}
|
|
11868
|
+
function valueMatchesType(defType, val) {
|
|
11869
|
+
const kind = runtimeKind(val);
|
|
11870
|
+
if (kind === null) return true;
|
|
11871
|
+
switch (defType) {
|
|
11872
|
+
case "string":
|
|
11873
|
+
return kind === "string";
|
|
11874
|
+
case "number":
|
|
11875
|
+
return kind === "number";
|
|
11876
|
+
case "boolean":
|
|
11877
|
+
return kind === "boolean";
|
|
11878
|
+
case "string[]":
|
|
11879
|
+
return kind === "string[]";
|
|
11880
|
+
case "object":
|
|
11881
|
+
case "assessment":
|
|
11882
|
+
return kind === "object";
|
|
11883
|
+
default:
|
|
11884
|
+
return true;
|
|
11885
|
+
}
|
|
11886
|
+
}
|
|
11887
|
+
function describeKind(val) {
|
|
11888
|
+
if (Array.isArray(val)) return "array";
|
|
11889
|
+
return typeof val;
|
|
11890
|
+
}
|
|
11891
|
+
function validateNodeProperties(node, basePath, warnings) {
|
|
11892
|
+
const type = node.type;
|
|
11893
|
+
if (typeof type !== "string") return;
|
|
11894
|
+
const schema = getPropertySchema(type);
|
|
11895
|
+
if (!schema) return;
|
|
11896
|
+
const props = node.properties;
|
|
11897
|
+
if (!props || typeof props !== "object" || Array.isArray(props)) return;
|
|
11898
|
+
for (const [key, value] of Object.entries(props)) {
|
|
11899
|
+
const def = schema[key];
|
|
11900
|
+
if (!def) continue;
|
|
11901
|
+
if (value === null || value === void 0) continue;
|
|
11902
|
+
if (!valueMatchesType(def.type, value)) {
|
|
11903
|
+
warnings.push({
|
|
11904
|
+
path: `${basePath}.properties.${key}`,
|
|
11905
|
+
message: `Property "${key}" on ${type} should be ${def.type}, got ${describeKind(value)}.`,
|
|
11906
|
+
rule: "property-type"
|
|
11907
|
+
});
|
|
11908
|
+
continue;
|
|
11909
|
+
}
|
|
11910
|
+
if (def.enum && typeof value === "string" && !def.enum.includes(value)) {
|
|
11911
|
+
warnings.push({
|
|
11912
|
+
path: `${basePath}.properties.${key}`,
|
|
11913
|
+
message: `Property "${key}" on ${type} has value "${value}" outside its allowed set: ${def.enum.join(", ")}.`,
|
|
11914
|
+
rule: "property-enum"
|
|
11915
|
+
});
|
|
11916
|
+
}
|
|
11917
|
+
}
|
|
11918
|
+
}
|
|
11919
|
+
function isUPGDocument(doc) {
|
|
11920
|
+
return validateUPGDocument(doc).valid;
|
|
11837
11921
|
}
|
|
11838
11922
|
|
|
11839
11923
|
// src/frameworks/canonical.ts
|
|
@@ -13072,7 +13156,7 @@ var UPG_FRAMEWORKS = [
|
|
|
13072
13156
|
],
|
|
13073
13157
|
"name": "RICE Scoring",
|
|
13074
13158
|
"version": "1.0.0",
|
|
13075
|
-
"description": "Score features and
|
|
13159
|
+
"description": "Score features, opportunities, and needs by Reach, Impact, Confidence, and Effort to produce a ranked priority list.",
|
|
13076
13160
|
"category": "prioritization",
|
|
13077
13161
|
"origin": {
|
|
13078
13162
|
"type": "practitioner",
|
|
@@ -13090,7 +13174,7 @@ var UPG_FRAMEWORKS = [
|
|
|
13090
13174
|
{
|
|
13091
13175
|
"label": "Items to score",
|
|
13092
13176
|
"entityTypeId": "feature",
|
|
13093
|
-
"description": "Features, opportunities, or
|
|
13177
|
+
"description": "Features, opportunities, or needs being evaluated"
|
|
13094
13178
|
},
|
|
13095
13179
|
{
|
|
13096
13180
|
"label": "Opportunities to score",
|
|
@@ -13256,7 +13340,60 @@ var UPG_FRAMEWORKS = [
|
|
|
13256
13340
|
"label": "RICE Score",
|
|
13257
13341
|
"format": "number"
|
|
13258
13342
|
}
|
|
13259
|
-
]
|
|
13343
|
+
],
|
|
13344
|
+
"scoring_lens": {
|
|
13345
|
+
"applies_to": [
|
|
13346
|
+
"feature",
|
|
13347
|
+
"opportunity",
|
|
13348
|
+
"need"
|
|
13349
|
+
],
|
|
13350
|
+
"inputs": [
|
|
13351
|
+
{
|
|
13352
|
+
"property": "reach",
|
|
13353
|
+
"type": "assessment",
|
|
13354
|
+
"scale_id": "reach_5",
|
|
13355
|
+
"required": true,
|
|
13356
|
+
"scope": "framework",
|
|
13357
|
+
"label": "Reach",
|
|
13358
|
+
"description": "How many users will this impact per quarter?"
|
|
13359
|
+
},
|
|
13360
|
+
{
|
|
13361
|
+
"property": "impact",
|
|
13362
|
+
"type": "assessment",
|
|
13363
|
+
"scale_id": "impact_5",
|
|
13364
|
+
"required": true,
|
|
13365
|
+
"scope": "framework",
|
|
13366
|
+
"label": "Impact",
|
|
13367
|
+
"description": "How much will this impact each user, on the impact scale?"
|
|
13368
|
+
},
|
|
13369
|
+
{
|
|
13370
|
+
"property": "confidence",
|
|
13371
|
+
"type": "assessment",
|
|
13372
|
+
"scale_id": "confidence_5",
|
|
13373
|
+
"required": true,
|
|
13374
|
+
"scope": "framework",
|
|
13375
|
+
"label": "Confidence",
|
|
13376
|
+
"description": "How confident are you in the reach, impact, and effort estimates?"
|
|
13377
|
+
},
|
|
13378
|
+
{
|
|
13379
|
+
"property": "effort",
|
|
13380
|
+
"type": "assessment",
|
|
13381
|
+
"scale_id": "effort_5",
|
|
13382
|
+
"required": true,
|
|
13383
|
+
"scope": "framework",
|
|
13384
|
+
"label": "Effort",
|
|
13385
|
+
"description": "How much work is required to build and ship this, on the effort scale?"
|
|
13386
|
+
}
|
|
13387
|
+
],
|
|
13388
|
+
"computed": [
|
|
13389
|
+
{
|
|
13390
|
+
"property": "rice_score",
|
|
13391
|
+
"expression": "(reach * impact * confidence) / effort",
|
|
13392
|
+
"label": "RICE Score",
|
|
13393
|
+
"format": "number"
|
|
13394
|
+
}
|
|
13395
|
+
]
|
|
13396
|
+
}
|
|
13260
13397
|
},
|
|
13261
13398
|
"structure": {
|
|
13262
13399
|
"pattern": "table"
|
|
@@ -15381,7 +15518,7 @@ var UPG_FRAMEWORKS = [
|
|
|
15381
15518
|
"entity_types": [
|
|
15382
15519
|
{
|
|
15383
15520
|
"type": "team",
|
|
15384
|
-
"role": "
|
|
15521
|
+
"role": "item"
|
|
15385
15522
|
},
|
|
15386
15523
|
{
|
|
15387
15524
|
"type": "retrospective",
|
|
@@ -15775,7 +15912,48 @@ var UPG_FRAMEWORKS = [
|
|
|
15775
15912
|
"label": "ICE Score",
|
|
15776
15913
|
"format": "number"
|
|
15777
15914
|
}
|
|
15778
|
-
]
|
|
15915
|
+
],
|
|
15916
|
+
"scoring_lens": {
|
|
15917
|
+
"applies_to": [
|
|
15918
|
+
"feature",
|
|
15919
|
+
"opportunity",
|
|
15920
|
+
"need"
|
|
15921
|
+
],
|
|
15922
|
+
"inputs": [
|
|
15923
|
+
{
|
|
15924
|
+
"property": "impact",
|
|
15925
|
+
"type": "number",
|
|
15926
|
+
"required": true,
|
|
15927
|
+
"scope": "framework",
|
|
15928
|
+
"label": "Impact",
|
|
15929
|
+
"description": "Expected impact on the target metric (1-10)"
|
|
15930
|
+
},
|
|
15931
|
+
{
|
|
15932
|
+
"property": "confidence",
|
|
15933
|
+
"type": "number",
|
|
15934
|
+
"required": true,
|
|
15935
|
+
"scope": "framework",
|
|
15936
|
+
"label": "Confidence",
|
|
15937
|
+
"description": "Confidence in the impact estimate (1-10)"
|
|
15938
|
+
},
|
|
15939
|
+
{
|
|
15940
|
+
"property": "ease",
|
|
15941
|
+
"type": "number",
|
|
15942
|
+
"required": true,
|
|
15943
|
+
"scope": "framework",
|
|
15944
|
+
"label": "Ease",
|
|
15945
|
+
"description": "Ease of implementation (1-10)"
|
|
15946
|
+
}
|
|
15947
|
+
],
|
|
15948
|
+
"computed": [
|
|
15949
|
+
{
|
|
15950
|
+
"property": "ice_score",
|
|
15951
|
+
"expression": "impact * confidence * ease",
|
|
15952
|
+
"label": "ICE Score",
|
|
15953
|
+
"format": "number"
|
|
15954
|
+
}
|
|
15955
|
+
]
|
|
15956
|
+
}
|
|
15779
15957
|
},
|
|
15780
15958
|
"structure": {
|
|
15781
15959
|
"pattern": "table"
|
|
@@ -15984,7 +16162,55 @@ var UPG_FRAMEWORKS = [
|
|
|
15984
16162
|
"label": "WSJF Score",
|
|
15985
16163
|
"format": "number"
|
|
15986
16164
|
}
|
|
15987
|
-
]
|
|
16165
|
+
],
|
|
16166
|
+
"scoring_lens": {
|
|
16167
|
+
"applies_to": [
|
|
16168
|
+
"feature",
|
|
16169
|
+
"opportunity"
|
|
16170
|
+
],
|
|
16171
|
+
"inputs": [
|
|
16172
|
+
{
|
|
16173
|
+
"property": "user_value",
|
|
16174
|
+
"type": "number",
|
|
16175
|
+
"required": true,
|
|
16176
|
+
"scope": "framework",
|
|
16177
|
+
"label": "User/Business Value",
|
|
16178
|
+
"description": "Relative value to users and the business if delivered"
|
|
16179
|
+
},
|
|
16180
|
+
{
|
|
16181
|
+
"property": "time_criticality",
|
|
16182
|
+
"type": "number",
|
|
16183
|
+
"required": true,
|
|
16184
|
+
"scope": "framework",
|
|
16185
|
+
"label": "Time Criticality",
|
|
16186
|
+
"description": "How much value decays if delivery is delayed (deadlines, competition, seasonal windows)"
|
|
16187
|
+
},
|
|
16188
|
+
{
|
|
16189
|
+
"property": "risk_reduction",
|
|
16190
|
+
"type": "number",
|
|
16191
|
+
"required": true,
|
|
16192
|
+
"scope": "framework",
|
|
16193
|
+
"label": "Risk Reduction / Opportunity Enablement",
|
|
16194
|
+
"description": "Value from reducing risk or enabling future opportunities"
|
|
16195
|
+
},
|
|
16196
|
+
{
|
|
16197
|
+
"property": "job_size",
|
|
16198
|
+
"type": "number",
|
|
16199
|
+
"required": true,
|
|
16200
|
+
"scope": "framework",
|
|
16201
|
+
"label": "Job Size",
|
|
16202
|
+
"description": "Estimated effort (story points, t-shirt size, or person-weeks)"
|
|
16203
|
+
}
|
|
16204
|
+
],
|
|
16205
|
+
"computed": [
|
|
16206
|
+
{
|
|
16207
|
+
"property": "wsjf_score",
|
|
16208
|
+
"expression": "(user_value + time_criticality + risk_reduction) / job_size",
|
|
16209
|
+
"label": "WSJF Score",
|
|
16210
|
+
"format": "number"
|
|
16211
|
+
}
|
|
16212
|
+
]
|
|
16213
|
+
}
|
|
15988
16214
|
},
|
|
15989
16215
|
"structure": {
|
|
15990
16216
|
"pattern": "table"
|
|
@@ -16057,7 +16283,7 @@ var UPG_FRAMEWORKS = [
|
|
|
16057
16283
|
],
|
|
16058
16284
|
"name": "Cost of Delay",
|
|
16059
16285
|
"version": "1.0.0",
|
|
16060
|
-
"description": "Quantify the economic cost of not shipping a feature to drive priority decisions. Combines urgency with value.",
|
|
16286
|
+
"description": "Quantify the economic cost of not shipping a feature or opportunity to drive priority decisions. Combines urgency with value.",
|
|
16061
16287
|
"category": "prioritization",
|
|
16062
16288
|
"origin": {
|
|
16063
16289
|
"type": "practitioner",
|
|
@@ -16173,7 +16399,39 @@ var UPG_FRAMEWORKS = [
|
|
|
16173
16399
|
"label": "WSJF Score",
|
|
16174
16400
|
"format": "number"
|
|
16175
16401
|
}
|
|
16176
|
-
]
|
|
16402
|
+
],
|
|
16403
|
+
"scoring_lens": {
|
|
16404
|
+
"applies_to": [
|
|
16405
|
+
"feature",
|
|
16406
|
+
"opportunity"
|
|
16407
|
+
],
|
|
16408
|
+
"inputs": [
|
|
16409
|
+
{
|
|
16410
|
+
"property": "cost_of_delay",
|
|
16411
|
+
"type": "number",
|
|
16412
|
+
"required": true,
|
|
16413
|
+
"scope": "framework",
|
|
16414
|
+
"label": "Cost of Delay",
|
|
16415
|
+
"description": "Weekly revenue impact of not shipping"
|
|
16416
|
+
},
|
|
16417
|
+
{
|
|
16418
|
+
"property": "job_size",
|
|
16419
|
+
"type": "number",
|
|
16420
|
+
"required": true,
|
|
16421
|
+
"scope": "framework",
|
|
16422
|
+
"label": "Job Size",
|
|
16423
|
+
"description": "Weeks of development effort"
|
|
16424
|
+
}
|
|
16425
|
+
],
|
|
16426
|
+
"computed": [
|
|
16427
|
+
{
|
|
16428
|
+
"property": "wsjf_score",
|
|
16429
|
+
"expression": "cost_of_delay / job_size",
|
|
16430
|
+
"label": "WSJF Score",
|
|
16431
|
+
"format": "number"
|
|
16432
|
+
}
|
|
16433
|
+
]
|
|
16434
|
+
}
|
|
16177
16435
|
},
|
|
16178
16436
|
"structure": {
|
|
16179
16437
|
"pattern": "table"
|
|
@@ -25074,7 +25332,7 @@ function isCanonical(text) {
|
|
|
25074
25332
|
}
|
|
25075
25333
|
|
|
25076
25334
|
// src/index.ts
|
|
25077
|
-
var UPG_VERSION = "0.8.
|
|
25335
|
+
var UPG_VERSION = "0.8.7";
|
|
25078
25336
|
var UPG_FORMAT_VERSION = "0.4.0";
|
|
25079
25337
|
var MARKDOWN_FORMAT_VERSION = "0.1";
|
|
25080
25338
|
var UPG_TYPES = getTypes();
|
|
@@ -25142,6 +25400,7 @@ export {
|
|
|
25142
25400
|
BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK,
|
|
25143
25401
|
BUSINESS_GTM_GROWTH_PLAYBOOK,
|
|
25144
25402
|
BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK,
|
|
25403
|
+
CONTENT_DEPTH_WARNING_RULES,
|
|
25145
25404
|
DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK,
|
|
25146
25405
|
ENGINEERING_PLATFORM_PLAYBOOK,
|
|
25147
25406
|
EXPERIENCE_DESIGN_BRAND_PLAYBOOK,
|