@particle-academy/fancy-conformance 0.21.2 → 0.22.0

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/VERSION CHANGED
@@ -1 +1 @@
1
- 0.21.2
1
+ 0.22.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-academy/fancy-conformance",
3
- "version": "0.21.2",
3
+ "version": "0.22.0",
4
4
  "description": "Shared cross-language conformance fixtures for the Fancy suite. One contract, N implementations, and a single table that every implementation asserts in its own CI \u2014 so 'parity' is a test result rather than a claim. Ships the fixture data itself, so a Rust, Go or Python runner can consume it without a JavaScript toolchain.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -42,7 +42,7 @@
42
42
  "fill": "8B5CF6",
43
43
  "align": "left",
44
44
  "anchor": "middle",
45
- "fontSizeHundredths": "1400",
45
+ "fontSizeHundredths": "1050",
46
46
  "letterSpacingHundredths": "0",
47
47
  "caps": "none",
48
48
  "padding": {
@@ -116,7 +116,7 @@
116
116
  "fill": null,
117
117
  "align": "left",
118
118
  "anchor": "middle",
119
- "fontSizeHundredths": "1400",
119
+ "fontSizeHundredths": "1050",
120
120
  "letterSpacingHundredths": "0",
121
121
  "caps": "none",
122
122
  "padding": {
@@ -189,7 +189,7 @@
189
189
  "fill": "F8FAFC",
190
190
  "align": "left",
191
191
  "anchor": "middle",
192
- "fontSizeHundredths": "1400",
192
+ "fontSizeHundredths": "1050",
193
193
  "letterSpacingHundredths": "0",
194
194
  "caps": "none",
195
195
  "padding": {
@@ -264,7 +264,7 @@
264
264
  "fill": "1B3A5C",
265
265
  "align": "left",
266
266
  "anchor": "middle",
267
- "fontSizeHundredths": "1400",
267
+ "fontSizeHundredths": "1050",
268
268
  "letterSpacingHundredths": "0",
269
269
  "caps": "none",
270
270
  "padding": {
@@ -337,7 +337,7 @@
337
337
  "fill": null,
338
338
  "align": "left",
339
339
  "anchor": "middle",
340
- "fontSizeHundredths": "1400",
340
+ "fontSizeHundredths": "1050",
341
341
  "letterSpacingHundredths": "0",
342
342
  "caps": "none",
343
343
  "padding": {
@@ -401,7 +401,7 @@
401
401
  "fill": null,
402
402
  "align": "left",
403
403
  "anchor": "middle",
404
- "fontSizeHundredths": "1400",
404
+ "fontSizeHundredths": "1050",
405
405
  "letterSpacingHundredths": "0",
406
406
  "caps": "none",
407
407
  "padding": {
@@ -413,17 +413,17 @@
413
413
  "borders": {
414
414
  "left": null,
415
415
  "right": {
416
- "widthEmu": "12700",
416
+ "widthEmu": "4763",
417
417
  "color": "D8E0E8",
418
418
  "style": "solid"
419
419
  },
420
420
  "top": {
421
- "widthEmu": "12700",
421
+ "widthEmu": "4763",
422
422
  "color": "D8E0E8",
423
423
  "style": "solid"
424
424
  },
425
425
  "bottom": {
426
- "widthEmu": "12700",
426
+ "widthEmu": "4763",
427
427
  "color": "D8E0E8",
428
428
  "style": "solid"
429
429
  }
@@ -484,7 +484,7 @@
484
484
  "fill": "8B5CF6",
485
485
  "align": "left",
486
486
  "anchor": "middle",
487
- "fontSizeHundredths": "1400",
487
+ "fontSizeHundredths": "1050",
488
488
  "letterSpacingHundredths": "0",
489
489
  "caps": "none",
490
490
  "padding": {
@@ -495,22 +495,22 @@
495
495
  },
496
496
  "borders": {
497
497
  "left": {
498
- "widthEmu": "25400",
498
+ "widthEmu": "9525",
499
499
  "color": "1B3A5C",
500
500
  "style": "solid"
501
501
  },
502
502
  "right": {
503
- "widthEmu": "6350",
503
+ "widthEmu": "2381",
504
504
  "color": "D8E0E8",
505
505
  "style": "solid"
506
506
  },
507
507
  "top": {
508
- "widthEmu": "25400",
508
+ "widthEmu": "9525",
509
509
  "color": "1B3A5C",
510
510
  "style": "solid"
511
511
  },
512
512
  "bottom": {
513
- "widthEmu": "6350",
513
+ "widthEmu": "2381",
514
514
  "color": "D8E0E8",
515
515
  "style": "solid"
516
516
  }
@@ -572,7 +572,7 @@
572
572
  "fill": null,
573
573
  "align": "left",
574
574
  "anchor": "middle",
575
- "fontSizeHundredths": "1400",
575
+ "fontSizeHundredths": "1050",
576
576
  "letterSpacingHundredths": "0",
577
577
  "caps": "none",
578
578
  "padding": {
@@ -583,22 +583,22 @@
583
583
  },
584
584
  "borders": {
585
585
  "left": {
586
- "widthEmu": "6350",
586
+ "widthEmu": "2381",
587
587
  "color": "D8E0E8",
588
588
  "style": "solid"
589
589
  },
590
590
  "right": {
591
- "widthEmu": "25400",
591
+ "widthEmu": "9525",
592
592
  "color": "1B3A5C",
593
593
  "style": "solid"
594
594
  },
595
595
  "top": {
596
- "widthEmu": "6350",
596
+ "widthEmu": "2381",
597
597
  "color": "D8E0E8",
598
598
  "style": "solid"
599
599
  },
600
600
  "bottom": {
601
- "widthEmu": "25400",
601
+ "widthEmu": "9525",
602
602
  "color": "1B3A5C",
603
603
  "style": "solid"
604
604
  }
@@ -645,7 +645,7 @@
645
645
  "fill": null,
646
646
  "align": "left",
647
647
  "anchor": "middle",
648
- "fontSizeHundredths": "1400",
648
+ "fontSizeHundredths": "1050",
649
649
  "letterSpacingHundredths": "0",
650
650
  "caps": "none",
651
651
  "padding": {
@@ -721,7 +721,7 @@
721
721
  "fill": "8B5CF6",
722
722
  "align": "left",
723
723
  "anchor": "middle",
724
- "fontSizeHundredths": "1400",
724
+ "fontSizeHundredths": "1050",
725
725
  "letterSpacingHundredths": "0",
726
726
  "caps": "none",
727
727
  "padding": {
@@ -798,7 +798,7 @@
798
798
  "fill": "8B5CF6",
799
799
  "align": "right",
800
800
  "anchor": "middle",
801
- "fontSizeHundredths": "1400",
801
+ "fontSizeHundredths": "1050",
802
802
  "letterSpacingHundredths": "0",
803
803
  "caps": "none",
804
804
  "padding": {
@@ -873,7 +873,7 @@
873
873
  "fill": null,
874
874
  "align": "center",
875
875
  "anchor": "middle",
876
- "fontSizeHundredths": "1400",
876
+ "fontSizeHundredths": "1050",
877
877
  "letterSpacingHundredths": "0",
878
878
  "caps": "none",
879
879
  "padding": {
@@ -950,7 +950,7 @@
950
950
  "fill": null,
951
951
  "align": "left",
952
952
  "anchor": "middle",
953
- "fontSizeHundredths": "1400",
953
+ "fontSizeHundredths": "1050",
954
954
  "letterSpacingHundredths": "0",
955
955
  "caps": "none",
956
956
  "padding": {
@@ -1041,8 +1041,8 @@
1041
1041
  "fill": "0E7C86",
1042
1042
  "align": "left",
1043
1043
  "anchor": "middle",
1044
- "fontSizeHundredths": "1400",
1045
- "letterSpacingHundredths": "60",
1044
+ "fontSizeHundredths": "1050",
1045
+ "letterSpacingHundredths": "23",
1046
1046
  "caps": "none",
1047
1047
  "padding": {
1048
1048
  "left": "91440",
@@ -1117,13 +1117,13 @@
1117
1117
  "fill": null,
1118
1118
  "align": "left",
1119
1119
  "anchor": "middle",
1120
- "fontSizeHundredths": "1400",
1120
+ "fontSizeHundredths": "1050",
1121
1121
  "letterSpacingHundredths": "0",
1122
1122
  "caps": "none",
1123
1123
  "padding": {
1124
- "left": "152400",
1124
+ "left": "57150",
1125
1125
  "right": "91440",
1126
- "top": "38100",
1126
+ "top": "14288",
1127
1127
  "bottom": "45720"
1128
1128
  },
1129
1129
  "borders": {
@@ -1210,7 +1210,7 @@
1210
1210
  "fill": null,
1211
1211
  "align": "left",
1212
1212
  "anchor": "middle",
1213
- "fontSizeHundredths": "1400",
1213
+ "fontSizeHundredths": "1050",
1214
1214
  "letterSpacingHundredths": "0",
1215
1215
  "caps": "none",
1216
1216
  "padding": {
@@ -1221,17 +1221,17 @@
1221
1221
  },
1222
1222
  "borders": {
1223
1223
  "left": {
1224
- "widthEmu": "9525",
1224
+ "widthEmu": "3572",
1225
1225
  "color": "D9DEE4",
1226
1226
  "style": "solid"
1227
1227
  },
1228
1228
  "right": {
1229
- "widthEmu": "9525",
1229
+ "widthEmu": "3572",
1230
1230
  "color": "D9DEE4",
1231
1231
  "style": "solid"
1232
1232
  },
1233
1233
  "top": {
1234
- "widthEmu": "9525",
1234
+ "widthEmu": "3572",
1235
1235
  "color": "D9DEE4",
1236
1236
  "style": "solid"
1237
1237
  },
@@ -1402,7 +1402,7 @@
1402
1402
  "fill": null,
1403
1403
  "align": "left",
1404
1404
  "anchor": "middle",
1405
- "fontSizeHundredths": "1400",
1405
+ "fontSizeHundredths": "1050",
1406
1406
  "letterSpacingHundredths": "0",
1407
1407
  "caps": "none",
1408
1408
  "padding": {
@@ -1486,7 +1486,7 @@
1486
1486
  "fill": null,
1487
1487
  "align": "left",
1488
1488
  "anchor": "middle",
1489
- "fontSizeHundredths": "1400",
1489
+ "fontSizeHundredths": "1050",
1490
1490
  "letterSpacingHundredths": "0",
1491
1491
  "caps": "none",
1492
1492
  "padding": {
@@ -1572,7 +1572,7 @@
1572
1572
  "fill": "F8FAFC",
1573
1573
  "align": "left",
1574
1574
  "anchor": "middle",
1575
- "fontSizeHundredths": "1400",
1575
+ "fontSizeHundredths": "1050",
1576
1576
  "letterSpacingHundredths": "0",
1577
1577
  "caps": "none",
1578
1578
  "padding": {
@@ -1648,8 +1648,8 @@
1648
1648
  "fill": "8B5CF6",
1649
1649
  "align": "left",
1650
1650
  "anchor": "middle",
1651
- "fontSizeHundredths": "1400",
1652
- "letterSpacingHundredths": "120",
1651
+ "fontSizeHundredths": "1050",
1652
+ "letterSpacingHundredths": "45",
1653
1653
  "caps": "small",
1654
1654
  "padding": {
1655
1655
  "left": "91440",
@@ -1724,7 +1724,7 @@
1724
1724
  "fill": null,
1725
1725
  "align": "left",
1726
1726
  "anchor": "middle",
1727
- "fontSizeHundredths": "1400",
1727
+ "fontSizeHundredths": "1050",
1728
1728
  "letterSpacingHundredths": "0",
1729
1729
  "caps": "none",
1730
1730
  "padding": {
@@ -1798,7 +1798,7 @@
1798
1798
  "fill": null,
1799
1799
  "align": "left",
1800
1800
  "anchor": "middle",
1801
- "fontSizeHundredths": "1400",
1801
+ "fontSizeHundredths": "1050",
1802
1802
  "letterSpacingHundredths": "0",
1803
1803
  "caps": "none",
1804
1804
  "padding": {
@@ -1835,10 +1835,10 @@
1835
1835
  }
1836
1836
  },
1837
1837
  {
1838
- "id": "0026-font-size-is-halved-into-points",
1839
- "title": "A table's fontSize is halved into points, as it is everywhere else in the schema",
1838
+ "id": "0026-font-size-is-a-design-pixel",
1839
+ "title": "A table's fontSize is a design pixel: 26 on the default 1920 canvas is 9.75pt",
1840
1840
  "since": "0.7.0",
1841
- "notes": "The halving is a schema-wide convention (fancy-slides designs against a 1920px width), not a table quirk. A port that takes the number as points renders every table at double size and nothing else disagrees.",
1841
+ "notes": "Every length in a deck is a pixel on the design canvas (theme.slideWidth, 1920 by default) and keeps its share of the slide: points = px * 720 / slideWidth. Until dark-slide 0.10 a table's fontSize was halved into points instead (26 -> 13pt), which made PowerPoint text a third larger than fancy-slides draws it. A port still halving fails here and nowhere else.",
1842
1842
  "fn": "resolvedCell",
1843
1843
  "input": {
1844
1844
  "element": {
@@ -1872,7 +1872,7 @@
1872
1872
  "fill": null,
1873
1873
  "align": "left",
1874
1874
  "anchor": "middle",
1875
- "fontSizeHundredths": "1300",
1875
+ "fontSizeHundredths": "975",
1876
1876
  "letterSpacingHundredths": "0",
1877
1877
  "caps": "none",
1878
1878
  "padding": {
@@ -1907,6 +1907,168 @@
1907
1907
  "rowSpan": "1",
1908
1908
  "merged": "none"
1909
1909
  }
1910
+ },
1911
+ {
1912
+ "id": "0027-slide-width-1440-reproduces-the-old-halving",
1913
+ "title": "theme.slideWidth 1440 lands every length where dark-slide 0.9 put it",
1914
+ "since": "0.22.0",
1915
+ "notes": "720 / 1440 is exactly the old halving, which makes 1440 the documented way to keep a 0.9 deck's output. fontSize 26 is 13pt as before; lengths that 0.9 took as points are doubled in the input (letterSpacing 2.4 for 1.2pt, padding 18 for 9pt, border width 3 for 1.5pt) and land on the same points.",
1916
+ "fn": "resolvedCell",
1917
+ "input": {
1918
+ "element": {
1919
+ "type": "table",
1920
+ "w": 0.9,
1921
+ "columns": [
1922
+ {
1923
+ "key": "a",
1924
+ "label": "A"
1925
+ }
1926
+ ],
1927
+ "rows": [
1928
+ {
1929
+ "a": "1"
1930
+ }
1931
+ ],
1932
+ "style": {
1933
+ "fontSize": 26,
1934
+ "letterSpacing": 2.4,
1935
+ "padding": 18,
1936
+ "borders": {
1937
+ "width": 3
1938
+ }
1939
+ }
1940
+ },
1941
+ "theme": {
1942
+ "slideWidth": 1440
1943
+ },
1944
+ "row": 1,
1945
+ "col": 0
1946
+ },
1947
+ "expected": {
1948
+ "text": "1",
1949
+ "bold": "0",
1950
+ "italic": "0",
1951
+ "underline": "0",
1952
+ "color": "0F172A",
1953
+ "fill": null,
1954
+ "align": "left",
1955
+ "anchor": "middle",
1956
+ "fontSizeHundredths": "1300",
1957
+ "letterSpacingHundredths": "120",
1958
+ "caps": "none",
1959
+ "padding": {
1960
+ "left": "114300",
1961
+ "right": "114300",
1962
+ "top": "114300",
1963
+ "bottom": "114300"
1964
+ },
1965
+ "borders": {
1966
+ "left": {
1967
+ "widthEmu": "19050",
1968
+ "color": "D9DEE4",
1969
+ "style": "solid"
1970
+ },
1971
+ "right": {
1972
+ "widthEmu": "19050",
1973
+ "color": "D9DEE4",
1974
+ "style": "solid"
1975
+ },
1976
+ "top": {
1977
+ "widthEmu": "19050",
1978
+ "color": "D9DEE4",
1979
+ "style": "solid"
1980
+ },
1981
+ "bottom": {
1982
+ "widthEmu": "19050",
1983
+ "color": "D9DEE4",
1984
+ "style": "solid"
1985
+ }
1986
+ },
1987
+ "colSpan": "1",
1988
+ "rowSpan": "1",
1989
+ "merged": "none"
1990
+ }
1991
+ },
1992
+ {
1993
+ "id": "0028-authored-lengths-convert-defaults-stay-points",
1994
+ "title": "A stated padding and border width are design pixels; the insets and rule nobody stated stay PowerPoint's points",
1995
+ "since": "0.22.0",
1996
+ "notes": "On the default canvas a stated padding of 32 is 12pt and a stated border width of 8 is 3pt. The top and bottom insets here are NOT stated, so they stay PowerPoint's own 3.6pt; a port that scaled the defaults too would shrink every table that never asked for different insets. Values chosen so no conversion lands on a rounding tie.",
1997
+ "fn": "resolvedCell",
1998
+ "input": {
1999
+ "element": {
2000
+ "type": "table",
2001
+ "w": 0.9,
2002
+ "columns": [
2003
+ {
2004
+ "key": "a",
2005
+ "label": "A"
2006
+ }
2007
+ ],
2008
+ "rows": [
2009
+ {
2010
+ "a": "1"
2011
+ }
2012
+ ],
2013
+ "style": {
2014
+ "padding": {
2015
+ "left": 32,
2016
+ "right": 32
2017
+ },
2018
+ "borders": {
2019
+ "width": 8,
2020
+ "color": "#1B3A5C"
2021
+ }
2022
+ }
2023
+ },
2024
+ "theme": {},
2025
+ "row": 1,
2026
+ "col": 0
2027
+ },
2028
+ "expected": {
2029
+ "text": "1",
2030
+ "bold": "0",
2031
+ "italic": "0",
2032
+ "underline": "0",
2033
+ "color": "0F172A",
2034
+ "fill": null,
2035
+ "align": "left",
2036
+ "anchor": "middle",
2037
+ "fontSizeHundredths": "1050",
2038
+ "letterSpacingHundredths": "0",
2039
+ "caps": "none",
2040
+ "padding": {
2041
+ "left": "152400",
2042
+ "right": "152400",
2043
+ "top": "45720",
2044
+ "bottom": "45720"
2045
+ },
2046
+ "borders": {
2047
+ "left": {
2048
+ "widthEmu": "38100",
2049
+ "color": "1B3A5C",
2050
+ "style": "solid"
2051
+ },
2052
+ "right": {
2053
+ "widthEmu": "38100",
2054
+ "color": "1B3A5C",
2055
+ "style": "solid"
2056
+ },
2057
+ "top": {
2058
+ "widthEmu": "38100",
2059
+ "color": "1B3A5C",
2060
+ "style": "solid"
2061
+ },
2062
+ "bottom": {
2063
+ "widthEmu": "38100",
2064
+ "color": "1B3A5C",
2065
+ "style": "solid"
2066
+ }
2067
+ },
2068
+ "colSpan": "1",
2069
+ "rowSpan": "1",
2070
+ "merged": "none"
2071
+ }
1910
2072
  }
1911
2073
  ]
1912
2074
  }
@@ -29,6 +29,7 @@
29
29
  "AN ABSENT KEY AND A FALSE KEY ARE DIFFERENT, and conflating them is the most likely port bug in the whole model. Absent falls through to the next layer; `false` STOPS the chain and means off. `borders: false` on a table with a header must therefore beat the header band's own borders, and a row that sets `borders: {bottom: false}` must keep the other three sides. Cases 0005, 0006 and 0016 pin all three directions; a port written with `??` / `or` / `.get(k, default)` passes the first and fails the others.",
30
30
  "COLUMN WIDTHS ARE TWO MODES CHOSEN BY THE VALUES. Every declared width <= 1 makes them FRACTIONS of the table and undeclared columns share the remainder; any declared width > 1 makes them WEIGHTS and undeclared columns weigh 1. No width anywhere is an equal split. Case 0018 pins the boundary at exactly 1.0, which is the value that decides the mode.",
31
31
  "THE WIDTHS MUST SUM TO THE TABLE EXACTLY, so they are accumulated and differenced rather than each rounded independently. Case 0017's goldens are NOT totalEmu * frac rounded per column — 0019 exists specifically because independent rounding gives a different, plausible-looking answer that leaves the grid a few EMU off the frame.",
32
+ "EVERY AUTHORED LENGTH IS A DESIGN PIXEL, since dark-slide 0.10 (suite 0.22.0). fontSize, letterSpacing, padding, border width and row height are pixels on a canvas theme.slideWidth wide (1920 by default) and resolve to points = px * 720 / slideWidth, computed in exactly that order so every engine rounds the same double. Defaults that are PowerPoint's own (the 7.2pt / 3.6pt insets, the 0.75pt rule) stay points. Before 0.10 a fontSize was halved and every other length taken as points; case 0026 pins the new conversion, 0027 that slideWidth 1440 reproduces the old one exactly, and 0028 that a stated length converts while an unstated default does not. Goldens are regenerated by RUNNING the reference: `python scripts/build-table-cell-model-goldens.py --write`, which without --write is a drift check.",
32
33
  "SPANS ARE CLAMPED TO THE GRID, never emitted as written. A colSpan of 99 on a two-column table is a 2 (case 0020). A row with fewer <a:tc> than the grid declares is a corrupt file rather than a narrow table, so the covered cells are still emitted, carrying `merged` and an empty text.",
33
34
  "THE SIBLING SUITE IS last-word/docx-constructs, and the two packages fail on the SAME axis — per-cell borders, insets, vertical alignment, merging — because both models were narrower than the XML their writers already emitted. That suite pins WordprocessingML output; this one pins a resolver. They are deliberately not the same shape: the XML cannot be shared (w:tcBorders / w:tcMar / w:vAlign against a:lnL..lnB / marL..marB / @anchor) but the DECISIONS can, and this file is expressed in points-and-hex for that reason. Promoting it to `shared/` is the right move the day last-word resolves through the same model; declaring it shared before then would list an implementation that does not run it, which is the failure this repository exists to prevent."
34
35
  ]
@@ -35,7 +35,7 @@
35
35
  "notes": [
36
36
  "WHY THIS IS NOT `flow/graph-runs`. That suite's contract specifies a LENIENT import, a LOCAL kind registry carrying only the built-in and structural kinds, and the built-in offline executors. A connector kind is none of those. A connector case dropped into graph-runs would not be red-until-an-implementation-catches-up — it would be red against a contract that excludes it, permanently, which is not what \"lands here first, red\" means. This suite exists to state the different prerequisite out loud: its runners CARRY the connector packages.",
37
37
 
38
- "IMPORT IS STRICT HERE, and graph-runs is lenient. That difference is deliberate and worth stating rather than inheriting: a document one engine accepts and another refuses is not one document. It is also live `fancy-flow-php`'s `FancyFlowManager::toGraph()` forces `lenient: true` on every `run()` caller while the TypeScript and Python twins default strict, so the same versionless graph runs in Laravel and is refused by the editor meant to edit it. Ungating the version check is queued on the PHP side; until it lands, a PHP runner for this suite must call the strict import path explicitly rather than going through `run()`.",
38
+ "IMPORT IS STRICT HERE, and graph-runs is lenient. That difference is deliberate and worth stating rather than inheriting: a document one engine accepts and another refuses is not one document. What `lenient` softens is VOCABULARY, a kind the host has not registered, and that is exactly what this suite must not soften: a connector package whose registrar did not run would otherwise import with a warning and fail later, at the node. So a runner still imports strictly, and a PHP runner still calls the strict import path explicitly, because `fancy-flow-php`'s `FancyFlowManager::toGraph()` forces `lenient: true` on every `run()` caller. The schema VERSION split this note used to record is CLOSED: `lenient` stopped softening the version in fancy-flow 0.70.0, fancy-flow-php 0.52.0 and fancy-flow (Python) 0.20.0, so a document without `version: 1` is refused by every import in every runtime, and fancy-flow-php's `run()` throws for it rather than running it. Before that, the same versionless graph ran in Laravel and was refused by a default (strict) import in the TypeScript and Python twins.",
39
39
 
40
40
  "A RUN IDENTITY MUST BE SUPPLIED. The connector derives an idempotency key from it, and some connectors refuse to write without one — so a runner that omits it is not exercising the same code path. Derive it from the graph so every runner sends the same key; the reference derivation is `\"lab\" + sha256(canonicalJson(input.schema))[0:8]`, and `canonicalJson` is DEFINED on the case under `input.runIdentity.canonicalJson` — sorted keys, no whitespace, unescaped slashes and unicode. Not defined here as well, so there is one definition rather than two that can drift. That was originally kept short to dodge a defect in `fancy-connector-core` — `Idempotency::fit()` clamped only at the 255-character catalogue ceiling and never at the provider's own declared `idempotencyMaxLength`, so Discord (25) refused a legitimate 29-character key. Fixed in fancy-connector-core 0.4.0; the short derivation is kept here because a stable short key is good practice, not because it is still required.",
41
41