@unified-product-graph/core 0.7.4 → 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/dist/index.cjs CHANGED
@@ -49,6 +49,7 @@ __export(index_exports, {
49
49
  UPG_ANTI_PATTERNS: () => UPG_ANTI_PATTERNS,
50
50
  UPG_APPROACHES: () => UPG_APPROACHES,
51
51
  UPG_APPROACHES_BY_ID: () => UPG_APPROACHES_BY_ID,
52
+ UPG_CANONICAL_FORMAT_VERSION: () => UPG_CANONICAL_FORMAT_VERSION,
52
53
  UPG_CONTAINMENT_FREE_TYPES: () => UPG_CONTAINMENT_FREE_TYPES,
53
54
  UPG_COUNT_BENCHMARKS: () => UPG_COUNT_BENCHMARKS,
54
55
  UPG_CROSS_EDGE_TYPES: () => UPG_CROSS_EDGE_TYPES,
@@ -112,8 +113,10 @@ __export(index_exports, {
112
113
  coerceProductStage: () => coerceProductStage,
113
114
  collectSlugsForType: () => collectSlugsForType,
114
115
  compareVersions: () => compareVersions,
116
+ computeBodyChecksum: () => computeBodyChecksum,
115
117
  evaluateAntiPatterns: () => evaluateAntiPatterns,
116
118
  findInvalidStatusMigrationTargets: () => findInvalidStatusMigrationTargets,
119
+ formatUpgText: () => formatUpgText,
117
120
  generateSlug: () => generateSlug,
118
121
  getAnchorEntity: () => getAnchorEntity,
119
122
  getAntiPatternById: () => getAntiPatternById,
@@ -156,6 +159,7 @@ __export(index_exports, {
156
159
  getValidChildren: () => getValidChildren,
157
160
  getVisibleTypes: () => getVisibleTypes,
158
161
  hasStatusMigration: () => hasStatusMigration,
162
+ isCanonical: () => isCanonical,
159
163
  isCanonicalProductStage: () => isCanonicalProductStage,
160
164
  isContainmentFreeType: () => isContainmentFreeType,
161
165
  isDeprecatedType: () => isDeprecatedType,
@@ -166,6 +170,7 @@ __export(index_exports, {
166
170
  isLifecycleFreeType: () => isLifecycleFreeType,
167
171
  isLifecyclePlannedType: () => isLifecyclePlannedType,
168
172
  isPolymorphicEdge: () => isPolymorphicEdge,
173
+ isPortfolio: () => isPortfolio,
169
174
  isRegisteredPolymorphicEdge: () => isRegisteredPolymorphicEdge,
170
175
  isSubSequenceStep: () => isSubSequenceStep,
171
176
  isUPGDocument: () => isUPGDocument,
@@ -175,6 +180,8 @@ __export(index_exports, {
175
180
  migrateNodeProperties: () => migrateNodeProperties,
176
181
  migrateProductStage: () => migrateProductStage,
177
182
  migrateStatusValue: () => migrateStatusValue,
183
+ normalizeDocument: () => normalizeDocument,
184
+ parseUpg: () => parseUpg,
178
185
  pickCanonicalEdge: () => pickCanonicalEdge,
179
186
  resolveAllEdges: () => resolveAllEdges,
180
187
  resolveContainmentEdge: () => resolveContainmentEdge,
@@ -182,6 +189,7 @@ __export(index_exports, {
182
189
  resolveLabel: () => resolveLabel,
183
190
  resolveSlugCollision: () => resolveSlugCollision,
184
191
  rotateSlug: () => rotateSlug,
192
+ serializeCanonical: () => serializeCanonical,
185
193
  validateProductStageStrict: () => validateProductStageStrict,
186
194
  validateUPGDocument: () => validateUPGDocument,
187
195
  validateUPGFramework: () => validateUPGFramework,
@@ -12821,26 +12829,6 @@ var UPG_FRAMEWORKS = [
12821
12829
  "label": "Items to score",
12822
12830
  "entityTypeId": "feature",
12823
12831
  "description": "Features, opportunities, or solutions being evaluated"
12824
- },
12825
- {
12826
- "label": "Reach",
12827
- "entityTypeId": "feature",
12828
- "description": "How many users does this affect per quarter?"
12829
- },
12830
- {
12831
- "label": "Impact",
12832
- "entityTypeId": "feature",
12833
- "description": "How much does this move the target outcome?"
12834
- },
12835
- {
12836
- "label": "Confidence",
12837
- "entityTypeId": "feature",
12838
- "description": "How sure are we about these estimates?"
12839
- },
12840
- {
12841
- "label": "Effort",
12842
- "entityTypeId": "feature",
12843
- "description": "How many person-months to build?"
12844
12832
  }
12845
12833
  ],
12846
12834
  "data": {
@@ -12854,31 +12842,35 @@ var UPG_FRAMEWORKS = [
12854
12842
  "feature": [
12855
12843
  {
12856
12844
  "property": "reach",
12857
- "type": "number",
12845
+ "type": "assessment",
12846
+ "scale_id": "reach_5",
12858
12847
  "required": true,
12859
12848
  "label": "Reach",
12860
12849
  "description": "How many users will this impact per quarter?"
12861
12850
  },
12862
12851
  {
12863
12852
  "property": "impact",
12864
- "type": "number",
12853
+ "type": "assessment",
12854
+ "scale_id": "impact_5",
12865
12855
  "required": true,
12866
12856
  "label": "Impact",
12867
- "description": "How much will this impact each user? (1=minimal, 2=low, 3=medium, 4=high, 5=massive)"
12857
+ "description": "How much will this impact each user, on the impact scale?"
12868
12858
  },
12869
12859
  {
12870
12860
  "property": "confidence",
12871
- "type": "number",
12861
+ "type": "assessment",
12862
+ "scale_id": "confidence_5",
12872
12863
  "required": true,
12873
12864
  "label": "Confidence",
12874
- "description": "How confident are you in the estimates? (0.5=low, 0.8=medium, 1.0=high)"
12865
+ "description": "How confident are you in the reach, impact, and effort estimates?"
12875
12866
  },
12876
12867
  {
12877
12868
  "property": "effort",
12878
- "type": "number",
12869
+ "type": "assessment",
12870
+ "scale_id": "effort_5",
12879
12871
  "required": true,
12880
12872
  "label": "Effort",
12881
- "description": "Person-months of effort required"
12873
+ "description": "How much work is required to build and ship this, on the effort scale?"
12882
12874
  }
12883
12875
  ]
12884
12876
  },
@@ -13072,7 +13064,7 @@ var UPG_FRAMEWORKS = [
13072
13064
  {
13073
13065
  "label": "Performance",
13074
13066
  "entityTypeId": "feature",
13075
- "description": "More is better, with linear satisfaction increase"
13067
+ "description": "More is better: linear satisfaction increase"
13076
13068
  },
13077
13069
  {
13078
13070
  "label": "Delighters",
@@ -13321,24 +13313,9 @@ var UPG_FRAMEWORKS = [
13321
13313
  ],
13322
13314
  "slots": [
13323
13315
  {
13324
- "label": "Must Have",
13325
- "entityTypeId": "feature",
13326
- "description": "Non-negotiable requirements for this release"
13327
- },
13328
- {
13329
- "label": "Should Have",
13330
- "entityTypeId": "feature",
13331
- "description": "Important but not critical; a workaround exists"
13332
- },
13333
- {
13334
- "label": "Could Have",
13335
- "entityTypeId": "feature",
13336
- "description": "Nice to have; include if time permits"
13337
- },
13338
- {
13339
- "label": "Won't Have",
13316
+ "label": "Requirements to categorise",
13340
13317
  "entityTypeId": "feature",
13341
- "description": "Explicitly out of scope for now"
13318
+ "description": "Features or requirements sorted into the four MoSCoW buckets"
13342
13319
  }
13343
13320
  ],
13344
13321
  "data": {
@@ -13348,7 +13325,23 @@ var UPG_FRAMEWORKS = [
13348
13325
  "role": "scored_item"
13349
13326
  }
13350
13327
  ],
13351
- "required_properties": {},
13328
+ "required_properties": {
13329
+ "feature": [
13330
+ {
13331
+ "property": "moscow",
13332
+ "type": "enum",
13333
+ "required": true,
13334
+ "label": "MoSCoW priority",
13335
+ "description": "Which scope bucket this requirement falls into for the current release",
13336
+ "enum_values": [
13337
+ "must",
13338
+ "should",
13339
+ "could",
13340
+ "wont"
13341
+ ]
13342
+ }
13343
+ ]
13344
+ },
13352
13345
  "computed_properties": [
13353
13346
  {
13354
13347
  "property": "must_have_ratio",
@@ -13368,28 +13361,19 @@ var UPG_FRAMEWORKS = [
13368
13361
  "columns": [
13369
13362
  {
13370
13363
  "property": "title",
13371
- "label": "Must Have",
13372
- "sortable": true
13373
- },
13374
- {
13375
- "property": "title",
13376
- "label": "Should Have",
13377
- "sortable": true
13378
- },
13379
- {
13380
- "property": "title",
13381
- "label": "Could Have",
13364
+ "label": "Requirement",
13382
13365
  "sortable": true
13383
13366
  },
13384
13367
  {
13385
- "property": "title",
13386
- "label": "Won't Have",
13387
- "sortable": true
13368
+ "property": "moscow",
13369
+ "label": "Priority",
13370
+ "sortable": true,
13371
+ "format": "badge"
13388
13372
  }
13389
13373
  ]
13390
13374
  },
13391
13375
  "sort_by": {
13392
- "property": "title",
13376
+ "property": "moscow",
13393
13377
  "direction": "asc"
13394
13378
  },
13395
13379
  "colour_by": "type",
@@ -13940,24 +13924,9 @@ var UPG_FRAMEWORKS = [
13940
13924
  ],
13941
13925
  "slots": [
13942
13926
  {
13943
- "label": "Deployment Frequency",
13927
+ "label": "Delivery metric",
13944
13928
  "entityTypeId": "metric",
13945
- "description": "How often you deploy to production"
13946
- },
13947
- {
13948
- "label": "Lead Time for Changes",
13949
- "entityTypeId": "metric",
13950
- "description": "Time from commit to production"
13951
- },
13952
- {
13953
- "label": "Change Failure Rate",
13954
- "entityTypeId": "metric",
13955
- "description": "Percentage of deployments causing failures"
13956
- },
13957
- {
13958
- "label": "Time to Restore",
13959
- "entityTypeId": "metric",
13960
- "description": "How long to recover from a failure"
13929
+ "description": "One of the four DORA software-delivery performance metrics"
13961
13930
  }
13962
13931
  ],
13963
13932
  "data": {
@@ -13967,7 +13936,36 @@ var UPG_FRAMEWORKS = [
13967
13936
  "role": "item"
13968
13937
  }
13969
13938
  ],
13970
- "required_properties": {},
13939
+ "required_properties": {
13940
+ "metric": [
13941
+ {
13942
+ "property": "dora_metric",
13943
+ "type": "enum",
13944
+ "required": true,
13945
+ "label": "DORA metric",
13946
+ "description": "Which of the four DORA metrics this measures",
13947
+ "enum_values": [
13948
+ "deployment_frequency",
13949
+ "lead_time_for_changes",
13950
+ "change_failure_rate",
13951
+ "time_to_restore"
13952
+ ]
13953
+ },
13954
+ {
13955
+ "property": "performance_tier",
13956
+ "type": "enum",
13957
+ "required": false,
13958
+ "label": "Performance tier",
13959
+ "description": "Where this metric sits on the DORA elite-to-low benchmark",
13960
+ "enum_values": [
13961
+ "elite",
13962
+ "high",
13963
+ "medium",
13964
+ "low"
13965
+ ]
13966
+ }
13967
+ ]
13968
+ },
13971
13969
  "computed_properties": [
13972
13970
  {
13973
13971
  "property": "stability_index",
@@ -14123,29 +14121,9 @@ var UPG_FRAMEWORKS = [
14123
14121
  ],
14124
14122
  "slots": [
14125
14123
  {
14126
- "label": "Acquisition",
14124
+ "label": "Lifecycle metric",
14127
14125
  "entityTypeId": "metric",
14128
- "description": "How do users find you?"
14129
- },
14130
- {
14131
- "label": "Activation",
14132
- "entityTypeId": "metric",
14133
- "description": "Do users have a great first experience?"
14134
- },
14135
- {
14136
- "label": "Retention",
14137
- "entityTypeId": "metric",
14138
- "description": "Do users come back?"
14139
- },
14140
- {
14141
- "label": "Revenue",
14142
- "entityTypeId": "metric",
14143
- "description": "Can you monetise the behaviour?"
14144
- },
14145
- {
14146
- "label": "Referral",
14147
- "entityTypeId": "metric",
14148
- "description": "Do users tell others?"
14126
+ "description": "A metric tracking one stage of the customer lifecycle funnel"
14149
14127
  }
14150
14128
  ],
14151
14129
  "data": {
@@ -14155,7 +14133,24 @@ var UPG_FRAMEWORKS = [
14155
14133
  "role": "item"
14156
14134
  }
14157
14135
  ],
14158
- "required_properties": {},
14136
+ "required_properties": {
14137
+ "metric": [
14138
+ {
14139
+ "property": "lifecycle_stage",
14140
+ "type": "enum",
14141
+ "required": true,
14142
+ "label": "Lifecycle stage",
14143
+ "description": "Which AARRR funnel stage this metric measures",
14144
+ "enum_values": [
14145
+ "acquisition",
14146
+ "activation",
14147
+ "retention",
14148
+ "revenue",
14149
+ "referral"
14150
+ ]
14151
+ }
14152
+ ]
14153
+ },
14159
14154
  "computed_properties": [
14160
14155
  {
14161
14156
  "property": "conversion_rate",
@@ -14167,7 +14162,39 @@ var UPG_FRAMEWORKS = [
14167
14162
  ]
14168
14163
  },
14169
14164
  "structure": {
14170
- "pattern": "funnel"
14165
+ "pattern": "funnel",
14166
+ "stages": [
14167
+ {
14168
+ "id": "acquisition",
14169
+ "label": "Acquisition",
14170
+ "order": 0,
14171
+ "entity_type": "metric"
14172
+ },
14173
+ {
14174
+ "id": "activation",
14175
+ "label": "Activation",
14176
+ "order": 1,
14177
+ "entity_type": "metric"
14178
+ },
14179
+ {
14180
+ "id": "retention",
14181
+ "label": "Retention",
14182
+ "order": 2,
14183
+ "entity_type": "metric"
14184
+ },
14185
+ {
14186
+ "id": "revenue",
14187
+ "label": "Revenue",
14188
+ "order": 3,
14189
+ "entity_type": "metric"
14190
+ },
14191
+ {
14192
+ "id": "referral",
14193
+ "label": "Referral",
14194
+ "order": 4,
14195
+ "entity_type": "metric"
14196
+ }
14197
+ ]
14171
14198
  },
14172
14199
  "presentation": {
14173
14200
  "layout": {
@@ -14214,24 +14241,9 @@ var UPG_FRAMEWORKS = [
14214
14241
  ],
14215
14242
  "slots": [
14216
14243
  {
14217
- "label": "North Star",
14218
- "entityTypeId": "metric",
14219
- "description": "The single metric reflecting core value delivered"
14220
- },
14221
- {
14222
- "label": "Input Metric 1",
14223
- "entityTypeId": "metric",
14224
- "description": "A driver metric you can directly influence"
14225
- },
14226
- {
14227
- "label": "Input Metric 2",
14228
- "entityTypeId": "metric",
14229
- "description": "A driver metric you can directly influence"
14230
- },
14231
- {
14232
- "label": "Input Metric 3",
14244
+ "label": "Metric",
14233
14245
  "entityTypeId": "metric",
14234
- "description": "A driver metric you can directly influence"
14246
+ "description": "The North Star metric or one of its 3-5 input (driver) metrics"
14235
14247
  }
14236
14248
  ],
14237
14249
  "data": {
@@ -14241,7 +14253,28 @@ var UPG_FRAMEWORKS = [
14241
14253
  "role": "item"
14242
14254
  }
14243
14255
  ],
14244
- "required_properties": {},
14256
+ "required_properties": {
14257
+ "metric": [
14258
+ {
14259
+ "property": "metric_role",
14260
+ "type": "enum",
14261
+ "required": true,
14262
+ "label": "Metric role",
14263
+ "description": "Whether this is the single North Star or a driver that feeds it",
14264
+ "enum_values": [
14265
+ "north_star",
14266
+ "input"
14267
+ ]
14268
+ },
14269
+ {
14270
+ "property": "leverage",
14271
+ "type": "number",
14272
+ "required": false,
14273
+ "label": "Leverage",
14274
+ "description": "How strongly this input metric moves the North Star (input metrics only)"
14275
+ }
14276
+ ]
14277
+ },
14245
14278
  "computed_properties": [
14246
14279
  {
14247
14280
  "property": "nsm_impact",
@@ -14541,7 +14574,7 @@ var UPG_FRAMEWORKS = [
14541
14574
  ]
14542
14575
  },
14543
14576
  "education": {
14544
- "purpose": "Design the product itself as the primary growth driver (free tier, self-serve onboarding, in-product virality), reducing dependence on sales-led acquisition.",
14577
+ "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.",
14545
14578
  "core_question": "Can our product acquire, activate, and expand users without human intervention, and where in the loop do we still need sales?",
14546
14579
  "when_to_use": [
14547
14580
  "You are launching a new product, feature, or entering a new market",
@@ -14951,7 +14984,7 @@ var UPG_FRAMEWORKS = [
14951
14984
  {
14952
14985
  "label": "Team",
14953
14986
  "entityTypeId": "team",
14954
- "description": "Team conducting the health check: all members vote anonymously on each dimension"
14987
+ "description": "Team conducting the health check; all members vote anonymously on each dimension"
14955
14988
  },
14956
14989
  {
14957
14990
  "label": "Retrospective",
@@ -15040,7 +15073,7 @@ var UPG_FRAMEWORKS = [
15040
15073
  },
15041
15074
  "education": {
15042
15075
  "purpose": "Regularly assess team health across dimensions (psychological safety, autonomy, mission clarity, fun, speed, learning) to catch problems early and celebrate strengths.",
15043
- "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?",
15076
+ "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?",
15044
15077
  "when_to_use": [
15045
15078
  "You need to improve team collaboration, clarity, or effectiveness",
15046
15079
  "Roles and responsibilities are unclear or causing friction",
@@ -15056,7 +15089,7 @@ var UPG_FRAMEWORKS = [
15056
15089
  "id": "raid-log",
15057
15090
  "name": "RAID Log",
15058
15091
  "version": "1.0.0",
15059
- "description": "A project management register tracking Risks, Assumptions, Issues, and Dependencies: the four categories most likely to derail a project if left unmanaged.",
15092
+ "description": "A project management register tracking Risks, Assumptions, Issues, and Dependencies, the four categories most likely to derail a project if left unmanaged.",
15060
15093
  "category": "program_mgmt",
15061
15094
  "origin": {
15062
15095
  "type": "practitioner",
@@ -23507,6 +23540,273 @@ function validateExpression(expr) {
23507
23540
  return null;
23508
23541
  }
23509
23542
 
23543
+ // src/format/canonical.ts
23544
+ var import_node_crypto = require("crypto");
23545
+ var UPG_CANONICAL_FORMAT_VERSION = "1.0.0";
23546
+ var INTEGRITY_HASH_PRIMITIVE = "sha256";
23547
+ var INTEGRITY_DIGEST_HEX = 32;
23548
+ var INTEGRITY_ALGORITHM = "sha256-128";
23549
+ function canonicalizeOpen(value) {
23550
+ if (Array.isArray(value)) return value.map(canonicalizeOpen);
23551
+ if (value !== null && typeof value === "object") {
23552
+ const out = {};
23553
+ for (const key of Object.keys(value).sort(byCodeUnit)) {
23554
+ out[key] = canonicalizeOpen(value[key]);
23555
+ }
23556
+ return out;
23557
+ }
23558
+ return value;
23559
+ }
23560
+ function byCodeUnit(a, b) {
23561
+ return a < b ? -1 : a > b ? 1 : 0;
23562
+ }
23563
+ function isEmpty(value) {
23564
+ if (value === null || value === void 0) return true;
23565
+ if (typeof value === "string") return value.length === 0;
23566
+ if (Array.isArray(value)) return value.length === 0;
23567
+ if (typeof value === "object") return Object.keys(value).length === 0;
23568
+ return false;
23569
+ }
23570
+ function orderedObject(source, keyOrder, opts = {}) {
23571
+ const force = new Set(opts.forceKeys ?? []);
23572
+ const open = new Set(opts.openKeys ?? []);
23573
+ const out = {};
23574
+ const emit = (key) => {
23575
+ if (!(key in source)) return;
23576
+ const raw = source[key];
23577
+ if (!force.has(key) && isEmpty(raw)) return;
23578
+ out[key] = open.has(key) ? canonicalizeOpen(raw) : raw;
23579
+ };
23580
+ for (const key of keyOrder) emit(key);
23581
+ for (const key of Object.keys(source).sort(byCodeUnit)) {
23582
+ if (!keyOrder.includes(key)) emit(key);
23583
+ }
23584
+ return out;
23585
+ }
23586
+ function repairNodeDrift(node) {
23587
+ const out = { ...node };
23588
+ if (typeof out.properties === "string") {
23589
+ out.properties = parseDriftString(out.properties, "object", `node ${node.id}.properties`);
23590
+ }
23591
+ if (typeof out.tags === "string") {
23592
+ out.tags = parseDriftString(out.tags, "array", `node ${node.id}.tags`);
23593
+ }
23594
+ return out;
23595
+ }
23596
+ function parseDriftString(raw, expect, where) {
23597
+ let parsed;
23598
+ try {
23599
+ parsed = JSON.parse(raw);
23600
+ } catch {
23601
+ throw new Error(`[upg fmt] ${where} is a string that is not valid JSON: ${truncate(raw)}`);
23602
+ }
23603
+ const ok = expect === "array" ? Array.isArray(parsed) : parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
23604
+ if (!ok) throw new Error(`[upg fmt] ${where} is a string that does not parse to a JSON ${expect}: ${truncate(raw)}`);
23605
+ return parsed;
23606
+ }
23607
+ function truncate(s) {
23608
+ return s.length > 60 ? s.slice(0, 57) + "..." : s;
23609
+ }
23610
+ function deriveSummary(description) {
23611
+ if (!description) return void 0;
23612
+ const firstLine = description.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
23613
+ if (!firstLine) return void 0;
23614
+ return firstLine.length > 200 ? firstLine.slice(0, 197) + "..." : firstLine;
23615
+ }
23616
+ function sortNodes(nodes) {
23617
+ return [...nodes].sort(
23618
+ (a, b) => byCodeUnit(a.type ?? "", b.type ?? "") || byCodeUnit(a.slug ?? a.title ?? "", b.slug ?? b.title ?? "") || byCodeUnit(a.id ?? "", b.id ?? "")
23619
+ );
23620
+ }
23621
+ function sortEdges(edges) {
23622
+ return [...edges].sort(
23623
+ (a, b) => byCodeUnit(a.source ?? "", b.source ?? "") || byCodeUnit(a.target ?? "", b.target ?? "") || byCodeUnit(a.type ?? "", b.type ?? "") || byCodeUnit(a.id ?? "", b.id ?? "")
23624
+ );
23625
+ }
23626
+ var NODE_KEY_ORDER = [
23627
+ "id",
23628
+ "type",
23629
+ "title",
23630
+ "slug",
23631
+ "aliases",
23632
+ "description",
23633
+ "tags",
23634
+ "status",
23635
+ "lifecycle_status",
23636
+ "source_id",
23637
+ "source_type",
23638
+ "mapping_confidence",
23639
+ "external_tool",
23640
+ "external_ref",
23641
+ "external_id",
23642
+ "sort_order",
23643
+ "properties"
23644
+ ];
23645
+ var EDGE_KEY_ORDER = ["id", "source", "target", "type", "mapping_confidence"];
23646
+ var CROSS_EDGE_KEY_ORDER = ["id", "source", "target", "type", "source_product_id", "target_product_id", "mapping_confidence"];
23647
+ var PRODUCT_KEY_ORDER = ["id", "title", "description", "stage", "properties"];
23648
+ function canonicalNode(node) {
23649
+ const repaired = repairNodeDrift(node);
23650
+ if (Array.isArray(repaired.tags)) {
23651
+ repaired.tags = [...new Set(repaired.tags)].sort(byCodeUnit);
23652
+ }
23653
+ return orderedObject(repaired, NODE_KEY_ORDER, { forceKeys: ["id", "type", "title"], openKeys: ["properties"] });
23654
+ }
23655
+ function canonicalEdge(edge) {
23656
+ return orderedObject(edge, EDGE_KEY_ORDER, {
23657
+ forceKeys: ["id", "source", "target", "type"]
23658
+ });
23659
+ }
23660
+ function canonicalCrossEdge(edge) {
23661
+ return orderedObject(edge, CROSS_EDGE_KEY_ORDER, {
23662
+ forceKeys: ["id", "source", "target", "type"]
23663
+ });
23664
+ }
23665
+ function canonicalProduct(product) {
23666
+ return orderedObject(product, PRODUCT_KEY_ORDER, { forceKeys: ["id", "title"], openKeys: ["properties"] });
23667
+ }
23668
+ function singleBody(doc) {
23669
+ return {
23670
+ product: canonicalProduct(doc.product),
23671
+ nodes: sortNodes(doc.nodes ?? []).map(canonicalNode),
23672
+ edges: sortEdges(doc.edges ?? []).map(canonicalEdge)
23673
+ };
23674
+ }
23675
+ function portfolioBody(doc) {
23676
+ const products = [...doc.products ?? []].sort((a, b) => byCodeUnit(a.id ?? "", b.id ?? "")).map((p) => {
23677
+ const { nodes, edges, ...rest } = p;
23678
+ return {
23679
+ ...canonicalProduct(rest),
23680
+ nodes: sortNodes(nodes ?? []).map(canonicalNode),
23681
+ edges: sortEdges(edges ?? []).map(canonicalEdge)
23682
+ };
23683
+ });
23684
+ return {
23685
+ organization: orderedObject(doc.organization, ["id", "title", "description", "logo_url", "industry"], { forceKeys: ["id", "title"] }),
23686
+ 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"] })),
23687
+ 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"] })),
23688
+ products,
23689
+ cross_edges: sortEdges(doc.cross_edges ?? []).map(canonicalCrossEdge)
23690
+ };
23691
+ }
23692
+ function computeBodyChecksum(doc) {
23693
+ const body = isPortfolio(doc) ? portfolioBody(doc) : singleBody(doc);
23694
+ const content = JSON.stringify(body);
23695
+ return (0, import_node_crypto.createHash)(INTEGRITY_HASH_PRIMITIVE).update(content).digest("hex").slice(0, INTEGRITY_DIGEST_HEX);
23696
+ }
23697
+ function isPortfolio(doc) {
23698
+ return doc.type === "portfolio" || "cross_edges" in doc;
23699
+ }
23700
+ function serializeCanonical(doc, opts = {}) {
23701
+ if (isPortfolio(doc)) return serializePortfolioWithHeader(doc, opts);
23702
+ return serializeSingleWithHeader(doc, opts);
23703
+ }
23704
+ function buildProvenance(doc, opts) {
23705
+ const source = opts.source ?? doc.source ?? { tool: "unknown" };
23706
+ return orderedObject(
23707
+ {
23708
+ tool: source.tool,
23709
+ tool_version: source.tool_version,
23710
+ workspace_id: source.workspace_id,
23711
+ exported_at: opts.exportedAt ?? doc.exported_at
23712
+ },
23713
+ ["tool", "tool_version", "workspace_id", "exported_at"],
23714
+ { forceKeys: ["tool"] }
23715
+ );
23716
+ }
23717
+ function serializeSingleWithHeader(doc, opts) {
23718
+ const body = singleBody(doc);
23719
+ const product = doc.product;
23720
+ const header = {
23721
+ format_version: UPG_CANONICAL_FORMAT_VERSION,
23722
+ spec_version: doc.upg_version,
23723
+ product: orderedObject(
23724
+ { id: product.id, title: product.title, stage: product.stage },
23725
+ ["id", "title", "stage"],
23726
+ { forceKeys: ["id", "title"] }
23727
+ )
23728
+ };
23729
+ const summary = deriveSummary(product.description);
23730
+ if (summary) header.summary = summary;
23731
+ header.counts = { nodes: doc.nodes?.length ?? 0, edges: doc.edges?.length ?? 0 };
23732
+ header.provenance = buildProvenance(doc, opts);
23733
+ header.integrity = { algorithm: INTEGRITY_ALGORITHM, body: computeBodyChecksum(doc) };
23734
+ return JSON.stringify({ $upg: header, ...body }, null, 2) + "\n";
23735
+ }
23736
+ function serializePortfolioWithHeader(doc, opts) {
23737
+ const body = portfolioBody(doc);
23738
+ const org = doc.organization;
23739
+ const header = {
23740
+ format_version: UPG_CANONICAL_FORMAT_VERSION,
23741
+ spec_version: doc.upg_version,
23742
+ kind: "portfolio",
23743
+ organization: orderedObject(
23744
+ { id: org.id, title: org.title },
23745
+ ["id", "title"],
23746
+ { forceKeys: ["id", "title"] }
23747
+ )
23748
+ };
23749
+ const summary = deriveSummary(org.description);
23750
+ if (summary) header.summary = summary;
23751
+ header.counts = {
23752
+ products: doc.products?.length ?? 0,
23753
+ product_areas: doc.product_areas?.length ?? 0,
23754
+ portfolios: doc.portfolios?.length ?? 0,
23755
+ cross_edges: doc.cross_edges?.length ?? 0
23756
+ };
23757
+ header.provenance = buildProvenance(doc, opts);
23758
+ header.integrity = { algorithm: INTEGRITY_ALGORITHM, body: computeBodyChecksum(doc) };
23759
+ return JSON.stringify({ $upg: header, ...body }, null, 2) + "\n";
23760
+ }
23761
+ function parseUpg(text) {
23762
+ return normalizeDocument(JSON.parse(text));
23763
+ }
23764
+ function normalizeDocument(obj) {
23765
+ const raw = obj;
23766
+ const header = raw.$upg;
23767
+ const upg_version = header?.spec_version ?? raw.upg_version;
23768
+ const provenance = header?.provenance;
23769
+ const exported_at = provenance?.exported_at ?? raw.exported_at;
23770
+ const source = provenance ? { tool: provenance.tool, tool_version: provenance.tool_version, workspace_id: provenance.workspace_id } : raw.source;
23771
+ const _integrity = header ? void 0 : raw._integrity;
23772
+ const isPortfolioDoc = raw.type === "portfolio" || header?.kind === "portfolio" || "cross_edges" in raw;
23773
+ if (isPortfolioDoc) {
23774
+ const out2 = {
23775
+ upg_version,
23776
+ type: "portfolio",
23777
+ exported_at: exported_at ?? "",
23778
+ source: source ?? { tool: "unknown" },
23779
+ organization: raw.organization,
23780
+ product_areas: raw.product_areas ?? [],
23781
+ portfolios: raw.portfolios ?? [],
23782
+ products: (raw.products ?? []).map((p) => ({
23783
+ ...p,
23784
+ nodes: (p.nodes ?? []).map((n) => repairNodeDrift(n)),
23785
+ edges: p.edges ?? []
23786
+ })),
23787
+ cross_edges: raw.cross_edges ?? []
23788
+ };
23789
+ return out2;
23790
+ }
23791
+ const out = {
23792
+ upg_version,
23793
+ exported_at: exported_at ?? "",
23794
+ source: source ?? { tool: "unknown" },
23795
+ product: raw.product,
23796
+ nodes: (raw.nodes ?? []).map((n) => repairNodeDrift(n)),
23797
+ edges: raw.edges ?? []
23798
+ };
23799
+ if (_integrity) out._integrity = _integrity;
23800
+ return out;
23801
+ }
23802
+ function formatUpgText(text, opts = {}) {
23803
+ return serializeCanonical(parseUpg(text), opts);
23804
+ }
23805
+ function isCanonical(text) {
23806
+ const parsed = parseUpg(text);
23807
+ return text === serializeCanonical(parsed);
23808
+ }
23809
+
23510
23810
  // src/index.ts
23511
23811
  var UPG_VERSION = "0.7.3";
23512
23812
  var UPG_FORMAT_VERSION = "0.4.0";
@@ -23602,6 +23902,7 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
23602
23902
  UPG_ANTI_PATTERNS,
23603
23903
  UPG_APPROACHES,
23604
23904
  UPG_APPROACHES_BY_ID,
23905
+ UPG_CANONICAL_FORMAT_VERSION,
23605
23906
  UPG_CONTAINMENT_FREE_TYPES,
23606
23907
  UPG_COUNT_BENCHMARKS,
23607
23908
  UPG_CROSS_EDGE_TYPES,
@@ -23665,8 +23966,10 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
23665
23966
  coerceProductStage,
23666
23967
  collectSlugsForType,
23667
23968
  compareVersions,
23969
+ computeBodyChecksum,
23668
23970
  evaluateAntiPatterns,
23669
23971
  findInvalidStatusMigrationTargets,
23972
+ formatUpgText,
23670
23973
  generateSlug,
23671
23974
  getAnchorEntity,
23672
23975
  getAntiPatternById,
@@ -23709,6 +24012,7 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
23709
24012
  getValidChildren,
23710
24013
  getVisibleTypes,
23711
24014
  hasStatusMigration,
24015
+ isCanonical,
23712
24016
  isCanonicalProductStage,
23713
24017
  isContainmentFreeType,
23714
24018
  isDeprecatedType,
@@ -23719,6 +24023,7 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
23719
24023
  isLifecycleFreeType,
23720
24024
  isLifecyclePlannedType,
23721
24025
  isPolymorphicEdge,
24026
+ isPortfolio,
23722
24027
  isRegisteredPolymorphicEdge,
23723
24028
  isSubSequenceStep,
23724
24029
  isUPGDocument,
@@ -23728,6 +24033,8 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
23728
24033
  migrateNodeProperties,
23729
24034
  migrateProductStage,
23730
24035
  migrateStatusValue,
24036
+ normalizeDocument,
24037
+ parseUpg,
23731
24038
  pickCanonicalEdge,
23732
24039
  resolveAllEdges,
23733
24040
  resolveContainmentEdge,
@@ -23735,6 +24042,7 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
23735
24042
  resolveLabel,
23736
24043
  resolveSlugCollision,
23737
24044
  rotateSlug,
24045
+ serializeCanonical,
23738
24046
  validateProductStageStrict,
23739
24047
  validateUPGDocument,
23740
24048
  validateUPGFramework,