@bongos/core 1.19.534 → 1.19.535

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.
@@ -3,17 +3,17 @@
3
3
  "generator": "scripts/gds/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 1652,
7
- "unique_ids": 1433,
8
- "certain": 1652,
6
+ "strings": 1654,
7
+ "unique_ids": 1434,
8
+ "certain": 1654,
9
9
  "heuristic": 0,
10
- "duplicate_ids": 154
10
+ "duplicate_ids": 155
11
11
  },
12
12
  "surfaces": {
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
- "files": 73,
16
- "strings": 1187
15
+ "files": 74,
16
+ "strings": 1189
17
17
  },
18
18
  "status-dashboard": {
19
19
  "at": "modules/status-ui/public",
@@ -146,7 +146,7 @@
146
146
  "modules/hall-ui/public/hall-render.js:605",
147
147
  "modules/hall-ui/public/idea.js:207",
148
148
  "modules/hall-ui/public/ideas.js:476",
149
- "modules/hall-ui/public/watch.js:1021"
149
+ "modules/hall-ui/public/watch.js:1137"
150
150
  ]
151
151
  },
152
152
  {
@@ -218,7 +218,7 @@
218
218
  "modules/hall-ui/public/gate.js:219",
219
219
  "modules/hall-ui/public/government.js:1104",
220
220
  "modules/hall-ui/public/harbor.js:337",
221
- "modules/hall-ui/public/watch.js:1050"
221
+ "modules/hall-ui/public/watch.js:1166"
222
222
  ]
223
223
  },
224
224
  {
@@ -1073,6 +1073,16 @@
1073
1073
  "modules/hall-ui/public/ranks.html:6"
1074
1074
  ]
1075
1075
  },
1076
+ {
1077
+ "id": "b03e17693dcd",
1078
+ "surface": "builders-hall",
1079
+ "text": "Read it as a table",
1080
+ "occurrences": 2,
1081
+ "at": [
1082
+ "modules/hall-ui/public/drachmae.js:148",
1083
+ "modules/hall-ui/public/watch.js:1027"
1084
+ ]
1085
+ },
1076
1086
  {
1077
1087
  "id": "664f5731ddc3",
1078
1088
  "surface": "landing",
@@ -1450,7 +1460,7 @@
1450
1460
  "occurrences": 2,
1451
1461
  "at": [
1452
1462
  "modules/hall-ui/public/roadmap.js:108",
1453
- "modules/hall-ui/public/roadmap.js:263"
1463
+ "modules/hall-ui/public/roadmap.js:275"
1454
1464
  ]
1455
1465
  },
1456
1466
  {
@@ -1620,7 +1630,7 @@
1620
1630
  "occurrences": 2,
1621
1631
  "at": [
1622
1632
  "modules/hall-ui/public/roadmap.js:74",
1623
- "modules/hall-ui/public/roadmap.js:273"
1633
+ "modules/hall-ui/public/roadmap.js:285"
1624
1634
  ]
1625
1635
  },
1626
1636
  {
@@ -2361,7 +2371,7 @@
2361
2371
  "surface": "builders-hall",
2362
2372
  "text": "Audit log",
2363
2373
  "file": "modules/hall-ui/public/watch.html",
2364
- "line": 144,
2374
+ "line": 148,
2365
2375
  "origin": "html-text",
2366
2376
  "confidence": "certain"
2367
2377
  },
@@ -3540,7 +3550,7 @@
3540
3550
  "surface": "builders-hall",
3541
3551
  "text": "Could not load this page.",
3542
3552
  "file": "modules/hall-ui/public/watch.js",
3543
- "line": 1096,
3553
+ "line": 1212,
3544
3554
  "origin": "js-notice",
3545
3555
  "confidence": "certain"
3546
3556
  },
@@ -3882,7 +3892,7 @@
3882
3892
  "surface": "builders-hall",
3883
3893
  "text": "Could not verify your access. Go back and try again.",
3884
3894
  "file": "modules/hall-ui/public/watch.js",
3885
- "line": 1095,
3895
+ "line": 1211,
3886
3896
  "origin": "js-text-assign",
3887
3897
  "confidence": "certain"
3888
3898
  },
@@ -4008,7 +4018,7 @@
4008
4018
  "surface": "builders-hall",
4009
4019
  "text": "Dated exploit-detail engagement reports — kept out of the repo (threat-model §6.3) and served only to Archons. Upload via POST /api/bongos/security/docs.",
4010
4020
  "file": "modules/hall-ui/public/watch.html",
4011
- "line": 134,
4021
+ "line": 138,
4012
4022
  "origin": "html-text",
4013
4023
  "confidence": "certain"
4014
4024
  },
@@ -4350,7 +4360,7 @@
4350
4360
  "surface": "builders-hall",
4351
4361
  "text": "Engagement reports",
4352
4362
  "file": "modules/hall-ui/public/watch.html",
4353
- "line": 133,
4363
+ "line": 137,
4354
4364
  "origin": "html-text",
4355
4365
  "confidence": "certain"
4356
4366
  },
@@ -4368,7 +4378,7 @@
4368
4378
  "surface": "builders-hall",
4369
4379
  "text": "Estimation accuracy",
4370
4380
  "file": "modules/hall-ui/public/watch.html",
4371
- "line": 151,
4381
+ "line": 155,
4372
4382
  "origin": "html-text",
4373
4383
  "confidence": "certain"
4374
4384
  },
@@ -5034,7 +5044,7 @@
5034
5044
  "surface": "builders-hall",
5035
5045
  "text": "How task estimates compare with the time work actually took.",
5036
5046
  "file": "modules/hall-ui/public/watch.html",
5037
- "line": 152,
5047
+ "line": 156,
5038
5048
  "origin": "html-text",
5039
5049
  "confidence": "certain"
5040
5050
  },
@@ -5070,7 +5080,7 @@
5070
5080
  "surface": "builders-hall",
5071
5081
  "text": "How the whole build is doing — the program at a glance, then each version. Shipped work stays out of the way.",
5072
5082
  "file": "modules/hall-ui/public/roadmap.js",
5073
- "line": 400,
5083
+ "line": 416,
5074
5084
  "origin": "js-text-assign",
5075
5085
  "confidence": "certain"
5076
5086
  },
@@ -6321,7 +6331,7 @@
6321
6331
  "surface": "builders-hall",
6322
6332
  "text": "No version is in flight right now.",
6323
6333
  "file": "modules/hall-ui/public/roadmap.js",
6324
- "line": 380,
6334
+ "line": 392,
6325
6335
  "origin": "js-markup",
6326
6336
  "confidence": "certain"
6327
6337
  },
@@ -6330,7 +6340,7 @@
6330
6340
  "surface": "builders-hall",
6331
6341
  "text": "No versions declared yet.",
6332
6342
  "file": "modules/hall-ui/public/roadmap.js",
6333
- "line": 394,
6343
+ "line": 410,
6334
6344
  "origin": "js-markup",
6335
6345
  "confidence": "certain"
6336
6346
  },
@@ -6784,6 +6794,15 @@
6784
6794
  "origin": "js-markup",
6785
6795
  "confidence": "certain"
6786
6796
  },
6797
+ {
6798
+ "id": "d89d1923ecf9",
6799
+ "surface": "builders-hall",
6800
+ "text": "Over the last {…} days",
6801
+ "file": "modules/hall-ui/public/watch.js",
6802
+ "line": 1054,
6803
+ "origin": "js-markup",
6804
+ "confidence": "certain"
6805
+ },
6787
6806
  {
6788
6807
  "id": "695ffff13730",
6789
6808
  "surface": "builders-hall",
@@ -6798,7 +6817,7 @@
6798
6817
  "surface": "builders-hall",
6799
6818
  "text": "Override requests",
6800
6819
  "file": "modules/hall-ui/public/watch.html",
6801
- "line": 139,
6820
+ "line": 143,
6802
6821
  "origin": "html-text",
6803
6822
  "confidence": "certain"
6804
6823
  },
@@ -7198,6 +7217,15 @@
7198
7217
  "origin": "js-markup",
7199
7218
  "confidence": "certain"
7200
7219
  },
7220
+ {
7221
+ "id": "b03e17693dcd",
7222
+ "surface": "builders-hall",
7223
+ "text": "Read it as a table",
7224
+ "file": "modules/hall-ui/public/watch.js",
7225
+ "line": 1027,
7226
+ "origin": "js-markup",
7227
+ "confidence": "certain"
7228
+ },
7201
7229
  {
7202
7230
  "id": "b6fcfdd652a7",
7203
7231
  "surface": "builders-hall",
@@ -7653,7 +7681,7 @@
7653
7681
  "surface": "builders-hall",
7654
7682
  "text": "Restricted.",
7655
7683
  "file": "modules/hall-ui/public/watch.js",
7656
- "line": 1050,
7684
+ "line": 1166,
7657
7685
  "origin": "js-text-assign",
7658
7686
  "confidence": "certain"
7659
7687
  },
@@ -8382,7 +8410,7 @@
8382
8410
  "surface": "builders-hall",
8383
8411
  "text": "System oversight: people, operations and security.",
8384
8412
  "file": "modules/hall-ui/public/watch.js",
8385
- "line": 1109,
8413
+ "line": 1225,
8386
8414
  "origin": "js-text-assign",
8387
8415
  "confidence": "certain"
8388
8416
  },
@@ -8832,7 +8860,7 @@
8832
8860
  "surface": "builders-hall",
8833
8861
  "text": "The roadmap could not be loaded right now. Reload to try again.",
8834
8862
  "file": "modules/hall-ui/public/roadmap.js",
8835
- "line": 407,
8863
+ "line": 423,
8836
8864
  "origin": "js-markup",
8837
8865
  "confidence": "certain"
8838
8866
  },
@@ -8841,7 +8869,7 @@
8841
8869
  "surface": "builders-hall",
8842
8870
  "text": "The roadmap failed to load. Reload to try again.",
8843
8871
  "file": "modules/hall-ui/public/roadmap.js",
8844
- "line": 408,
8872
+ "line": 424,
8845
8873
  "origin": "js-notice",
8846
8874
  "confidence": "certain"
8847
8875
  },
@@ -9408,7 +9436,7 @@
9408
9436
  "surface": "builders-hall",
9409
9437
  "text": "Vulnerability reports",
9410
9438
  "file": "modules/hall-ui/public/watch.html",
9411
- "line": 128,
9439
+ "line": 132,
9412
9440
  "origin": "html-text",
9413
9441
  "confidence": "certain"
9414
9442
  },
@@ -9417,7 +9445,7 @@
9417
9445
  "surface": "builders-hall",
9418
9446
  "text": "Vulnerability reports, engagement reports, override requests, and the audit log.",
9419
9447
  "file": "modules/hall-ui/public/watch.html",
9420
- "line": 125,
9448
+ "line": 129,
9421
9449
  "origin": "html-text",
9422
9450
  "confidence": "certain"
9423
9451
  },
@@ -10236,7 +10264,7 @@
10236
10264
  "surface": "builders-hall",
10237
10265
  "text": "achieved ·",
10238
10266
  "file": "modules/hall-ui/public/roadmap.js",
10239
- "line": 263,
10267
+ "line": 275,
10240
10268
  "origin": "js-markup",
10241
10269
  "confidence": "certain"
10242
10270
  },
@@ -10704,7 +10732,7 @@
10704
10732
  "surface": "builders-hall",
10705
10733
  "text": "in progress ·",
10706
10734
  "file": "modules/hall-ui/public/roadmap.js",
10707
- "line": 263,
10735
+ "line": 275,
10708
10736
  "origin": "js-markup",
10709
10737
  "confidence": "certain"
10710
10738
  },
@@ -10956,7 +10984,7 @@
10956
10984
  "surface": "builders-hall",
10957
10985
  "text": "not started",
10958
10986
  "file": "modules/hall-ui/public/roadmap.js",
10959
- "line": 263,
10987
+ "line": 275,
10960
10988
  "origin": "js-markup",
10961
10989
  "confidence": "certain"
10962
10990
  },
@@ -11001,7 +11029,7 @@
11001
11029
  "surface": "builders-hall",
11002
11030
  "text": "of all in-flight tasks shipped",
11003
11031
  "file": "modules/hall-ui/public/roadmap.js",
11004
- "line": 273,
11032
+ "line": 285,
11005
11033
  "origin": "js-markup",
11006
11034
  "confidence": "certain"
11007
11035
  },
@@ -11028,7 +11056,7 @@
11028
11056
  "surface": "builders-hall",
11029
11057
  "text": "of {…}",
11030
11058
  "file": "modules/hall-ui/public/roadmap.js",
11031
- "line": 263,
11059
+ "line": 275,
11032
11060
  "origin": "js-markup",
11033
11061
  "confidence": "certain"
11034
11062
  },
@@ -11208,7 +11236,7 @@
11208
11236
  "surface": "builders-hall",
11209
11237
  "text": "shipped (live)",
11210
11238
  "file": "modules/hall-ui/public/roadmap.js",
11211
- "line": 273,
11239
+ "line": 285,
11212
11240
  "origin": "js-markup",
11213
11241
  "confidence": "certain"
11214
11242
  },
@@ -11316,7 +11344,7 @@
11316
11344
  "surface": "builders-hall",
11317
11345
  "text": "task {…}",
11318
11346
  "file": "modules/hall-ui/public/watch.js",
11319
- "line": 1021,
11347
+ "line": 1137,
11320
11348
  "origin": "js-markup",
11321
11349
  "confidence": "certain"
11322
11350
  },
@@ -11325,7 +11353,7 @@
11325
11353
  "surface": "builders-hall",
11326
11354
  "text": "tasks remaining",
11327
11355
  "file": "modules/hall-ui/public/roadmap.js",
11328
- "line": 273,
11356
+ "line": 285,
11329
11357
  "origin": "js-markup",
11330
11358
  "confidence": "certain"
11331
11359
  },
@@ -11442,7 +11470,7 @@
11442
11470
  "surface": "builders-hall",
11443
11471
  "text": "versions in flight",
11444
11472
  "file": "modules/hall-ui/public/roadmap.js",
11445
- "line": 273,
11473
+ "line": 285,
11446
11474
  "origin": "js-markup",
11447
11475
  "confidence": "certain"
11448
11476
  },
@@ -11829,7 +11857,7 @@
11829
11857
  "surface": "builders-hall",
11830
11858
  "text": "{…} shipped, {…} confirmed, {…} completed, {…} remaining of {…}",
11831
11859
  "file": "modules/hall-ui/public/roadmap.js",
11832
- "line": 273,
11860
+ "line": 285,
11833
11861
  "origin": "js-markup",
11834
11862
  "confidence": "certain"
11835
11863
  },
@@ -11838,7 +11866,7 @@
11838
11866
  "surface": "builders-hall",
11839
11867
  "text": "{…} tasks · {…}",
11840
11868
  "file": "modules/hall-ui/public/roadmap.js",
11841
- "line": 390,
11869
+ "line": 406,
11842
11870
  "origin": "js-markup",
11843
11871
  "confidence": "certain"
11844
11872
  },
@@ -1517,5 +1517,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1517
1517
  joinabilityMode did at 1.19.374.
1518
1518
  1.19.534 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1519
1519
  landed since 1.19.533 with no explicit bump. run 33885355024. (task 1002620)
1520
+ 1.19.535 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1521
+ landed since 1.19.534 with no explicit bump. run 33886686350. (task 1002620)
1520
1522
  ---------------------------------------------------------------------------
1521
1523
  ```
@@ -737,13 +737,25 @@
737
737
  + '</svg>';
738
738
  }
739
739
 
740
- // Horizontal labelled bars. rows: [{ label, value, valueLabel }].
740
+ // Horizontal labelled bars. rows: [{ label, value, start, valueLabel }].
741
741
  // opts: { max, tone: 'accent'|'neutral', labelWidth, valueWidth, barHeight,
742
742
  // pill, ariaLabel, list }
743
743
  //
744
744
  // `max` is the caller's when several series must share one scale; otherwise
745
745
  // the largest value here. It floors at 1 so an all-zero set draws empty
746
746
  // tracks instead of dividing by zero.
747
+ //
748
+ // A row's optional `start` OFFSETS its bar instead of beginning it at the
749
+ // track's left edge, which turns the same primitive into a SPAN series — a
750
+ // timeline. It arrived with the roadmap's versions (task 1003447): each one
751
+ // holds a `started_at` and a `shipped_at`, and the series those two dates
752
+ // support is "when did each version run", every bar on one shared axis. That
753
+ // is a bar with a left offset and nothing else, so it is an OPTION here
754
+ // rather than a second class family — the same seam every other difference
755
+ // between call sites rides. `start` is in the SAME units as `value` and
756
+ // measured from the axis origin, so the kit still never learns what a date
757
+ // is: a caller passes `max` as the axis span and `start` as the offset into
758
+ // it, exactly as it already passes x0/x1/yMax to the plot.
747
759
  function barSeriesHtml(rows, opts = {}) {
748
760
  const list = (rows || []).filter((r) => r && r.label !== undefined && r.label !== null);
749
761
  if (!list.length) return '';
@@ -775,14 +787,34 @@
775
787
  + (opts.ariaLabel ? ` aria-label="${esc(opts.ariaLabel)}"` : '')
776
788
  + '>'
777
789
  + list.map((r, i) => {
778
- const w = Math.round((vals[i] / max) * 100);
790
+ // A POSITIVE value always draws at least a sliver. Rounding alone sends
791
+ // anything under half a percent of the max to `width:0%`, which reads as
792
+ // "none" — the one thing it is not. Zero stays zero: an empty track is
793
+ // the honest picture of nothing, and the column series makes the
794
+ // opposite choice for the opposite reason (a zero bucket there is a week
795
+ // that happened). Load-bearing for a span: a version that started and
796
+ // shipped inside one day is a real span half a pixel wide.
797
+ const raw = Math.round((vals[i] / max) * 100);
798
+ const w = vals[i] > 0 ? Math.max(1, raw) : raw;
799
+ // The offset takes the SAME floor, and for the same reason: a span that
800
+ // begins just after the axis origin but rounds to 0% draws flush against
801
+ // the track edge, which is the picture of "started first" — and on a
802
+ // timeline that is a claim about ordering, not a rounding artefact.
803
+ const start = Number(r.start);
804
+ const rawOff = Number.isFinite(start) && start > 0 ? Math.round((start / max) * 100) : 0;
805
+ const off = Number.isFinite(start) && start > 0 ? Math.max(1, rawOff) : 0;
806
+ // Offset by MARGIN, not `left`: the fill is an ordinary block inside the
807
+ // track, so absolute positioning would need a positioned ancestor the
808
+ // track does not declare. Omitted entirely at zero, so every existing
809
+ // caller's markup is byte-identical to what it was.
810
+ const geom = off > 0 ? `margin-left:${off}%;width:${w}%` : `width:${w}%`;
779
811
  const val = r.valueLabel === undefined || r.valueLabel === null
780
812
  ? String(vals[i])
781
813
  : String(r.valueLabel);
782
814
  return `<div class="kit-bars__row"${rowRole}>`
783
815
  + `<span class="kit-bars__label">${esc(r.label)}</span>`
784
816
  + `<span class="kit-bars__track" aria-hidden="true">`
785
- + `<span class="kit-bars__fill" style="width:${w}%"></span></span>`
817
+ + `<span class="kit-bars__fill" style="${geom}"></span></span>`
786
818
  + `<span class="kit-bars__val">${esc(val)}</span>`
787
819
  + '</div>';
788
820
  }).join('')
@@ -818,10 +850,60 @@
818
850
  return { width, height, pad, base: height - pad.b };
819
851
  }
820
852
 
853
+ // The VERTICAL window a series should be drawn in — the companion to
854
+ // timeStepGeometry's yMin/yMax, and geometry rather than date semantics, which
855
+ // is why it lives here rather than at the call site (task 1003447).
856
+ //
857
+ // `zeroBased` (the default) is right whenever zero is a state the series can
858
+ // actually reach: a count of dead exports sitting at zero for a week is the
859
+ // reading, and lifting the floor off it would invent a change. Pass false for
860
+ // a LEVEL — repo-health's total_loc — where zero is unreachable and a zero
861
+ // floor squashes a month of real movement into a flat band across the top.
862
+ // A fitted floor magnifies every wiggle, so a caller that passes false owes
863
+ // the reader the range in words somewhere on the page.
864
+ //
865
+ // `air` is the fraction of the range left as breathing room (12% by default)
866
+ // so the line neither rides the top edge nor welds itself to the axis. A FLAT
867
+ // series has no range to take a fraction of, so the air falls back to the
868
+ // value's own magnitude — and, for a series flat at zero, to 1, which leaves
869
+ // it drawn honestly along the axis.
870
+ //
871
+ // A MISSING entry is dropped, not read as zero: a null from a snapshot that
872
+ // predates a column would otherwise drag `lo` to zero and draw a cliff nothing
873
+ // recorded. Note this is the one place in the kit that must NOT go through
874
+ // `chartNums` — that maps a null to 0 on purpose, because a gap-filled DAILY
875
+ // feed really does mean "nothing happened that day". Here a null means "not
876
+ // measured", and the two are opposite readings of the same byte. `Number(null)`
877
+ // and `Number('')` are both a finite 0, so the emptiness has to be tested for
878
+ // before the coercion, never after it.
879
+ function plotWindow(values, opts = {}) {
880
+ const nums = (values || [])
881
+ .filter((v) => v !== null && v !== undefined && v !== '')
882
+ .map(Number)
883
+ .filter((n) => Number.isFinite(n));
884
+ if (!nums.length) return { lo: 0, hi: 0, yMin: 0, yMax: 1, n: 0 };
885
+ const lo = Math.min(...nums);
886
+ const hi = Math.max(...nums);
887
+ const frac = Number.isFinite(Number(opts.air)) ? Number(opts.air) : 0.12;
888
+ const air = ((hi - lo) || Math.abs(hi) || 1) * frac;
889
+ const zeroBased = opts.zeroBased !== false;
890
+ return { lo, hi, yMin: zeroBased ? 0 : lo - air, yMax: hi + air, n: nums.length };
891
+ }
892
+
821
893
  // A STEP line over a time axis: the value holds until the next recorded
822
894
  // point, then rises — which is what a cumulative "how much by now" series
823
895
  // means, and why this is not a straight polyline between samples.
824
896
  // points: [{ x, value }] in data space, sorted. Returns { d, area, xs }.
897
+ //
898
+ // `yMin` (default 0) is the value the AXIS sits at, and it exists because the
899
+ // second kind of series this plot draws is not cumulative. A cumulative line
900
+ // starts at nothing and every reading is "how much by now", so zero is the
901
+ // only honest floor. A LEVEL — repo-health's total_loc, task 1003447 — is a
902
+ // large number that moves a little: on a zero floor its whole month of change
903
+ // is a flat band across the top, and the chart says nothing the headline
904
+ // figure did not. A caller that fits the floor to the data MUST say so on the
905
+ // page (watch.js puts the range in the figure's foot), because a non-zero
906
+ // baseline silently multiplies how big every wiggle looks.
825
907
  function timeStepGeometry(points, opts = {}) {
826
908
  const { width, height, pad, base } = plotBox(opts);
827
909
  const list = points || [];
@@ -829,11 +911,14 @@
829
911
  const x0 = Number(opts.x0 ?? list[0].x);
830
912
  const x1 = Number(opts.x1 ?? list[list.length - 1].x);
831
913
  const last = list[list.length - 1];
832
- // A span of 0 (one point, no window) would divide by zero; yMax 0 likewise.
914
+ // A span of 0 (one point, no window) would divide by zero; a flat series
915
+ // whose yMin and yMax are equal likewise.
833
916
  const spanX = (x1 - x0) || 1;
834
- const yMax = Number(opts.yMax ?? last.value) || 1;
917
+ const yMin = Number.isFinite(Number(opts.yMin)) ? Number(opts.yMin) : 0;
918
+ const yMax = Number(opts.yMax ?? last.value);
919
+ const spanY = ((Number.isFinite(yMax) ? yMax : 0) - yMin) || 1;
835
920
  const px = (t) => pad.l + ((t - x0) / spanX) * (width - pad.l - pad.r);
836
- const py = (v) => height - pad.b - (v / yMax) * (height - pad.t - pad.b);
921
+ const py = (v) => height - pad.b - ((v - yMin) / spanY) * (height - pad.t - pad.b);
837
922
  let d = `M${px(list[0].x).toFixed(1)},${py(list[0].value).toFixed(1)}`;
838
923
  list.forEach((p, i) => {
839
924
  if (i === 0) return;
@@ -961,7 +1046,8 @@
961
1046
  plotSvgHtml, plotValueLabelHtml, timeStepSeriesHtml, timeStepGeometry, columnSeriesHtml,
962
1047
  // plotBox is exported so a caller mapping pointer x back into viewBox
963
1048
  // space reads the plot's width instead of re-declaring the number.
964
- plotBox,
1049
+ // plotWindow fits the vertical axis to a level series (task 1003447).
1050
+ plotBox, plotWindow,
965
1051
  // states
966
1052
  emptyStateHtml, skeletonRows, collapsibleCard,
967
1053
  // ship-time task visual (task 1003109)
@@ -0,0 +1,105 @@
1
+ // roadmap-timeline.js — the program in time (task 1003447, criterion
2
+ // `legibility-series-are-drawn`).
3
+ //
4
+ // /public/versions has always carried `started_at` and `shipped_at` per version,
5
+ // and /roadmap drew NEITHER: both dates reached the browser and came out as
6
+ // "Sep 2026" on a shipped pill. The series two dates support is a SPAN — when
7
+ // each version ran — and every span on ONE shared axis is the reading the page
8
+ // exists for: whether the build is speeding up, and how much of the in-flight
9
+ // version's run is already behind it.
10
+ //
11
+ // Its own file rather than another sixty lines of roadmap.js, for the reason
12
+ // profile-charts.js is its own file: the builders are PURE, so a test imports
13
+ // this and runs the real arithmetic over the real fixture instead of agreeing
14
+ // with a copy of it. Exposed as window.OTBRoadmapTimeline; node-require-safe
15
+ // (the hall-kit.js / dom-utils.js pattern — assignment is the only eval-time
16
+ // side effect, and only when a window exists).
17
+ //
18
+ // The DRAWING is the kit's: OTBKit.barSeriesHtml with a per-row `start`, the
19
+ // span option added for exactly this. Date arithmetic stays here — the kit takes
20
+ // `max` as the axis span and `start` as an offset into it, and never learns what
21
+ // a month is. That is the same seam every other chart in the hall rides.
22
+ (() => {
23
+ 'use strict';
24
+
25
+ const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
26
+
27
+ // "May ’26" — short enough that a whole span label fits the value column at
28
+ // phone width, which "May 2026 – September 2026" does not.
29
+ function ymShort(t) {
30
+ const d = new Date(t);
31
+ return Number.isNaN(d.getTime()) ? '' : `${MON[d.getMonth()]} ’${String(d.getFullYear()).slice(-2)}`;
32
+ }
33
+
34
+ // One row per version that actually started, oldest first.
35
+ //
36
+ // A version with no parsable `start` is NOT on the timeline, because it has
37
+ // not happened. Defaulting it to the axis origin would draw it as having begun
38
+ // at the start of the project, which is the opposite of true; the caller says
39
+ // how many were left out instead.
40
+ //
41
+ // `open` is a version still running (or parked mid-run): its span reaches
42
+ // today and its label says "now", rather than naming an end date nothing
43
+ // recorded. `to` is floored at `from` so a clock skew or a bad shipped_at
44
+ // cannot produce a negative span the kit would draw backwards.
45
+ function timelineRows(all, now) {
46
+ return (all || [])
47
+ .map((v) => {
48
+ const from = Date.parse(v && v.start);
49
+ if (!Number.isFinite(from)) return null;
50
+ const shipped = Date.parse(v.ship);
51
+ const open = !Number.isFinite(shipped);
52
+ return { v, from, to: Math.max(open ? now : shipped, from), open };
53
+ })
54
+ .filter(Boolean)
55
+ .sort((a, b) => a.from - b.from);
56
+ }
57
+
58
+ // The chart card: the spans, and the foot that says what the axis covers and
59
+ // what is missing from it. Returns { count, html }, or NULL when there is
60
+ // nothing worth drawing — so the caller renders no section at all rather than
61
+ // an empty panel. One return shape either way, never a string sometimes and an
62
+ // object other times.
63
+ //
64
+ // `kit` is a parameter (defaulting to window.OTBKit) so this module is pure
65
+ // enough to import in node and run against the real kit in a test.
66
+ function timelineCardHtml(all, now, kit) {
67
+ const k = kit || (typeof window !== 'undefined' ? window.OTBKit : null);
68
+ if (!k) return null;
69
+ const rows = timelineRows(all, now);
70
+ // One version is not a timeline: a single bar filling the whole axis says
71
+ // nothing that version's own card does not already say.
72
+ if (rows.length < 2) return null;
73
+
74
+ const t0 = rows[0].from;
75
+ const t1 = Math.max(now, ...rows.map((r) => r.to));
76
+ const span = (t1 - t0) || 1;
77
+ const bars = k.barSeriesHtml(
78
+ rows.map((r) => ({
79
+ label: r.v.id,
80
+ value: r.to - r.from,
81
+ start: r.from - t0,
82
+ valueLabel: `${ymShort(r.from)} → ${r.open ? 'now' : ymShort(r.to)}`,
83
+ })),
84
+ {
85
+ max: span,
86
+ list: true,
87
+ ariaLabel: 'When each version ran',
88
+ labelWidth: '92px',
89
+ valueWidth: '108px',
90
+ barHeight: '10px',
91
+ pill: true,
92
+ },
93
+ );
94
+ const missing = (all || []).length - rows.length;
95
+ const foot = `${ymShort(t0)} to today, one bar per version.`
96
+ + (missing > 0
97
+ ? ` ${missing} version${missing === 1 ? '' : 's'} with no start date ${missing === 1 ? 'is' : 'are'} not shown.`
98
+ : '');
99
+ return { count: rows.length, html: k.chartCardHtml('When each version ran', bars, foot) };
100
+ }
101
+
102
+ const api = { timelineRows, timelineCardHtml, ymShort };
103
+ if (typeof window !== 'undefined') window.OTBRoadmapTimeline = api;
104
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
105
+ })();
@@ -45,6 +45,12 @@
45
45
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
46
46
  <script src="/builders/palette.js" defer></script>
47
47
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
48
- <script src="/builders/roadmap.js?v=2026-07-24-deck1"></script>
48
+ <!-- The chart kit and the timeline's series builders (task 1003447). Both MUST
49
+ precede roadmap.js: the page reads window.OTBKit / window.OTBRoadmapTimeline
50
+ at render time, so loading either after would be a blank section in
51
+ production and a green unit suite. tests/hall_kit_charts.mjs pins the order. -->
52
+ <script src="/builders/hall-kit.js"></script>
53
+ <script src="/builders/roadmap-timeline.js"></script>
54
+ <script src="/builders/roadmap.js?v=2026-09-03-timeline"></script>
49
55
  </body>
50
56
  </html>
@@ -241,6 +241,18 @@
241
241
  </article>`;
242
242
  }
243
243
 
244
+ // The program in time (task 1003447). The SERIES is roadmap-timeline.js's —
245
+ // pure builders in their own file so a test runs the real arithmetic over the
246
+ // real fixture — and the SECTION chrome is this page's, which is the same seam
247
+ // the chart kit keeps everywhere: the module draws, the page frames. Missing
248
+ // module or fewer than two started versions renders nothing at all, so the
249
+ // deck reads exactly as it did before rather than growing an empty panel.
250
+ function timelineHtml(all, now) {
251
+ const mod = window.OTBRoadmapTimeline;
252
+ const card = mod ? mod.timelineCardHtml(all, now) : null;
253
+ return card ? sectionHtml('flight', 'In time', card.count, '', card.html) : '';
254
+ }
255
+
244
256
  function sectionHtml(dotCls, title, count, note, inner) {
245
257
  return `<section class="rm-sec">
246
258
  <div class="rm-sec__h"><span class="rm-sec__dot rm-sec__dot--${dotCls}"></span><span class="rm-sec__t">${title}</span><span class="rm-sec__n">${count}</span><span class="rm-sec__rule"></span></div>
@@ -380,6 +392,10 @@
380
392
  html += `<div class="rm-hero"><div class="rm-lab" style="max-width:none">No version is in flight right now.</div></div>`;
381
393
  }
382
394
 
395
+ // Directly under the hero: both are program-level readings, and this is the
396
+ // one that answers "how fast" rather than "how much".
397
+ html += timelineHtml(all, Date.now());
398
+
383
399
  if (staged.length) {
384
400
  html += sectionHtml('staged', 'Staged', staged.length, 'Seeded with goals, not yet started.', `<div class="rm-grid">${staged.map(stagedCard).join('')}</div>`);
385
401
  }