@unified-product-graph/core 0.7.3 → 0.7.5
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 +48 -0
- package/dist/index.cjs +433 -125
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +130 -5
- package/dist/index.d.ts +130 -5
- package/dist/index.js +425 -125
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12630,26 +12630,6 @@ var UPG_FRAMEWORKS = [
|
|
|
12630
12630
|
"label": "Items to score",
|
|
12631
12631
|
"entityTypeId": "feature",
|
|
12632
12632
|
"description": "Features, opportunities, or solutions being evaluated"
|
|
12633
|
-
},
|
|
12634
|
-
{
|
|
12635
|
-
"label": "Reach",
|
|
12636
|
-
"entityTypeId": "feature",
|
|
12637
|
-
"description": "How many users does this affect per quarter?"
|
|
12638
|
-
},
|
|
12639
|
-
{
|
|
12640
|
-
"label": "Impact",
|
|
12641
|
-
"entityTypeId": "feature",
|
|
12642
|
-
"description": "How much does this move the target outcome?"
|
|
12643
|
-
},
|
|
12644
|
-
{
|
|
12645
|
-
"label": "Confidence",
|
|
12646
|
-
"entityTypeId": "feature",
|
|
12647
|
-
"description": "How sure are we about these estimates?"
|
|
12648
|
-
},
|
|
12649
|
-
{
|
|
12650
|
-
"label": "Effort",
|
|
12651
|
-
"entityTypeId": "feature",
|
|
12652
|
-
"description": "How many person-months to build?"
|
|
12653
12633
|
}
|
|
12654
12634
|
],
|
|
12655
12635
|
"data": {
|
|
@@ -12663,31 +12643,35 @@ var UPG_FRAMEWORKS = [
|
|
|
12663
12643
|
"feature": [
|
|
12664
12644
|
{
|
|
12665
12645
|
"property": "reach",
|
|
12666
|
-
"type": "
|
|
12646
|
+
"type": "assessment",
|
|
12647
|
+
"scale_id": "reach_5",
|
|
12667
12648
|
"required": true,
|
|
12668
12649
|
"label": "Reach",
|
|
12669
12650
|
"description": "How many users will this impact per quarter?"
|
|
12670
12651
|
},
|
|
12671
12652
|
{
|
|
12672
12653
|
"property": "impact",
|
|
12673
|
-
"type": "
|
|
12654
|
+
"type": "assessment",
|
|
12655
|
+
"scale_id": "impact_5",
|
|
12674
12656
|
"required": true,
|
|
12675
12657
|
"label": "Impact",
|
|
12676
|
-
"description": "How much will this impact each user
|
|
12658
|
+
"description": "How much will this impact each user, on the impact scale?"
|
|
12677
12659
|
},
|
|
12678
12660
|
{
|
|
12679
12661
|
"property": "confidence",
|
|
12680
|
-
"type": "
|
|
12662
|
+
"type": "assessment",
|
|
12663
|
+
"scale_id": "confidence_5",
|
|
12681
12664
|
"required": true,
|
|
12682
12665
|
"label": "Confidence",
|
|
12683
|
-
"description": "How confident are you in the
|
|
12666
|
+
"description": "How confident are you in the reach, impact, and effort estimates?"
|
|
12684
12667
|
},
|
|
12685
12668
|
{
|
|
12686
12669
|
"property": "effort",
|
|
12687
|
-
"type": "
|
|
12670
|
+
"type": "assessment",
|
|
12671
|
+
"scale_id": "effort_5",
|
|
12688
12672
|
"required": true,
|
|
12689
12673
|
"label": "Effort",
|
|
12690
|
-
"description": "
|
|
12674
|
+
"description": "How much work is required to build and ship this, on the effort scale?"
|
|
12691
12675
|
}
|
|
12692
12676
|
]
|
|
12693
12677
|
},
|
|
@@ -12881,7 +12865,7 @@ var UPG_FRAMEWORKS = [
|
|
|
12881
12865
|
{
|
|
12882
12866
|
"label": "Performance",
|
|
12883
12867
|
"entityTypeId": "feature",
|
|
12884
|
-
"description": "More is better
|
|
12868
|
+
"description": "More is better: linear satisfaction increase"
|
|
12885
12869
|
},
|
|
12886
12870
|
{
|
|
12887
12871
|
"label": "Delighters",
|
|
@@ -13130,24 +13114,9 @@ var UPG_FRAMEWORKS = [
|
|
|
13130
13114
|
],
|
|
13131
13115
|
"slots": [
|
|
13132
13116
|
{
|
|
13133
|
-
"label": "
|
|
13134
|
-
"entityTypeId": "feature",
|
|
13135
|
-
"description": "Non-negotiable requirements for this release"
|
|
13136
|
-
},
|
|
13137
|
-
{
|
|
13138
|
-
"label": "Should Have",
|
|
13139
|
-
"entityTypeId": "feature",
|
|
13140
|
-
"description": "Important but not critical; a workaround exists"
|
|
13141
|
-
},
|
|
13142
|
-
{
|
|
13143
|
-
"label": "Could Have",
|
|
13144
|
-
"entityTypeId": "feature",
|
|
13145
|
-
"description": "Nice to have; include if time permits"
|
|
13146
|
-
},
|
|
13147
|
-
{
|
|
13148
|
-
"label": "Won't Have",
|
|
13117
|
+
"label": "Requirements to categorise",
|
|
13149
13118
|
"entityTypeId": "feature",
|
|
13150
|
-
"description": "
|
|
13119
|
+
"description": "Features or requirements sorted into the four MoSCoW buckets"
|
|
13151
13120
|
}
|
|
13152
13121
|
],
|
|
13153
13122
|
"data": {
|
|
@@ -13157,7 +13126,23 @@ var UPG_FRAMEWORKS = [
|
|
|
13157
13126
|
"role": "scored_item"
|
|
13158
13127
|
}
|
|
13159
13128
|
],
|
|
13160
|
-
"required_properties": {
|
|
13129
|
+
"required_properties": {
|
|
13130
|
+
"feature": [
|
|
13131
|
+
{
|
|
13132
|
+
"property": "moscow",
|
|
13133
|
+
"type": "enum",
|
|
13134
|
+
"required": true,
|
|
13135
|
+
"label": "MoSCoW priority",
|
|
13136
|
+
"description": "Which scope bucket this requirement falls into for the current release",
|
|
13137
|
+
"enum_values": [
|
|
13138
|
+
"must",
|
|
13139
|
+
"should",
|
|
13140
|
+
"could",
|
|
13141
|
+
"wont"
|
|
13142
|
+
]
|
|
13143
|
+
}
|
|
13144
|
+
]
|
|
13145
|
+
},
|
|
13161
13146
|
"computed_properties": [
|
|
13162
13147
|
{
|
|
13163
13148
|
"property": "must_have_ratio",
|
|
@@ -13177,28 +13162,19 @@ var UPG_FRAMEWORKS = [
|
|
|
13177
13162
|
"columns": [
|
|
13178
13163
|
{
|
|
13179
13164
|
"property": "title",
|
|
13180
|
-
"label": "
|
|
13181
|
-
"sortable": true
|
|
13182
|
-
},
|
|
13183
|
-
{
|
|
13184
|
-
"property": "title",
|
|
13185
|
-
"label": "Should Have",
|
|
13186
|
-
"sortable": true
|
|
13187
|
-
},
|
|
13188
|
-
{
|
|
13189
|
-
"property": "title",
|
|
13190
|
-
"label": "Could Have",
|
|
13165
|
+
"label": "Requirement",
|
|
13191
13166
|
"sortable": true
|
|
13192
13167
|
},
|
|
13193
13168
|
{
|
|
13194
|
-
"property": "
|
|
13195
|
-
"label": "
|
|
13196
|
-
"sortable": true
|
|
13169
|
+
"property": "moscow",
|
|
13170
|
+
"label": "Priority",
|
|
13171
|
+
"sortable": true,
|
|
13172
|
+
"format": "badge"
|
|
13197
13173
|
}
|
|
13198
13174
|
]
|
|
13199
13175
|
},
|
|
13200
13176
|
"sort_by": {
|
|
13201
|
-
"property": "
|
|
13177
|
+
"property": "moscow",
|
|
13202
13178
|
"direction": "asc"
|
|
13203
13179
|
},
|
|
13204
13180
|
"colour_by": "type",
|
|
@@ -13749,24 +13725,9 @@ var UPG_FRAMEWORKS = [
|
|
|
13749
13725
|
],
|
|
13750
13726
|
"slots": [
|
|
13751
13727
|
{
|
|
13752
|
-
"label": "
|
|
13728
|
+
"label": "Delivery metric",
|
|
13753
13729
|
"entityTypeId": "metric",
|
|
13754
|
-
"description": "
|
|
13755
|
-
},
|
|
13756
|
-
{
|
|
13757
|
-
"label": "Lead Time for Changes",
|
|
13758
|
-
"entityTypeId": "metric",
|
|
13759
|
-
"description": "Time from commit to production"
|
|
13760
|
-
},
|
|
13761
|
-
{
|
|
13762
|
-
"label": "Change Failure Rate",
|
|
13763
|
-
"entityTypeId": "metric",
|
|
13764
|
-
"description": "Percentage of deployments causing failures"
|
|
13765
|
-
},
|
|
13766
|
-
{
|
|
13767
|
-
"label": "Time to Restore",
|
|
13768
|
-
"entityTypeId": "metric",
|
|
13769
|
-
"description": "How long to recover from a failure"
|
|
13730
|
+
"description": "One of the four DORA software-delivery performance metrics"
|
|
13770
13731
|
}
|
|
13771
13732
|
],
|
|
13772
13733
|
"data": {
|
|
@@ -13776,7 +13737,36 @@ var UPG_FRAMEWORKS = [
|
|
|
13776
13737
|
"role": "item"
|
|
13777
13738
|
}
|
|
13778
13739
|
],
|
|
13779
|
-
"required_properties": {
|
|
13740
|
+
"required_properties": {
|
|
13741
|
+
"metric": [
|
|
13742
|
+
{
|
|
13743
|
+
"property": "dora_metric",
|
|
13744
|
+
"type": "enum",
|
|
13745
|
+
"required": true,
|
|
13746
|
+
"label": "DORA metric",
|
|
13747
|
+
"description": "Which of the four DORA metrics this measures",
|
|
13748
|
+
"enum_values": [
|
|
13749
|
+
"deployment_frequency",
|
|
13750
|
+
"lead_time_for_changes",
|
|
13751
|
+
"change_failure_rate",
|
|
13752
|
+
"time_to_restore"
|
|
13753
|
+
]
|
|
13754
|
+
},
|
|
13755
|
+
{
|
|
13756
|
+
"property": "performance_tier",
|
|
13757
|
+
"type": "enum",
|
|
13758
|
+
"required": false,
|
|
13759
|
+
"label": "Performance tier",
|
|
13760
|
+
"description": "Where this metric sits on the DORA elite-to-low benchmark",
|
|
13761
|
+
"enum_values": [
|
|
13762
|
+
"elite",
|
|
13763
|
+
"high",
|
|
13764
|
+
"medium",
|
|
13765
|
+
"low"
|
|
13766
|
+
]
|
|
13767
|
+
}
|
|
13768
|
+
]
|
|
13769
|
+
},
|
|
13780
13770
|
"computed_properties": [
|
|
13781
13771
|
{
|
|
13782
13772
|
"property": "stability_index",
|
|
@@ -13932,29 +13922,9 @@ var UPG_FRAMEWORKS = [
|
|
|
13932
13922
|
],
|
|
13933
13923
|
"slots": [
|
|
13934
13924
|
{
|
|
13935
|
-
"label": "
|
|
13925
|
+
"label": "Lifecycle metric",
|
|
13936
13926
|
"entityTypeId": "metric",
|
|
13937
|
-
"description": "
|
|
13938
|
-
},
|
|
13939
|
-
{
|
|
13940
|
-
"label": "Activation",
|
|
13941
|
-
"entityTypeId": "metric",
|
|
13942
|
-
"description": "Do users have a great first experience?"
|
|
13943
|
-
},
|
|
13944
|
-
{
|
|
13945
|
-
"label": "Retention",
|
|
13946
|
-
"entityTypeId": "metric",
|
|
13947
|
-
"description": "Do users come back?"
|
|
13948
|
-
},
|
|
13949
|
-
{
|
|
13950
|
-
"label": "Revenue",
|
|
13951
|
-
"entityTypeId": "metric",
|
|
13952
|
-
"description": "Can you monetise the behaviour?"
|
|
13953
|
-
},
|
|
13954
|
-
{
|
|
13955
|
-
"label": "Referral",
|
|
13956
|
-
"entityTypeId": "metric",
|
|
13957
|
-
"description": "Do users tell others?"
|
|
13927
|
+
"description": "A metric tracking one stage of the customer lifecycle funnel"
|
|
13958
13928
|
}
|
|
13959
13929
|
],
|
|
13960
13930
|
"data": {
|
|
@@ -13964,7 +13934,24 @@ var UPG_FRAMEWORKS = [
|
|
|
13964
13934
|
"role": "item"
|
|
13965
13935
|
}
|
|
13966
13936
|
],
|
|
13967
|
-
"required_properties": {
|
|
13937
|
+
"required_properties": {
|
|
13938
|
+
"metric": [
|
|
13939
|
+
{
|
|
13940
|
+
"property": "lifecycle_stage",
|
|
13941
|
+
"type": "enum",
|
|
13942
|
+
"required": true,
|
|
13943
|
+
"label": "Lifecycle stage",
|
|
13944
|
+
"description": "Which AARRR funnel stage this metric measures",
|
|
13945
|
+
"enum_values": [
|
|
13946
|
+
"acquisition",
|
|
13947
|
+
"activation",
|
|
13948
|
+
"retention",
|
|
13949
|
+
"revenue",
|
|
13950
|
+
"referral"
|
|
13951
|
+
]
|
|
13952
|
+
}
|
|
13953
|
+
]
|
|
13954
|
+
},
|
|
13968
13955
|
"computed_properties": [
|
|
13969
13956
|
{
|
|
13970
13957
|
"property": "conversion_rate",
|
|
@@ -13976,7 +13963,39 @@ var UPG_FRAMEWORKS = [
|
|
|
13976
13963
|
]
|
|
13977
13964
|
},
|
|
13978
13965
|
"structure": {
|
|
13979
|
-
"pattern": "funnel"
|
|
13966
|
+
"pattern": "funnel",
|
|
13967
|
+
"stages": [
|
|
13968
|
+
{
|
|
13969
|
+
"id": "acquisition",
|
|
13970
|
+
"label": "Acquisition",
|
|
13971
|
+
"order": 0,
|
|
13972
|
+
"entity_type": "metric"
|
|
13973
|
+
},
|
|
13974
|
+
{
|
|
13975
|
+
"id": "activation",
|
|
13976
|
+
"label": "Activation",
|
|
13977
|
+
"order": 1,
|
|
13978
|
+
"entity_type": "metric"
|
|
13979
|
+
},
|
|
13980
|
+
{
|
|
13981
|
+
"id": "retention",
|
|
13982
|
+
"label": "Retention",
|
|
13983
|
+
"order": 2,
|
|
13984
|
+
"entity_type": "metric"
|
|
13985
|
+
},
|
|
13986
|
+
{
|
|
13987
|
+
"id": "revenue",
|
|
13988
|
+
"label": "Revenue",
|
|
13989
|
+
"order": 3,
|
|
13990
|
+
"entity_type": "metric"
|
|
13991
|
+
},
|
|
13992
|
+
{
|
|
13993
|
+
"id": "referral",
|
|
13994
|
+
"label": "Referral",
|
|
13995
|
+
"order": 4,
|
|
13996
|
+
"entity_type": "metric"
|
|
13997
|
+
}
|
|
13998
|
+
]
|
|
13980
13999
|
},
|
|
13981
14000
|
"presentation": {
|
|
13982
14001
|
"layout": {
|
|
@@ -14023,24 +14042,9 @@ var UPG_FRAMEWORKS = [
|
|
|
14023
14042
|
],
|
|
14024
14043
|
"slots": [
|
|
14025
14044
|
{
|
|
14026
|
-
"label": "
|
|
14027
|
-
"entityTypeId": "metric",
|
|
14028
|
-
"description": "The single metric reflecting core value delivered"
|
|
14029
|
-
},
|
|
14030
|
-
{
|
|
14031
|
-
"label": "Input Metric 1",
|
|
14032
|
-
"entityTypeId": "metric",
|
|
14033
|
-
"description": "A driver metric you can directly influence"
|
|
14034
|
-
},
|
|
14035
|
-
{
|
|
14036
|
-
"label": "Input Metric 2",
|
|
14037
|
-
"entityTypeId": "metric",
|
|
14038
|
-
"description": "A driver metric you can directly influence"
|
|
14039
|
-
},
|
|
14040
|
-
{
|
|
14041
|
-
"label": "Input Metric 3",
|
|
14045
|
+
"label": "Metric",
|
|
14042
14046
|
"entityTypeId": "metric",
|
|
14043
|
-
"description": "
|
|
14047
|
+
"description": "The North Star metric or one of its 3-5 input (driver) metrics"
|
|
14044
14048
|
}
|
|
14045
14049
|
],
|
|
14046
14050
|
"data": {
|
|
@@ -14050,7 +14054,28 @@ var UPG_FRAMEWORKS = [
|
|
|
14050
14054
|
"role": "item"
|
|
14051
14055
|
}
|
|
14052
14056
|
],
|
|
14053
|
-
"required_properties": {
|
|
14057
|
+
"required_properties": {
|
|
14058
|
+
"metric": [
|
|
14059
|
+
{
|
|
14060
|
+
"property": "metric_role",
|
|
14061
|
+
"type": "enum",
|
|
14062
|
+
"required": true,
|
|
14063
|
+
"label": "Metric role",
|
|
14064
|
+
"description": "Whether this is the single North Star or a driver that feeds it",
|
|
14065
|
+
"enum_values": [
|
|
14066
|
+
"north_star",
|
|
14067
|
+
"input"
|
|
14068
|
+
]
|
|
14069
|
+
},
|
|
14070
|
+
{
|
|
14071
|
+
"property": "leverage",
|
|
14072
|
+
"type": "number",
|
|
14073
|
+
"required": false,
|
|
14074
|
+
"label": "Leverage",
|
|
14075
|
+
"description": "How strongly this input metric moves the North Star (input metrics only)"
|
|
14076
|
+
}
|
|
14077
|
+
]
|
|
14078
|
+
},
|
|
14054
14079
|
"computed_properties": [
|
|
14055
14080
|
{
|
|
14056
14081
|
"property": "nsm_impact",
|
|
@@ -14350,7 +14375,7 @@ var UPG_FRAMEWORKS = [
|
|
|
14350
14375
|
]
|
|
14351
14376
|
},
|
|
14352
14377
|
"education": {
|
|
14353
|
-
"purpose": "Design the product itself as the primary growth driver (free tier, self-serve onboarding, in-product virality)
|
|
14378
|
+
"purpose": "Design the product itself as the primary growth driver (free tier, self-serve onboarding, in-product virality) to reduce dependence on sales-led acquisition.",
|
|
14354
14379
|
"core_question": "Can our product acquire, activate, and expand users without human intervention, and where in the loop do we still need sales?",
|
|
14355
14380
|
"when_to_use": [
|
|
14356
14381
|
"You are launching a new product, feature, or entering a new market",
|
|
@@ -14760,7 +14785,7 @@ var UPG_FRAMEWORKS = [
|
|
|
14760
14785
|
{
|
|
14761
14786
|
"label": "Team",
|
|
14762
14787
|
"entityTypeId": "team",
|
|
14763
|
-
"description": "Team conducting the health check
|
|
14788
|
+
"description": "Team conducting the health check; all members vote anonymously on each dimension"
|
|
14764
14789
|
},
|
|
14765
14790
|
{
|
|
14766
14791
|
"label": "Retrospective",
|
|
@@ -14849,7 +14874,7 @@ var UPG_FRAMEWORKS = [
|
|
|
14849
14874
|
},
|
|
14850
14875
|
"education": {
|
|
14851
14876
|
"purpose": "Regularly assess team health across dimensions (psychological safety, autonomy, mission clarity, fun, speed, learning) to catch problems early and celebrate strengths.",
|
|
14852
|
-
"core_question": "How is the team really doing
|
|
14877
|
+
"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?",
|
|
14853
14878
|
"when_to_use": [
|
|
14854
14879
|
"You need to improve team collaboration, clarity, or effectiveness",
|
|
14855
14880
|
"Roles and responsibilities are unclear or causing friction",
|
|
@@ -14865,7 +14890,7 @@ var UPG_FRAMEWORKS = [
|
|
|
14865
14890
|
"id": "raid-log",
|
|
14866
14891
|
"name": "RAID Log",
|
|
14867
14892
|
"version": "1.0.0",
|
|
14868
|
-
"description": "A project management register tracking Risks, Assumptions, Issues, and Dependencies
|
|
14893
|
+
"description": "A project management register tracking Risks, Assumptions, Issues, and Dependencies, the four categories most likely to derail a project if left unmanaged.",
|
|
14869
14894
|
"category": "program_mgmt",
|
|
14870
14895
|
"origin": {
|
|
14871
14896
|
"type": "practitioner",
|
|
@@ -23316,6 +23341,273 @@ function validateExpression(expr) {
|
|
|
23316
23341
|
return null;
|
|
23317
23342
|
}
|
|
23318
23343
|
|
|
23344
|
+
// src/format/canonical.ts
|
|
23345
|
+
import { createHash } from "crypto";
|
|
23346
|
+
var UPG_CANONICAL_FORMAT_VERSION = "1.0.0";
|
|
23347
|
+
var INTEGRITY_HASH_PRIMITIVE = "sha256";
|
|
23348
|
+
var INTEGRITY_DIGEST_HEX = 32;
|
|
23349
|
+
var INTEGRITY_ALGORITHM = "sha256-128";
|
|
23350
|
+
function canonicalizeOpen(value) {
|
|
23351
|
+
if (Array.isArray(value)) return value.map(canonicalizeOpen);
|
|
23352
|
+
if (value !== null && typeof value === "object") {
|
|
23353
|
+
const out = {};
|
|
23354
|
+
for (const key of Object.keys(value).sort(byCodeUnit)) {
|
|
23355
|
+
out[key] = canonicalizeOpen(value[key]);
|
|
23356
|
+
}
|
|
23357
|
+
return out;
|
|
23358
|
+
}
|
|
23359
|
+
return value;
|
|
23360
|
+
}
|
|
23361
|
+
function byCodeUnit(a, b) {
|
|
23362
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
23363
|
+
}
|
|
23364
|
+
function isEmpty(value) {
|
|
23365
|
+
if (value === null || value === void 0) return true;
|
|
23366
|
+
if (typeof value === "string") return value.length === 0;
|
|
23367
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
23368
|
+
if (typeof value === "object") return Object.keys(value).length === 0;
|
|
23369
|
+
return false;
|
|
23370
|
+
}
|
|
23371
|
+
function orderedObject(source, keyOrder, opts = {}) {
|
|
23372
|
+
const force = new Set(opts.forceKeys ?? []);
|
|
23373
|
+
const open = new Set(opts.openKeys ?? []);
|
|
23374
|
+
const out = {};
|
|
23375
|
+
const emit = (key) => {
|
|
23376
|
+
if (!(key in source)) return;
|
|
23377
|
+
const raw = source[key];
|
|
23378
|
+
if (!force.has(key) && isEmpty(raw)) return;
|
|
23379
|
+
out[key] = open.has(key) ? canonicalizeOpen(raw) : raw;
|
|
23380
|
+
};
|
|
23381
|
+
for (const key of keyOrder) emit(key);
|
|
23382
|
+
for (const key of Object.keys(source).sort(byCodeUnit)) {
|
|
23383
|
+
if (!keyOrder.includes(key)) emit(key);
|
|
23384
|
+
}
|
|
23385
|
+
return out;
|
|
23386
|
+
}
|
|
23387
|
+
function repairNodeDrift(node) {
|
|
23388
|
+
const out = { ...node };
|
|
23389
|
+
if (typeof out.properties === "string") {
|
|
23390
|
+
out.properties = parseDriftString(out.properties, "object", `node ${node.id}.properties`);
|
|
23391
|
+
}
|
|
23392
|
+
if (typeof out.tags === "string") {
|
|
23393
|
+
out.tags = parseDriftString(out.tags, "array", `node ${node.id}.tags`);
|
|
23394
|
+
}
|
|
23395
|
+
return out;
|
|
23396
|
+
}
|
|
23397
|
+
function parseDriftString(raw, expect, where) {
|
|
23398
|
+
let parsed;
|
|
23399
|
+
try {
|
|
23400
|
+
parsed = JSON.parse(raw);
|
|
23401
|
+
} catch {
|
|
23402
|
+
throw new Error(`[upg fmt] ${where} is a string that is not valid JSON: ${truncate(raw)}`);
|
|
23403
|
+
}
|
|
23404
|
+
const ok = expect === "array" ? Array.isArray(parsed) : parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
|
|
23405
|
+
if (!ok) throw new Error(`[upg fmt] ${where} is a string that does not parse to a JSON ${expect}: ${truncate(raw)}`);
|
|
23406
|
+
return parsed;
|
|
23407
|
+
}
|
|
23408
|
+
function truncate(s) {
|
|
23409
|
+
return s.length > 60 ? s.slice(0, 57) + "..." : s;
|
|
23410
|
+
}
|
|
23411
|
+
function deriveSummary(description) {
|
|
23412
|
+
if (!description) return void 0;
|
|
23413
|
+
const firstLine = description.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
23414
|
+
if (!firstLine) return void 0;
|
|
23415
|
+
return firstLine.length > 200 ? firstLine.slice(0, 197) + "..." : firstLine;
|
|
23416
|
+
}
|
|
23417
|
+
function sortNodes(nodes) {
|
|
23418
|
+
return [...nodes].sort(
|
|
23419
|
+
(a, b) => byCodeUnit(a.type ?? "", b.type ?? "") || byCodeUnit(a.slug ?? a.title ?? "", b.slug ?? b.title ?? "") || byCodeUnit(a.id ?? "", b.id ?? "")
|
|
23420
|
+
);
|
|
23421
|
+
}
|
|
23422
|
+
function sortEdges(edges) {
|
|
23423
|
+
return [...edges].sort(
|
|
23424
|
+
(a, b) => byCodeUnit(a.source ?? "", b.source ?? "") || byCodeUnit(a.target ?? "", b.target ?? "") || byCodeUnit(a.type ?? "", b.type ?? "") || byCodeUnit(a.id ?? "", b.id ?? "")
|
|
23425
|
+
);
|
|
23426
|
+
}
|
|
23427
|
+
var NODE_KEY_ORDER = [
|
|
23428
|
+
"id",
|
|
23429
|
+
"type",
|
|
23430
|
+
"title",
|
|
23431
|
+
"slug",
|
|
23432
|
+
"aliases",
|
|
23433
|
+
"description",
|
|
23434
|
+
"tags",
|
|
23435
|
+
"status",
|
|
23436
|
+
"lifecycle_status",
|
|
23437
|
+
"source_id",
|
|
23438
|
+
"source_type",
|
|
23439
|
+
"mapping_confidence",
|
|
23440
|
+
"external_tool",
|
|
23441
|
+
"external_ref",
|
|
23442
|
+
"external_id",
|
|
23443
|
+
"sort_order",
|
|
23444
|
+
"properties"
|
|
23445
|
+
];
|
|
23446
|
+
var EDGE_KEY_ORDER = ["id", "source", "target", "type", "mapping_confidence"];
|
|
23447
|
+
var CROSS_EDGE_KEY_ORDER = ["id", "source", "target", "type", "source_product_id", "target_product_id", "mapping_confidence"];
|
|
23448
|
+
var PRODUCT_KEY_ORDER = ["id", "title", "description", "stage", "properties"];
|
|
23449
|
+
function canonicalNode(node) {
|
|
23450
|
+
const repaired = repairNodeDrift(node);
|
|
23451
|
+
if (Array.isArray(repaired.tags)) {
|
|
23452
|
+
repaired.tags = [...new Set(repaired.tags)].sort(byCodeUnit);
|
|
23453
|
+
}
|
|
23454
|
+
return orderedObject(repaired, NODE_KEY_ORDER, { forceKeys: ["id", "type", "title"], openKeys: ["properties"] });
|
|
23455
|
+
}
|
|
23456
|
+
function canonicalEdge(edge) {
|
|
23457
|
+
return orderedObject(edge, EDGE_KEY_ORDER, {
|
|
23458
|
+
forceKeys: ["id", "source", "target", "type"]
|
|
23459
|
+
});
|
|
23460
|
+
}
|
|
23461
|
+
function canonicalCrossEdge(edge) {
|
|
23462
|
+
return orderedObject(edge, CROSS_EDGE_KEY_ORDER, {
|
|
23463
|
+
forceKeys: ["id", "source", "target", "type"]
|
|
23464
|
+
});
|
|
23465
|
+
}
|
|
23466
|
+
function canonicalProduct(product) {
|
|
23467
|
+
return orderedObject(product, PRODUCT_KEY_ORDER, { forceKeys: ["id", "title"], openKeys: ["properties"] });
|
|
23468
|
+
}
|
|
23469
|
+
function singleBody(doc) {
|
|
23470
|
+
return {
|
|
23471
|
+
product: canonicalProduct(doc.product),
|
|
23472
|
+
nodes: sortNodes(doc.nodes ?? []).map(canonicalNode),
|
|
23473
|
+
edges: sortEdges(doc.edges ?? []).map(canonicalEdge)
|
|
23474
|
+
};
|
|
23475
|
+
}
|
|
23476
|
+
function portfolioBody(doc) {
|
|
23477
|
+
const products = [...doc.products ?? []].sort((a, b) => byCodeUnit(a.id ?? "", b.id ?? "")).map((p) => {
|
|
23478
|
+
const { nodes, edges, ...rest } = p;
|
|
23479
|
+
return {
|
|
23480
|
+
...canonicalProduct(rest),
|
|
23481
|
+
nodes: sortNodes(nodes ?? []).map(canonicalNode),
|
|
23482
|
+
edges: sortEdges(edges ?? []).map(canonicalEdge)
|
|
23483
|
+
};
|
|
23484
|
+
});
|
|
23485
|
+
return {
|
|
23486
|
+
organization: orderedObject(doc.organization, ["id", "title", "description", "logo_url", "industry"], { forceKeys: ["id", "title"] }),
|
|
23487
|
+
product_areas: [...doc.product_areas ?? []].sort((a, b) => byCodeUnit(a.id ?? "", b.id ?? "")).map((a) => orderedObject(a, ["id", "title", "description", "parent_area_id", "strategic_priority", "products"], { forceKeys: ["id", "title"] })),
|
|
23488
|
+
portfolios: [...doc.portfolios ?? []].sort((a, b) => byCodeUnit(a.id ?? "", b.id ?? "")).map((p) => orderedObject(p, ["id", "title", "description", "parent_portfolio_id", "hierarchy_model", "products"], { forceKeys: ["id", "title"] })),
|
|
23489
|
+
products,
|
|
23490
|
+
cross_edges: sortEdges(doc.cross_edges ?? []).map(canonicalCrossEdge)
|
|
23491
|
+
};
|
|
23492
|
+
}
|
|
23493
|
+
function computeBodyChecksum(doc) {
|
|
23494
|
+
const body = isPortfolio(doc) ? portfolioBody(doc) : singleBody(doc);
|
|
23495
|
+
const content = JSON.stringify(body);
|
|
23496
|
+
return createHash(INTEGRITY_HASH_PRIMITIVE).update(content).digest("hex").slice(0, INTEGRITY_DIGEST_HEX);
|
|
23497
|
+
}
|
|
23498
|
+
function isPortfolio(doc) {
|
|
23499
|
+
return doc.type === "portfolio" || "cross_edges" in doc;
|
|
23500
|
+
}
|
|
23501
|
+
function serializeCanonical(doc, opts = {}) {
|
|
23502
|
+
if (isPortfolio(doc)) return serializePortfolioWithHeader(doc, opts);
|
|
23503
|
+
return serializeSingleWithHeader(doc, opts);
|
|
23504
|
+
}
|
|
23505
|
+
function buildProvenance(doc, opts) {
|
|
23506
|
+
const source = opts.source ?? doc.source ?? { tool: "unknown" };
|
|
23507
|
+
return orderedObject(
|
|
23508
|
+
{
|
|
23509
|
+
tool: source.tool,
|
|
23510
|
+
tool_version: source.tool_version,
|
|
23511
|
+
workspace_id: source.workspace_id,
|
|
23512
|
+
exported_at: opts.exportedAt ?? doc.exported_at
|
|
23513
|
+
},
|
|
23514
|
+
["tool", "tool_version", "workspace_id", "exported_at"],
|
|
23515
|
+
{ forceKeys: ["tool"] }
|
|
23516
|
+
);
|
|
23517
|
+
}
|
|
23518
|
+
function serializeSingleWithHeader(doc, opts) {
|
|
23519
|
+
const body = singleBody(doc);
|
|
23520
|
+
const product = doc.product;
|
|
23521
|
+
const header = {
|
|
23522
|
+
format_version: UPG_CANONICAL_FORMAT_VERSION,
|
|
23523
|
+
spec_version: doc.upg_version,
|
|
23524
|
+
product: orderedObject(
|
|
23525
|
+
{ id: product.id, title: product.title, stage: product.stage },
|
|
23526
|
+
["id", "title", "stage"],
|
|
23527
|
+
{ forceKeys: ["id", "title"] }
|
|
23528
|
+
)
|
|
23529
|
+
};
|
|
23530
|
+
const summary = deriveSummary(product.description);
|
|
23531
|
+
if (summary) header.summary = summary;
|
|
23532
|
+
header.counts = { nodes: doc.nodes?.length ?? 0, edges: doc.edges?.length ?? 0 };
|
|
23533
|
+
header.provenance = buildProvenance(doc, opts);
|
|
23534
|
+
header.integrity = { algorithm: INTEGRITY_ALGORITHM, body: computeBodyChecksum(doc) };
|
|
23535
|
+
return JSON.stringify({ $upg: header, ...body }, null, 2) + "\n";
|
|
23536
|
+
}
|
|
23537
|
+
function serializePortfolioWithHeader(doc, opts) {
|
|
23538
|
+
const body = portfolioBody(doc);
|
|
23539
|
+
const org = doc.organization;
|
|
23540
|
+
const header = {
|
|
23541
|
+
format_version: UPG_CANONICAL_FORMAT_VERSION,
|
|
23542
|
+
spec_version: doc.upg_version,
|
|
23543
|
+
kind: "portfolio",
|
|
23544
|
+
organization: orderedObject(
|
|
23545
|
+
{ id: org.id, title: org.title },
|
|
23546
|
+
["id", "title"],
|
|
23547
|
+
{ forceKeys: ["id", "title"] }
|
|
23548
|
+
)
|
|
23549
|
+
};
|
|
23550
|
+
const summary = deriveSummary(org.description);
|
|
23551
|
+
if (summary) header.summary = summary;
|
|
23552
|
+
header.counts = {
|
|
23553
|
+
products: doc.products?.length ?? 0,
|
|
23554
|
+
product_areas: doc.product_areas?.length ?? 0,
|
|
23555
|
+
portfolios: doc.portfolios?.length ?? 0,
|
|
23556
|
+
cross_edges: doc.cross_edges?.length ?? 0
|
|
23557
|
+
};
|
|
23558
|
+
header.provenance = buildProvenance(doc, opts);
|
|
23559
|
+
header.integrity = { algorithm: INTEGRITY_ALGORITHM, body: computeBodyChecksum(doc) };
|
|
23560
|
+
return JSON.stringify({ $upg: header, ...body }, null, 2) + "\n";
|
|
23561
|
+
}
|
|
23562
|
+
function parseUpg(text) {
|
|
23563
|
+
return normalizeDocument(JSON.parse(text));
|
|
23564
|
+
}
|
|
23565
|
+
function normalizeDocument(obj) {
|
|
23566
|
+
const raw = obj;
|
|
23567
|
+
const header = raw.$upg;
|
|
23568
|
+
const upg_version = header?.spec_version ?? raw.upg_version;
|
|
23569
|
+
const provenance = header?.provenance;
|
|
23570
|
+
const exported_at = provenance?.exported_at ?? raw.exported_at;
|
|
23571
|
+
const source = provenance ? { tool: provenance.tool, tool_version: provenance.tool_version, workspace_id: provenance.workspace_id } : raw.source;
|
|
23572
|
+
const _integrity = header ? void 0 : raw._integrity;
|
|
23573
|
+
const isPortfolioDoc = raw.type === "portfolio" || header?.kind === "portfolio" || "cross_edges" in raw;
|
|
23574
|
+
if (isPortfolioDoc) {
|
|
23575
|
+
const out2 = {
|
|
23576
|
+
upg_version,
|
|
23577
|
+
type: "portfolio",
|
|
23578
|
+
exported_at: exported_at ?? "",
|
|
23579
|
+
source: source ?? { tool: "unknown" },
|
|
23580
|
+
organization: raw.organization,
|
|
23581
|
+
product_areas: raw.product_areas ?? [],
|
|
23582
|
+
portfolios: raw.portfolios ?? [],
|
|
23583
|
+
products: (raw.products ?? []).map((p) => ({
|
|
23584
|
+
...p,
|
|
23585
|
+
nodes: (p.nodes ?? []).map((n) => repairNodeDrift(n)),
|
|
23586
|
+
edges: p.edges ?? []
|
|
23587
|
+
})),
|
|
23588
|
+
cross_edges: raw.cross_edges ?? []
|
|
23589
|
+
};
|
|
23590
|
+
return out2;
|
|
23591
|
+
}
|
|
23592
|
+
const out = {
|
|
23593
|
+
upg_version,
|
|
23594
|
+
exported_at: exported_at ?? "",
|
|
23595
|
+
source: source ?? { tool: "unknown" },
|
|
23596
|
+
product: raw.product,
|
|
23597
|
+
nodes: (raw.nodes ?? []).map((n) => repairNodeDrift(n)),
|
|
23598
|
+
edges: raw.edges ?? []
|
|
23599
|
+
};
|
|
23600
|
+
if (_integrity) out._integrity = _integrity;
|
|
23601
|
+
return out;
|
|
23602
|
+
}
|
|
23603
|
+
function formatUpgText(text, opts = {}) {
|
|
23604
|
+
return serializeCanonical(parseUpg(text), opts);
|
|
23605
|
+
}
|
|
23606
|
+
function isCanonical(text) {
|
|
23607
|
+
const parsed = parseUpg(text);
|
|
23608
|
+
return text === serializeCanonical(parsed);
|
|
23609
|
+
}
|
|
23610
|
+
|
|
23319
23611
|
// src/index.ts
|
|
23320
23612
|
var UPG_VERSION = "0.7.3";
|
|
23321
23613
|
var UPG_FORMAT_VERSION = "0.4.0";
|
|
@@ -23410,6 +23702,7 @@ export {
|
|
|
23410
23702
|
UPG_ANTI_PATTERNS,
|
|
23411
23703
|
UPG_APPROACHES,
|
|
23412
23704
|
UPG_APPROACHES_BY_ID,
|
|
23705
|
+
UPG_CANONICAL_FORMAT_VERSION,
|
|
23413
23706
|
UPG_CONTAINMENT_FREE_TYPES,
|
|
23414
23707
|
UPG_COUNT_BENCHMARKS,
|
|
23415
23708
|
UPG_CROSS_EDGE_TYPES,
|
|
@@ -23473,8 +23766,10 @@ export {
|
|
|
23473
23766
|
coerceProductStage,
|
|
23474
23767
|
collectSlugsForType,
|
|
23475
23768
|
compareVersions,
|
|
23769
|
+
computeBodyChecksum,
|
|
23476
23770
|
evaluateAntiPatterns,
|
|
23477
23771
|
findInvalidStatusMigrationTargets,
|
|
23772
|
+
formatUpgText,
|
|
23478
23773
|
generateSlug,
|
|
23479
23774
|
getAnchorEntity,
|
|
23480
23775
|
getAntiPatternById,
|
|
@@ -23517,6 +23812,7 @@ export {
|
|
|
23517
23812
|
getValidChildren,
|
|
23518
23813
|
getVisibleTypes,
|
|
23519
23814
|
hasStatusMigration,
|
|
23815
|
+
isCanonical,
|
|
23520
23816
|
isCanonicalProductStage,
|
|
23521
23817
|
isContainmentFreeType,
|
|
23522
23818
|
isDeprecatedType,
|
|
@@ -23527,6 +23823,7 @@ export {
|
|
|
23527
23823
|
isLifecycleFreeType,
|
|
23528
23824
|
isLifecyclePlannedType,
|
|
23529
23825
|
isPolymorphicEdge,
|
|
23826
|
+
isPortfolio,
|
|
23530
23827
|
isRegisteredPolymorphicEdge,
|
|
23531
23828
|
isSubSequenceStep,
|
|
23532
23829
|
isUPGDocument,
|
|
@@ -23536,6 +23833,8 @@ export {
|
|
|
23536
23833
|
migrateNodeProperties,
|
|
23537
23834
|
migrateProductStage,
|
|
23538
23835
|
migrateStatusValue,
|
|
23836
|
+
normalizeDocument,
|
|
23837
|
+
parseUpg,
|
|
23539
23838
|
pickCanonicalEdge,
|
|
23540
23839
|
resolveAllEdges,
|
|
23541
23840
|
resolveContainmentEdge,
|
|
@@ -23543,6 +23842,7 @@ export {
|
|
|
23543
23842
|
resolveLabel,
|
|
23544
23843
|
resolveSlugCollision,
|
|
23545
23844
|
rotateSlug,
|
|
23845
|
+
serializeCanonical,
|
|
23546
23846
|
validateProductStageStrict,
|
|
23547
23847
|
validateUPGDocument,
|
|
23548
23848
|
validateUPGFramework,
|