@unified-product-graph/core 0.10.2 → 0.10.4

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.js CHANGED
@@ -977,6 +977,32 @@ function getTypeName(typeId) {
977
977
 
978
978
  // src/catalog/edge-catalog.ts
979
979
  var UPG_WILDCARD_ENDPOINT = "node";
980
+ var CLASSIFICATION_EDGE_PROPERTY_SCHEMA = {
981
+ confidence: {
982
+ type: "assessment",
983
+ scale_id: "confidence_5",
984
+ description: "How sure we are this node belongs in this classification cell, on the canonical confidence_5 scale. Numeric value plus a high/medium/low label.",
985
+ properties: {
986
+ value: { type: "number", description: "Numeric value 1-5 (confidence_5)." },
987
+ label: { type: "string", description: "Qualitative label (Guessing/Hunch/Some evidence/Confident/Data-backed, or high/medium/low)." },
988
+ scale_id: { type: "string", description: "Scale rated on (optional; defaults confidence_5)." },
989
+ normalized: { type: "number", description: "Normalized 0-1 value for cross-tool comparison (optional)." }
990
+ },
991
+ required: ["value", "label"]
992
+ },
993
+ assessed_on: {
994
+ type: "string",
995
+ description: 'Provenance: ISO date-time the classification was made or last re-checked. Drives staleness queries. @example "2026-06-13"'
996
+ },
997
+ rationale: {
998
+ type: "string",
999
+ description: "Optional. Short note on why this node sits in this cell. Edge-level, distinct from classification_value.rationale (why the value exists as a category)."
1000
+ },
1001
+ evidence: {
1002
+ type: "string",
1003
+ description: "Optional. A source URL, or a competitor_signal / evidence node id backing the classification. Mirrors feature_rivals_competitor_feature.evidence (free text or a node id)."
1004
+ }
1005
+ };
980
1006
  var UPG_EDGE_CATALOG = {
981
1007
  // ── Part 1: Core Narrative Spine (Ring 1) ──────────────────────────────────
982
1008
  // 1.1 User Domain
@@ -1149,7 +1175,13 @@ var UPG_EDGE_CATALOG = {
1149
1175
  // competitor can be classified directly against a `registry/{classification_value}`
1150
1176
  // canonical, eliminating the per-graph taxonomy node. Within-graph (the catalogue
1151
1177
  // case) here; cross-product against the registry canonical via create_cross_product_edge.
1152
- competitor_classified_as_classification_value: { forward_verb: "classified_as", reverse_verb: "classification_of", classification: "semantic", source_type: "competitor", target_type: "classification_value" },
1178
+ competitor_classified_as_classification_value: { forward_verb: "classified_as", reverse_verb: "classification_of", classification: "semantic", source_type: "competitor", target_type: "classification_value", carries_properties: true, property_schema: CLASSIFICATION_EDGE_PROPERTY_SCHEMA },
1179
+ // 0.10.3: the polymorphic, type-agnostic sibling. ANY node (a feature, a
1180
+ // product, a market_segment) classified against a classification_value, so
1181
+ // classification is not welded to the competitor type. Also dual-registered as
1182
+ // a cross-edge for the registry-canonical case. source_type:'node' (wildcard);
1183
+ // listed in UPG_POLYMORPHIC_EDGE_KEYS.
1184
+ node_classified_as_classification_value: { forward_verb: "classified_as", reverse_verb: "classification_of", classification: "semantic", source_type: "node", target_type: "classification_value", carries_properties: true, property_schema: CLASSIFICATION_EDGE_PROPERTY_SCHEMA },
1153
1185
  persona_anti_fit_for_classification_value: { forward_verb: "is_anti_fit_for", reverse_verb: "should_not_be_picked_by", classification: "semantic", source_type: "persona", target_type: "classification_value" },
1154
1186
  persona_anti_fit_for_product: { forward_verb: "is_anti_fit_for", reverse_verb: "should_not_be_picked_by", classification: "semantic", source_type: "persona", target_type: "product" },
1155
1187
  persona_anti_fit_for_competitor: { forward_verb: "is_anti_fit_for", reverse_verb: "should_not_be_picked_by", classification: "semantic", source_type: "persona", target_type: "competitor" },
@@ -2987,7 +3019,9 @@ var UPG_POLYMORPHIC_EDGE_KEYS = [
2987
3019
  // Universal architecture references
2988
3020
  "node_belongs_to_bounded_context",
2989
3021
  // Framework exercises (an exercise can include any entity type)
2990
- "framework_exercise_includes_node"
3022
+ "framework_exercise_includes_node",
3023
+ // Universal classification (any node classified against a classification_value)
3024
+ "node_classified_as_classification_value"
2991
3025
  ];
2992
3026
  var _POLY_KEY_SET = new Set(UPG_POLYMORPHIC_EDGE_KEYS);
2993
3027
  function isPolymorphicEdge(key) {
@@ -3001,6 +3035,10 @@ function edgeCarriesProperties(type) {
3001
3035
  const def = UPG_EDGE_CATALOG[type];
3002
3036
  return def?.carries_properties === true;
3003
3037
  }
3038
+ function getEdgePropertySchema(type) {
3039
+ const def = UPG_EDGE_CATALOG[type];
3040
+ return def?.property_schema;
3041
+ }
3004
3042
 
3005
3043
  // src/catalog/legacy-product-stages.ts
3006
3044
  var LEGACY_PRODUCT_STAGES = Object.freeze({
@@ -3045,7 +3083,8 @@ var UPG_CROSS_EDGE_TYPES = [
3045
3083
  "feature_rivals_competitor_feature",
3046
3084
  "competitor_signal_maps_to_feature",
3047
3085
  "competitor_signal_surfaces_opportunity",
3048
- "competitor_classified_as_classification_value"
3086
+ "competitor_classified_as_classification_value",
3087
+ "node_classified_as_classification_value"
3049
3088
  ];
3050
3089
  var REGISTRY_PRODUCT_ID = "registry";
3051
3090
 
@@ -18530,6 +18569,65 @@ function isUPGDocument(doc) {
18530
18569
  return validateUPGDocument(doc).valid;
18531
18570
  }
18532
18571
 
18572
+ // src/properties/edge-property-validation.ts
18573
+ function validateEdgeProperties(edgeType, properties) {
18574
+ const schema = getEdgePropertySchema(edgeType);
18575
+ if (!schema || !properties) return [];
18576
+ const errors = [];
18577
+ const allowed = Object.keys(schema);
18578
+ for (const key of Object.keys(properties)) {
18579
+ if (!allowed.includes(key)) {
18580
+ errors.push(`unknown property "${key}" for edge type "${edgeType}" (allowed: ${allowed.join(", ")})`);
18581
+ }
18582
+ }
18583
+ for (const [key, def] of Object.entries(schema)) {
18584
+ const val = properties[key];
18585
+ if (val === void 0 || val === null) continue;
18586
+ errors.push(...validateValue(key, def, val));
18587
+ }
18588
+ return errors;
18589
+ }
18590
+ function validateValue(key, def, val) {
18591
+ const errors = [];
18592
+ switch (def.type) {
18593
+ case "string":
18594
+ if (typeof val !== "string") errors.push(`property "${key}" must be a string`);
18595
+ break;
18596
+ case "number":
18597
+ if (typeof val !== "number") errors.push(`property "${key}" must be a number`);
18598
+ break;
18599
+ case "boolean":
18600
+ if (typeof val !== "boolean") errors.push(`property "${key}" must be a boolean`);
18601
+ break;
18602
+ case "assessment": {
18603
+ if (typeof val !== "object" || Array.isArray(val)) {
18604
+ errors.push(`property "${key}" must be an assessment object with value and label`);
18605
+ break;
18606
+ }
18607
+ const a = val;
18608
+ for (const r of def.required ?? ["value", "label"]) {
18609
+ if (a[r] === void 0 || a[r] === null) errors.push(`assessment "${key}" is missing required "${r}"`);
18610
+ }
18611
+ if (a.value !== void 0 && a.value !== null) {
18612
+ if (typeof a.value !== "number") {
18613
+ errors.push(`assessment "${key}".value must be a number`);
18614
+ } else {
18615
+ const scaleId = (typeof a.scale_id === "string" ? a.scale_id : void 0) ?? def.scale_id;
18616
+ const scale = scaleId ? getScale(scaleId) : void 0;
18617
+ if (scale && (a.value < scale.min || a.value > scale.max)) {
18618
+ errors.push(`assessment "${key}".value ${a.value} is out of range ${scale.min}-${scale.max} for scale ${scaleId}`);
18619
+ }
18620
+ }
18621
+ }
18622
+ if (typeof a.scale_id === "string" && def.scale_id !== void 0 && a.scale_id !== def.scale_id) {
18623
+ errors.push(`assessment "${key}".scale_id "${a.scale_id}" must be "${def.scale_id}"`);
18624
+ }
18625
+ break;
18626
+ }
18627
+ }
18628
+ return errors;
18629
+ }
18630
+
18533
18631
  // src/presentation/entity-emoji.ts
18534
18632
  var DEFAULT_ENTITY_EMOJI = "\u{1F4E6}";
18535
18633
  var ENTITY_EMOJI = {
@@ -28429,7 +28527,7 @@ function isCanonical(text) {
28429
28527
  }
28430
28528
 
28431
28529
  // src/index.ts
28432
- var UPG_VERSION = "0.10.2";
28530
+ var UPG_VERSION = "0.10.4";
28433
28531
  var UPG_FORMAT_VERSION = "0.4.0";
28434
28532
  var MARKDOWN_FORMAT_VERSION = "0.1";
28435
28533
  var UPG_TYPES = getTypes();
@@ -28497,6 +28595,7 @@ export {
28497
28595
  BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK,
28498
28596
  BUSINESS_GTM_GROWTH_PLAYBOOK,
28499
28597
  BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK,
28598
+ CLASSIFICATION_EDGE_PROPERTY_SCHEMA,
28500
28599
  CONTENT_DEPTH_WARNING_RULES,
28501
28600
  DEFAULT_ENTITY_EMOJI,
28502
28601
  DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK,
@@ -28608,6 +28707,7 @@ export {
28608
28707
  getDomainForType,
28609
28708
  getDomainIdForType,
28610
28709
  getDomainsInRing,
28710
+ getEdgePropertySchema,
28611
28711
  getEnumScale,
28612
28712
  getEnumValueMeta,
28613
28713
  getGuideForDomain,
@@ -28672,6 +28772,7 @@ export {
28672
28772
  ringOrderedDomainIds,
28673
28773
  rotateSlug,
28674
28774
  serializeCanonical,
28775
+ validateEdgeProperties,
28675
28776
  validateProductStageStrict,
28676
28777
  validateUPGDocument,
28677
28778
  validateUPGFramework,