@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.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": "number",
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": "number",
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? (1=minimal, 2=low, 3=medium, 4=high, 5=massive)"
12658
+ "description": "How much will this impact each user, on the impact scale?"
12677
12659
  },
12678
12660
  {
12679
12661
  "property": "confidence",
12680
- "type": "number",
12662
+ "type": "assessment",
12663
+ "scale_id": "confidence_5",
12681
12664
  "required": true,
12682
12665
  "label": "Confidence",
12683
- "description": "How confident are you in the estimates? (0.5=low, 0.8=medium, 1.0=high)"
12666
+ "description": "How confident are you in the reach, impact, and effort estimates?"
12684
12667
  },
12685
12668
  {
12686
12669
  "property": "effort",
12687
- "type": "number",
12670
+ "type": "assessment",
12671
+ "scale_id": "effort_5",
12688
12672
  "required": true,
12689
12673
  "label": "Effort",
12690
- "description": "Person-months of effort required"
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, with linear satisfaction increase"
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": "Must Have",
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": "Explicitly out of scope for now"
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": "Must Have",
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": "title",
13195
- "label": "Won't Have",
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": "title",
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": "Deployment Frequency",
13728
+ "label": "Delivery metric",
13753
13729
  "entityTypeId": "metric",
13754
- "description": "How often you deploy to production"
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": "Acquisition",
13925
+ "label": "Lifecycle metric",
13936
13926
  "entityTypeId": "metric",
13937
- "description": "How do users find you?"
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": "North Star",
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": "A driver metric you can directly influence"
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), reducing dependence on sales-led acquisition.",
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: all members vote anonymously on each dimension"
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? Where do they feel strong, where do they feel stuck, and what has changed since last check?",
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: the four categories most likely to derail a project if left unmanaged.",
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,