@crvouga/atlas 0.11.0 → 0.13.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.
@@ -3381,6 +3381,204 @@
3381
3381
  }
3382
3382
  }
3383
3383
  },
3384
+ "/api/reports/tests/health": {
3385
+ "get": {
3386
+ "operationId": "getCatalogHealth",
3387
+ "tags": [
3388
+ "Report"
3389
+ ],
3390
+ "summary": "Why specs, domains and counts disagree",
3391
+ "description": "Diagnoses the report's metadata, so a domain filter or a coverage percentage can be trusted or fixed at its source. It reads; it never changes a status, a slot or a winner.\n\n`counts` keeps the denominators apart: spec files, defined specs, specs with an executed test (and those without: the defined coverage gaps), tests, outlines, examples and per-environment observations. `reconciliation` walks from the Specs count to the Tests count. `findings` name ambiguous and unmatched spec paths, identical or same-titled specs, a domain shared by nearly every spec, tests with no spec, checks that fall back to their title as identity and root mappings that matched nothing.\n\nTwo paths are one logical spec only when a `root` mapping relates them (`from>to`: paths under `from` are the same specs as paths under `to`; an empty `from` matches every path). Without one, similar paths are reported as ambiguous and never merged. A check's label `outline` names the outline its example belongs to. Send `format=markdown` for Markdown.\n\nRole needed: `read`.",
3392
+ "x-atlas-role": "read",
3393
+ "security": [
3394
+ {
3395
+ "bearer": []
3396
+ },
3397
+ {
3398
+ "basic": []
3399
+ }
3400
+ ],
3401
+ "parameters": [
3402
+ {
3403
+ "required": false,
3404
+ "name": "root",
3405
+ "in": "query",
3406
+ "description": "A root mapping `from>to`, URL-encoded. Repeatable or comma-separated.",
3407
+ "example": ">specs/",
3408
+ "schema": {
3409
+ "type": "string"
3410
+ }
3411
+ },
3412
+ {
3413
+ "required": false,
3414
+ "name": "format",
3415
+ "in": "query",
3416
+ "description": "`markdown` for a Markdown report instead of JSON.",
3417
+ "schema": {
3418
+ "type": "string",
3419
+ "enum": [
3420
+ "json",
3421
+ "markdown"
3422
+ ]
3423
+ }
3424
+ },
3425
+ {
3426
+ "required": false,
3427
+ "name": "project",
3428
+ "in": "query",
3429
+ "description": "Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.",
3430
+ "example": "todo-app",
3431
+ "schema": {
3432
+ "type": "string",
3433
+ "pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$",
3434
+ "maxLength": 64
3435
+ }
3436
+ }
3437
+ ],
3438
+ "responses": {
3439
+ "200": {
3440
+ "description": "The diagnostics.",
3441
+ "headers": {
3442
+ "ETag": {
3443
+ "description": "Changes with the report, its history and the mappings.",
3444
+ "schema": {
3445
+ "type": "string"
3446
+ }
3447
+ }
3448
+ },
3449
+ "content": {
3450
+ "application/json": {
3451
+ "schema": {
3452
+ "$ref": "#/components/schemas/CatalogHealth"
3453
+ }
3454
+ }
3455
+ }
3456
+ },
3457
+ "304": {
3458
+ "description": "Unchanged since the `If-None-Match` ETag."
3459
+ },
3460
+ "401": {
3461
+ "description": "No token, or an unknown one. Send `Authorization: Bearer <token>`.",
3462
+ "content": {
3463
+ "application/problem+json": {
3464
+ "schema": {
3465
+ "$ref": "#/components/schemas/Problem"
3466
+ }
3467
+ }
3468
+ }
3469
+ },
3470
+ "503": {
3471
+ "description": "The server is still reading its backends. Retry after the number of seconds in `Retry-After`.",
3472
+ "content": {
3473
+ "application/problem+json": {
3474
+ "schema": {
3475
+ "$ref": "#/components/schemas/Problem"
3476
+ }
3477
+ }
3478
+ }
3479
+ }
3480
+ }
3481
+ }
3482
+ },
3483
+ "/api/projects/{project}/reports/tests/health": {
3484
+ "get": {
3485
+ "operationId": "getCatalogHealthForProject",
3486
+ "tags": [
3487
+ "Report"
3488
+ ],
3489
+ "summary": "Why specs, domains and counts disagree",
3490
+ "description": "Diagnoses the report's metadata, so a domain filter or a coverage percentage can be trusted or fixed at its source. It reads; it never changes a status, a slot or a winner.\n\n`counts` keeps the denominators apart: spec files, defined specs, specs with an executed test (and those without: the defined coverage gaps), tests, outlines, examples and per-environment observations. `reconciliation` walks from the Specs count to the Tests count. `findings` name ambiguous and unmatched spec paths, identical or same-titled specs, a domain shared by nearly every spec, tests with no spec, checks that fall back to their title as identity and root mappings that matched nothing.\n\nTwo paths are one logical spec only when a `root` mapping relates them (`from>to`: paths under `from` are the same specs as paths under `to`; an empty `from` matches every path). Without one, similar paths are reported as ambiguous and never merged. A check's label `outline` names the outline its example belongs to. Send `format=markdown` for Markdown.\n\nScoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.\n\nRole needed: `read`.",
3491
+ "x-atlas-role": "read",
3492
+ "security": [
3493
+ {
3494
+ "bearer": []
3495
+ },
3496
+ {
3497
+ "basic": []
3498
+ }
3499
+ ],
3500
+ "parameters": [
3501
+ {
3502
+ "required": true,
3503
+ "name": "project",
3504
+ "in": "path",
3505
+ "description": "Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.",
3506
+ "example": "todo-app",
3507
+ "schema": {
3508
+ "type": "string",
3509
+ "pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$",
3510
+ "maxLength": 64
3511
+ }
3512
+ },
3513
+ {
3514
+ "required": false,
3515
+ "name": "root",
3516
+ "in": "query",
3517
+ "description": "A root mapping `from>to`, URL-encoded. Repeatable or comma-separated.",
3518
+ "example": ">specs/",
3519
+ "schema": {
3520
+ "type": "string"
3521
+ }
3522
+ },
3523
+ {
3524
+ "required": false,
3525
+ "name": "format",
3526
+ "in": "query",
3527
+ "description": "`markdown` for a Markdown report instead of JSON.",
3528
+ "schema": {
3529
+ "type": "string",
3530
+ "enum": [
3531
+ "json",
3532
+ "markdown"
3533
+ ]
3534
+ }
3535
+ }
3536
+ ],
3537
+ "responses": {
3538
+ "200": {
3539
+ "description": "The diagnostics.",
3540
+ "headers": {
3541
+ "ETag": {
3542
+ "description": "Changes with the report, its history and the mappings.",
3543
+ "schema": {
3544
+ "type": "string"
3545
+ }
3546
+ }
3547
+ },
3548
+ "content": {
3549
+ "application/json": {
3550
+ "schema": {
3551
+ "$ref": "#/components/schemas/CatalogHealth"
3552
+ }
3553
+ }
3554
+ }
3555
+ },
3556
+ "304": {
3557
+ "description": "Unchanged since the `If-None-Match` ETag."
3558
+ },
3559
+ "401": {
3560
+ "description": "No token, or an unknown one. Send `Authorization: Bearer <token>`.",
3561
+ "content": {
3562
+ "application/problem+json": {
3563
+ "schema": {
3564
+ "$ref": "#/components/schemas/Problem"
3565
+ }
3566
+ }
3567
+ }
3568
+ },
3569
+ "503": {
3570
+ "description": "The server is still reading its backends. Retry after the number of seconds in `Retry-After`.",
3571
+ "content": {
3572
+ "application/problem+json": {
3573
+ "schema": {
3574
+ "$ref": "#/components/schemas/Problem"
3575
+ }
3576
+ }
3577
+ }
3578
+ }
3579
+ }
3580
+ }
3581
+ },
3384
3582
  "/api/reports/tests/history": {
3385
3583
  "get": {
3386
3584
  "operationId": "getTestHistory",
@@ -12499,6 +12697,404 @@
12499
12697
  ],
12500
12698
  "additionalProperties": {}
12501
12699
  },
12700
+ "CatalogHealth": {
12701
+ "type": "object",
12702
+ "properties": {
12703
+ "schemaVersion": {
12704
+ "type": "number",
12705
+ "const": 1
12706
+ },
12707
+ "version": {
12708
+ "type": "string",
12709
+ "description": "Changes whenever the report or its history does"
12710
+ },
12711
+ "generatedAtMs": {
12712
+ "type": "number"
12713
+ },
12714
+ "ok": {
12715
+ "type": "boolean",
12716
+ "description": "True when there is no warning-severity finding"
12717
+ },
12718
+ "counts": {
12719
+ "type": "object",
12720
+ "properties": {
12721
+ "specs": {
12722
+ "type": "object",
12723
+ "properties": {
12724
+ "files": {
12725
+ "type": "integer",
12726
+ "minimum": 0,
12727
+ "maximum": 9007199254740991,
12728
+ "description": "Spec files the report is laid over, before any root mapping"
12729
+ },
12730
+ "defined": {
12731
+ "type": "integer",
12732
+ "minimum": 0,
12733
+ "maximum": 9007199254740991,
12734
+ "description": "Logical specs: files joined by a declared root mapping count once"
12735
+ },
12736
+ "withTests": {
12737
+ "type": "integer",
12738
+ "minimum": 0,
12739
+ "maximum": 9007199254740991,
12740
+ "description": "Logical specs, defined or not, that have at least one test"
12741
+ },
12742
+ "executed": {
12743
+ "type": "integer",
12744
+ "minimum": 0,
12745
+ "maximum": 9007199254740991,
12746
+ "description": "Defined logical specs with a test that passed or failed"
12747
+ },
12748
+ "withoutExecution": {
12749
+ "type": "integer",
12750
+ "minimum": 0,
12751
+ "maximum": 9007199254740991,
12752
+ "description": "Defined logical specs with no passed or failed test: the defined coverage gaps"
12753
+ }
12754
+ },
12755
+ "required": [
12756
+ "files",
12757
+ "defined",
12758
+ "withTests",
12759
+ "executed",
12760
+ "withoutExecution"
12761
+ ],
12762
+ "additionalProperties": {}
12763
+ },
12764
+ "tests": {
12765
+ "type": "object",
12766
+ "properties": {
12767
+ "total": {
12768
+ "type": "integer",
12769
+ "minimum": 0,
12770
+ "maximum": 9007199254740991
12771
+ },
12772
+ "withSpec": {
12773
+ "type": "integer",
12774
+ "minimum": 0,
12775
+ "maximum": 9007199254740991
12776
+ },
12777
+ "withoutSpec": {
12778
+ "type": "integer",
12779
+ "minimum": 0,
12780
+ "maximum": 9007199254740991
12781
+ },
12782
+ "passed": {
12783
+ "type": "integer",
12784
+ "minimum": 0,
12785
+ "maximum": 9007199254740991,
12786
+ "description": "Tests whose current status is passed"
12787
+ },
12788
+ "historicalOnly": {
12789
+ "type": "integer",
12790
+ "minimum": 0,
12791
+ "maximum": 9007199254740991,
12792
+ "description": "Tests only the history holds a result for"
12793
+ }
12794
+ },
12795
+ "required": [
12796
+ "total",
12797
+ "withSpec",
12798
+ "withoutSpec",
12799
+ "passed",
12800
+ "historicalOnly"
12801
+ ],
12802
+ "additionalProperties": {}
12803
+ },
12804
+ "scenarios": {
12805
+ "type": "object",
12806
+ "properties": {
12807
+ "outlines": {
12808
+ "type": "integer",
12809
+ "minimum": 0,
12810
+ "maximum": 9007199254740991,
12811
+ "description": "Tests sharing the label `outline` count once; a test without it is its own outline"
12812
+ },
12813
+ "examples": {
12814
+ "type": "integer",
12815
+ "minimum": 0,
12816
+ "maximum": 9007199254740991,
12817
+ "description": "Tests (check identities), however many environments ran them"
12818
+ },
12819
+ "observations": {
12820
+ "type": "integer",
12821
+ "minimum": 0,
12822
+ "maximum": 9007199254740991,
12823
+ "description": "Current results per producer, environment and parameter set"
12824
+ },
12825
+ "executedObservations": {
12826
+ "type": "integer",
12827
+ "minimum": 0,
12828
+ "maximum": 9007199254740991
12829
+ },
12830
+ "passedObservations": {
12831
+ "type": "integer",
12832
+ "minimum": 0,
12833
+ "maximum": 9007199254740991
12834
+ }
12835
+ },
12836
+ "required": [
12837
+ "outlines",
12838
+ "examples",
12839
+ "observations",
12840
+ "executedObservations",
12841
+ "passedObservations"
12842
+ ],
12843
+ "additionalProperties": {}
12844
+ }
12845
+ },
12846
+ "required": [
12847
+ "specs",
12848
+ "tests",
12849
+ "scenarios"
12850
+ ],
12851
+ "additionalProperties": {}
12852
+ },
12853
+ "reconciliation": {
12854
+ "type": "object",
12855
+ "properties": {
12856
+ "specs": {
12857
+ "type": "integer",
12858
+ "minimum": 0,
12859
+ "maximum": 9007199254740991,
12860
+ "description": "What the Specs view counts: spec files"
12861
+ },
12862
+ "testSpecs": {
12863
+ "type": "integer",
12864
+ "minimum": 0,
12865
+ "maximum": 9007199254740991,
12866
+ "description": "What the Tests view groups by: logical specs with at least one test"
12867
+ },
12868
+ "steps": {
12869
+ "type": "array",
12870
+ "items": {
12871
+ "type": "object",
12872
+ "properties": {
12873
+ "code": {
12874
+ "type": "string"
12875
+ },
12876
+ "label": {
12877
+ "type": "string"
12878
+ },
12879
+ "delta": {
12880
+ "type": "integer",
12881
+ "minimum": -9007199254740991,
12882
+ "maximum": 9007199254740991
12883
+ }
12884
+ },
12885
+ "required": [
12886
+ "code",
12887
+ "label",
12888
+ "delta"
12889
+ ],
12890
+ "additionalProperties": {}
12891
+ },
12892
+ "description": "From `specs` to `testSpecs`: each step adds its `delta`"
12893
+ },
12894
+ "testsWithoutSpec": {
12895
+ "type": "integer",
12896
+ "minimum": 0,
12897
+ "maximum": 9007199254740991
12898
+ },
12899
+ "explanation": {
12900
+ "type": "array",
12901
+ "items": {
12902
+ "type": "string"
12903
+ }
12904
+ }
12905
+ },
12906
+ "required": [
12907
+ "specs",
12908
+ "testSpecs",
12909
+ "steps",
12910
+ "testsWithoutSpec",
12911
+ "explanation"
12912
+ ],
12913
+ "additionalProperties": {}
12914
+ },
12915
+ "identity": {
12916
+ "type": "object",
12917
+ "properties": {
12918
+ "checks": {
12919
+ "type": "integer",
12920
+ "minimum": 0,
12921
+ "maximum": 9007199254740991
12922
+ },
12923
+ "fallbackIdentity": {
12924
+ "type": "integer",
12925
+ "minimum": 0,
12926
+ "maximum": 9007199254740991,
12927
+ "description": "Checks whose id equals their title"
12928
+ },
12929
+ "basis": {
12930
+ "type": "string"
12931
+ }
12932
+ },
12933
+ "required": [
12934
+ "checks",
12935
+ "fallbackIdentity",
12936
+ "basis"
12937
+ ],
12938
+ "additionalProperties": {}
12939
+ },
12940
+ "domains": {
12941
+ "type": "array",
12942
+ "items": {
12943
+ "type": "object",
12944
+ "properties": {
12945
+ "name": {
12946
+ "type": "string"
12947
+ },
12948
+ "specs": {
12949
+ "type": "integer",
12950
+ "minimum": 0,
12951
+ "maximum": 9007199254740991
12952
+ }
12953
+ },
12954
+ "required": [
12955
+ "name",
12956
+ "specs"
12957
+ ],
12958
+ "additionalProperties": {}
12959
+ },
12960
+ "description": "Specs per domain, most first"
12961
+ },
12962
+ "roots": {
12963
+ "type": "array",
12964
+ "items": {
12965
+ "type": "object",
12966
+ "properties": {
12967
+ "from": {
12968
+ "type": "string"
12969
+ },
12970
+ "to": {
12971
+ "type": "string"
12972
+ },
12973
+ "matched": {
12974
+ "type": "integer",
12975
+ "minimum": 0,
12976
+ "maximum": 9007199254740991,
12977
+ "description": "Distinct paths the mapping changed"
12978
+ }
12979
+ },
12980
+ "required": [
12981
+ "from",
12982
+ "to",
12983
+ "matched"
12984
+ ],
12985
+ "additionalProperties": {}
12986
+ }
12987
+ },
12988
+ "aliases": {
12989
+ "type": "array",
12990
+ "items": {
12991
+ "type": "object",
12992
+ "properties": {
12993
+ "path": {
12994
+ "type": "string"
12995
+ },
12996
+ "sources": {
12997
+ "type": "array",
12998
+ "items": {
12999
+ "type": "string"
13000
+ }
13001
+ }
13002
+ },
13003
+ "required": [
13004
+ "path",
13005
+ "sources"
13006
+ ],
13007
+ "additionalProperties": {}
13008
+ },
13009
+ "description": "Logical specs seen under more than one original path, joined by a root mapping"
13010
+ },
13011
+ "findings": {
13012
+ "type": "array",
13013
+ "items": {
13014
+ "type": "object",
13015
+ "properties": {
13016
+ "code": {
13017
+ "type": "string",
13018
+ "description": "An open set: treat a code you do not know as informational"
13019
+ },
13020
+ "severity": {
13021
+ "type": "string",
13022
+ "enum": [
13023
+ "warning",
13024
+ "info"
13025
+ ]
13026
+ },
13027
+ "message": {
13028
+ "type": "string"
13029
+ },
13030
+ "count": {
13031
+ "type": "integer",
13032
+ "minimum": 0,
13033
+ "maximum": 9007199254740991
13034
+ },
13035
+ "items": {
13036
+ "type": "array",
13037
+ "items": {
13038
+ "type": "object",
13039
+ "properties": {
13040
+ "path": {
13041
+ "type": "string"
13042
+ },
13043
+ "candidates": {
13044
+ "description": "Other paths it could be the same promise as. Atlas has not merged them",
13045
+ "type": "array",
13046
+ "items": {
13047
+ "type": "object",
13048
+ "properties": {
13049
+ "path": {
13050
+ "type": "string"
13051
+ },
13052
+ "reason": {
13053
+ "type": "string"
13054
+ }
13055
+ },
13056
+ "required": [
13057
+ "path",
13058
+ "reason"
13059
+ ],
13060
+ "additionalProperties": {}
13061
+ }
13062
+ }
13063
+ },
13064
+ "required": [
13065
+ "path"
13066
+ ],
13067
+ "additionalProperties": {}
13068
+ },
13069
+ "description": "At most 25; `count` is the whole"
13070
+ }
13071
+ },
13072
+ "required": [
13073
+ "code",
13074
+ "severity",
13075
+ "message",
13076
+ "count",
13077
+ "items"
13078
+ ],
13079
+ "additionalProperties": {}
13080
+ }
13081
+ }
13082
+ },
13083
+ "required": [
13084
+ "schemaVersion",
13085
+ "version",
13086
+ "generatedAtMs",
13087
+ "ok",
13088
+ "counts",
13089
+ "reconciliation",
13090
+ "identity",
13091
+ "domains",
13092
+ "roots",
13093
+ "aliases",
13094
+ "findings"
13095
+ ],
13096
+ "additionalProperties": {}
13097
+ },
12502
13098
  "TestHistory": {
12503
13099
  "type": "object",
12504
13100
  "properties": {
@@ -268,6 +268,10 @@ type Props = {
268
268
  reads: 'steps' | 'trace';
269
269
  /** The check as written, shown until a recording is read or when there is none. */
270
270
  written?: ReactNode;
271
+ /** An exact earlier result the caller found (for example on a server that only sends winners). It wins over `layerId`. */
272
+ pinned?: HistoryEntry;
273
+ /** Choosing a run in the history always names its layer, even when that result is a current one, so the choice stays pinned. */
274
+ pin?: boolean;
271
275
  };
272
276
  /** Everything recorded for one check: the chosen result, how it went, and every other run of it. */
273
277
  export function CheckDetail(props: Props) {
@@ -298,13 +302,13 @@ export function CheckDetail(props: Props) {
298
302
  </>
299
303
  );
300
304
  }
301
- function RecordedCheckDetail({ slot, layerId, select, reads, written }: Props) {
305
+ function RecordedCheckDetail({ slot, layerId, select, reads, written, pinned, pin }: Props) {
302
306
  const { evidence, backends } = useEvidence();
303
307
  const now = useNow();
304
308
  const [variant, setVariant] = useState(0);
305
309
  const history = useMemo(() => checkHistory(evidence, slot.specPath, slot.id), [evidence, slot.specPath, slot.id]);
306
310
  const winner = slot.outcomes[Math.min(variant, slot.outcomes.length - 1)]!;
307
- const earlier = layerId ? history.find((entry) => entry.layer.id === layerId) : undefined;
311
+ const earlier = pinned ?? (layerId ? history.find((entry) => entry.layer.id === layerId) : undefined);
308
312
  const chosen: HistoryEntry = earlier ?? { layer: winner.layer, row: winner.row };
309
313
  const superseded = !slot.outcomes.some((outcome) => outcome.layer.id === chosen.layer.id);
310
314
  const { paths, report } = useRecordedReport(chosen);
@@ -397,7 +401,7 @@ function RecordedCheckDetail({ slot, layerId, select, reads, written }: Props) {
397
401
  {moments > 0 && (
398
402
  <Button
399
403
  type="button"
400
- onClick={() => select({ view: 'recording', check: slot.id, layer: earlier ? chosen.layer.id : null })}
404
+ onClick={() => select({ view: 'recording', check: slot.id, layer: earlier || variant > 0 ? chosen.layer.id : null })}
401
405
  variant="filled"
402
406
  >
403
407
  Open recording{' '}
@@ -428,7 +432,9 @@ function RecordedCheckDetail({ slot, layerId, select, reads, written }: Props) {
428
432
  type="button"
429
433
  aria-current={current || undefined}
430
434
  onClick={() =>
431
- select({ layer: slot.outcomes.some((outcome) => outcome.layer.id === entry.layer.id) ? null : entry.layer.id })
435
+ select({
436
+ layer: !pin && slot.outcomes.some((outcome) => outcome.layer.id === entry.layer.id) ? null : entry.layer.id
437
+ })
432
438
  }
433
439
  variant="light"
434
440
  >
@@ -15,6 +15,8 @@ type Props = {
15
15
  /** The check whose recording is open, once the combined report holds it. */
16
16
  slot: CheckSlot | undefined;
17
17
  layerId: string | undefined;
18
+ /** The exact result to play, when the caller already found it (it wins over `layerId` and the combined winner). */
19
+ entry?: HistoryEntry | undefined;
18
20
  label: string;
19
21
  back: string;
20
22
  moment: number | null;
@@ -36,14 +38,15 @@ function needsStylesheet(report: RunReport | undefined): boolean {
36
38
  return visit(report.steps) || (report.artifacts ?? []).some((artifact) => artifact.media === 'html');
37
39
  }
38
40
  /** One recorded test, opened in place of its spec. */
39
- export function RecordingPage({ slot, layerId, label, back, moment, onMoment, onClose }: Props) {
41
+ export function RecordingPage({ slot, layerId, entry, label, back, moment, onMoment, onClose }: Props) {
40
42
  const progress = useTestProgress(slot ?? null);
41
43
  const { evidence, backends, pending, ready, unavailable } = useEvidence();
42
44
  const now = useNow();
43
45
  const history = useMemo(() => (slot ? checkHistory(evidence, slot.specPath, slot.id) : []), [evidence, slot]);
44
46
  const winner = slot?.outcomes[0];
45
47
  const chosen: HistoryEntry | null =
46
- (layerId ? history.find((entry) => entry.layer.id === layerId) : undefined) ??
48
+ entry ??
49
+ (layerId ? history.find((earlier) => earlier.layer.id === layerId) : undefined) ??
47
50
  (winner ? { layer: winner.layer, row: winner.row } : null);
48
51
  const { paths, report } = useRecordedReport(chosen);
49
52
  const wantsCss = needsStylesheet(report.data);