File size: 31,180 Bytes
eea689d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2856a6c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eea689d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53f6747
 
 
 
 
 
eea689d
53f6747
 
 
 
 
eea689d
 
 
 
 
 
 
53f6747
 
eea689d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2856a6c
 
 
eea689d
 
 
 
 
53f6747
 
eea689d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2856a6c
 
 
 
 
 
 
eea689d
 
 
 
 
2856a6c
 
 
 
 
 
 
 
 
 
eea689d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2856a6c
 
 
 
 
 
 
eea689d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
"""Project the confirmed case records onto the canonical concept list and render the cohort export
bundle (#101, docs/prd.md sections 3, 8.1).

The column spec is the canonical concept list (`mapping.concept_list()`, #91), not the built-in CAP
checklist: the export speaks concepts, so a second laboratory targeting the same concepts re-derives the
same table. `mapping.cap_checklist_coverage()` (#95) is the bridge that says which confirmed CAP field
fills which concept column. A concept no CAP field covers, and no derivation reaches, is absent, and an
absent value is never a blank: it carries a coded reason in a parallel `<concept>__absent_reason` column,
from the field-state vocabulary docs/prd.md section 4.3 defines (`not_applicable`, `not_stated`,
`indeterminate`, `determined_by_join`, `constrained`).

The two invariants this projection serves:

1. Numbers stay numbers. The stored quantity survives into its own column, and a derived category sits
   beside it in a separate column, recomputed from that quantity by the registry's named rule
   (`taxonomy.recompute`, #35), never stored in its place. A confirmed `myometrial_invasion_percent` of
   60.0 and the `half_or_more` category it derives to are two columns, so the export can still answer the
   other convention's question.
2. No value without its evidence. The field-level provenance carries each value's verbatim evidence quote,
   its page, and the pass that read it, and the bundle carries a checksum over every cited span per case,
   so a second centre auditing the row reads the same source material.

The bundle carries the schema the second centre re-derives against (the concept list and its checksum),
the version of the rule set and the extraction model that touched the values, the staging edition each
stage speaks (docs/prd.md section 4.2), and a per-case source-document reference with an evidence
checksum. The rendered CSV, XLSX, and JSON come from one projection, so the three files and the on-screen
preview never disagree.

## Relates to

- #40 owns the target-schema selector and the `derive_stage` set projection: choosing an arbitrary
  confirmed dictionary as the target, and rendering a `constrained` stage as the candidate set with the
  variable whose absence blocks narrowing. This module names the staging edition it emitted and reserves
  the `constrained` code for that projection; it does not itself enumerate the candidate set.
- #39 owns the deeper export provenance: the byte-level checksum of each scanned report and the positional
  region of every evidence span. This module carries the source-document reference and a checksum over the
  cited spans, the reproducible provenance available from the confirmed record alone.
- #35 owns the per-value coding-system resolution beyond the FIGO edition; this module cites each concept
  to the registry via its `cited_to` in the column spec.
"""

from __future__ import annotations

import base64
import csv
import hashlib
import io
import json
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Optional

from sqlalchemy import Connection, text

from endopath import llm_extraction, longitudinal, mapping, projection, taxonomy
from endopath.mapping import AssertionType, CompiledConcept
from endopath.schema import ChecklistField, FieldStatus
from endopath.validation import FieldState

# The coded-absence reason applied to a concept the confirmed record does not carry. A molecular finding
# absent from a report of this era arrives from the clinical record rather than the report, so it is
# `determined_by_join` (docs/prd.md sections 4.3, 8.3); every other uncovered concept, and a derived
# concept whose input primitives are absent, is `not_stated`, the searched-and-absent code.
_JOIN_SOURCED_CATEGORY = "molecular"

ABSENT_REASON_SUFFIX = "__absent_reason"


@dataclass(frozen=True)
class _Projected:
    """One concept's value on one case: a concrete value with no reason, or a coded absence with no
    value. `origin` records how the value was reached, for the column-spec descriptor."""

    value: Optional[Any]
    absent_reason: Optional[str]
    origin: str  # 'confirmed' | 'derived' | 'absent'


def _primitive(value: object) -> object:
    """An enum to its value, everything else as-is, so the export carries JSON primitives."""
    return value.value if hasattr(value, "value") else value


def _concept_labels() -> dict[str, str]:
    """Concept id to human label, read from the dependency-graph nodes the concept ids key on."""
    data = json.loads(mapping.DEPENDENCY_GRAPH_PATH.read_text(encoding="utf-8"))
    return {node["id"]: node["label"] for node in data["nodes"]}


def _field_value_and_state(field: ChecklistField) -> tuple[Optional[Any], FieldState]:
    """Reduce one confirmed CAP field to a value and a field state. A gated field is `not_applicable`; a
    flagged field was searched for and confirmed absent (`not_stated`); an applicable field with no value
    is `not_stated`; a field carrying a value is present. Silence is never the response to a miss."""
    if not field.applicable:
        return None, FieldState.NOT_APPLICABLE
    if field.status is FieldStatus.FLAGGED:
        return None, FieldState.NOT_STATED
    value = _primitive(field.value)
    if value is None:
        return None, FieldState.NOT_STATED
    return value, FieldState.PRESENT


def _project_case(
    checklist,
    *,
    concepts: list[CompiledConcept],
    coverage: dict[str, str],
) -> dict[str, _Projected]:
    """Project one confirmed checklist onto the concept list: seed the covered concepts from their CAP
    fields, recompute a derived concept whose inputs are all present, and code every remaining concept as
    a coded absence (invariant: no value without a reason)."""
    concept_to_field = {concept_id: field_name for field_name, concept_id in coverage.items()}
    projected: dict[str, _Projected] = {}

    # Pass 1: every covered concept, from its confirmed CAP field, so a derivation's inputs are available
    # before any derived concept is filled.
    for concept in concepts:
        field_name = concept_to_field.get(concept.concept_id)
        if field_name is None:
            continue
        value, state = _field_value_and_state(getattr(checklist, field_name))
        if state is FieldState.PRESENT:
            projected[concept.concept_id] = _Projected(value, None, "confirmed")
        else:
            projected[concept.concept_id] = _Projected(None, state.value, "absent")

    # Pass 2: the concepts no CAP field covers.
    for concept in concepts:
        cid = concept.concept_id
        if cid in projected:
            continue
        if concept.assertion_type is AssertionType.DERIVED and concept.derived is not None:
            projected[cid] = _derive(concept, projected)
        else:
            reason = (
                FieldState.DETERMINED_BY_JOIN
                if concept.category == _JOIN_SOURCED_CATEGORY
                else FieldState.NOT_STATED
            )
            projected[cid] = _Projected(None, reason.value, "absent")
    return projected


def _derive(concept: CompiledConcept, projected: dict[str, _Projected]) -> _Projected:
    """Recompute a derived concept from its inputs by its registry-owned rule (`taxonomy.recompute`,
    #35), the same discipline the B1 validation pass runs: recompute only when every graph input is
    present, so a derived category is computed from the primitive rather than trusted as stored. A
    derivation whose input a gate removed inherits `not_applicable`; one whose input is merely absent, or
    that the per-case pass does not recompute, is `not_stated`."""
    assert concept.derived is not None
    inputs = [projected.get(input_id) for input_id in concept.derived.inputs]
    if any(item is not None and item.absent_reason == FieldState.NOT_APPLICABLE.value for item in inputs):
        return _Projected(None, FieldState.NOT_APPLICABLE.value, "absent")
    if all(item is not None and item.value is not None for item in inputs):
        recomputed = taxonomy.recompute(concept.concept_id, [item.value for item in inputs])  # type: ignore[union-attr]
        if recomputed is not None:
            return _Projected(recomputed, None, "derived")
    return _Projected(None, FieldState.NOT_STATED.value, "absent")


def _evidence_checksum(checklist) -> str:
    """A SHA-256 over the case's cited evidence: the field, verbatim quote, page, and pass of every value
    that carries a span, in a canonical order. A second centre confirming the same values against the same
    reports reads the same spans and computes the same checksum (invariant 2)."""
    spans = []
    for field_name, field in sorted(checklist.all_fields().items()):
        if field.evidence is not None:
            spans.append(
                {
                    "field": field_name,
                    "quote": field.evidence.quote,
                    "page_number": field.evidence.page_number,
                    "source": field.evidence.source,
                }
            )
    canonical = json.dumps(spans, sort_keys=True, ensure_ascii=False)
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()


def _columns(concepts: list[CompiledConcept], labels: dict[str, str]) -> list[dict]:
    """The column spec: the case-level identifiers, then, per concept in concept-list order, a value
    column and its parallel absent-reason column. The descriptor carries the concept's category,
    assertion type, cardinality, and registry citation so the bundle and the screen render the spec
    without re-deriving it."""
    columns: list[dict] = [
        {"key": "case_barcode", "label": "Case barcode", "kind": "identifier"},
        {"key": "report_date", "label": "Report date", "kind": "identifier"},
        {"key": "staging_edition", "label": "Staging edition", "kind": "identifier"},
        {"key": "status", "label": "Case status", "kind": "identifier"},
    ]
    for concept in concepts:
        cid = concept.concept_id
        label = labels.get(cid, cid)
        columns.append(
            {
                "key": cid,
                "label": label,
                "kind": "value",
                "concept_id": cid,
                "category": concept.category,
                "assertion_type": concept.assertion_type.value,
                "cardinality": concept.cardinality.value,
                "cited_to": concept.cited_to,
                "derived": concept.derived.model_dump(mode="json") if concept.derived else None,
                "absent_reason_key": f"{cid}{ABSENT_REASON_SUFFIX}",
            }
        )
        columns.append(
            {
                "key": f"{cid}{ABSENT_REASON_SUFFIX}",
                "label": f"{label} absent reason",
                "kind": "absent_reason",
                "of": cid,
            }
        )
    columns.extend(_outcome_columns())
    return columns


def _outcome_columns() -> list[dict]:
    """The join-sourced outcome columns, appended after the concept columns: the disease outcome joined from
    the GDC clinical record rather than read from the pathology report (`longitudinal.py`). Each value column
    is marked `sourced_via: "join"` and paired with its parallel absent-reason column, so the same renderer
    and the same coded-absence discipline apply as for a concept column. The block keeps the report-concept
    schema and its checksum meaning "what the report answers"."""
    columns: list[dict] = []
    for spec in longitudinal.OUTCOME_COLUMNS:
        key = spec["key"]
        columns.append(
            {
                "key": key,
                "label": spec["label"],
                "kind": "value",
                "category": longitudinal.OUTCOME_CATEGORY,
                "assertion_type": spec["assertion_type"],
                "sourced_via": "join",
                "join_source": longitudinal.JOIN_SOURCE,
                "derived": spec.get("derived"),
                "note": spec.get("note"),
                "absent_reason_key": f"{key}{ABSENT_REASON_SUFFIX}",
            }
        )
        columns.append(
            {
                "key": f"{key}{ABSENT_REASON_SUFFIX}",
                "label": f"{spec['label']} absent reason",
                "kind": "absent_reason",
                "of": key,
            }
        )
    return columns


def _load_longitudinal(conn: Connection) -> dict[str, list[longitudinal.Record]]:
    """Every longitudinal record grouped by participant, in one read (issue #131's batched discipline). The
    outcome projection joins by participant barcode, and a per-case query would reintroduce the N+1 the
    cohort export was restructured to avoid. The table holds a few thousand small rows, so one unfiltered
    read is cheaper than a query per case."""
    by_participant: dict[str, list[longitudinal.Record]] = {}
    rows = conn.execute(
        text(
            "SELECT participant_id, record_id, record_type, structured_data FROM longitudinal_records"
        )
    ).mappings().fetchall()
    for row in rows:
        data: dict = {}
        if row["structured_data"]:
            try:
                data = json.loads(row["structured_data"])
            except (json.JSONDecodeError, TypeError):
                data = {}
        by_participant.setdefault(row["participant_id"], []).append(
            longitudinal.Record(
                record_type=row["record_type"], record_id=row["record_id"], data=data
            )
        )
    return by_participant


def _load_longitudinal_joins(conn: Connection) -> dict[str, list[dict]]:
    """Every reviewer join decision grouped by case barcode, in one read (issue #131's batched discipline).
    #180 writes these from the Case Review panel: a per-record disposition (`confirmed` / `rejected`) with
    the confirmer's identity and role. The export and the cohort endpoint read them to drop rejected records
    and to attribute the confirmed linkages. A record with no row here has not been reviewed, not rejected."""
    by_case: dict[str, list[dict]] = {}
    rows = conn.execute(
        text(
            "SELECT case_barcode, record_id, disposition, confirmed_by, role, confirmed_at "
            "FROM longitudinal_joins"
        )
    ).mappings().fetchall()
    for row in rows:
        by_case.setdefault(row["case_barcode"], []).append(
            {
                "record_id": row["record_id"],
                "disposition": row["disposition"],
                "confirmed_by": row["confirmed_by"],
                "role": row["role"],
                "confirmed_at": row["confirmed_at"],
            }
        )
    return by_case


def _rejected_record_ids(case_joins: list[dict]) -> set[str]:
    """The record ids a reviewer explicitly rejected for this case. The barcode join is deterministic, so a
    record with no join row is unreviewed, not rejected: only an explicit `rejected` disposition excludes a
    record from the projection, leaving the default (no decision) projecting exactly as before."""
    return {j["record_id"] for j in case_joins if j.get("disposition") == "rejected"}


def _bundle(
    concepts: list[CompiledConcept],
    sources: list[dict],
    *,
    staging_edition: Optional[str],
    per_case_editions: list[str],
) -> dict:
    """The provenance bundle (docs/prd.md section 3): the schema the second centre re-derives against and
    its checksum, the rule-set and extraction-model versions that touched the values, the staging edition
    each stage speaks, the coding systems, and the per-case source references."""
    schema_checksum = hashlib.sha256(mapping.concept_list_json().encode("utf-8")).hexdigest()
    rule_set_checksum = hashlib.sha256(
        mapping.DEPENDENCY_GRAPH_PATH.read_bytes()
    ).hexdigest()
    named_rules = sorted(
        {
            concept.derived.rule
            for concept in concepts
            if concept.derived is not None
        }
    )
    return {
        "target_schema": {
            "name": "CAP/ICCR endometrial carcinoma synoptic, projected onto the canonical concept list",
            "concept_count": len(concepts),
            "schema_checksum": schema_checksum,
        },
        "staging_edition": {
            "selected": staging_edition or "as_recorded",
            "per_case_editions": sorted(set(per_case_editions)),
        },
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "rule_versions": {
            "dependency_graph_checksum": rule_set_checksum,
            "named_rules": named_rules,
        },
        "model_versions": {"extraction": llm_extraction.MODEL},
        "coding_systems": {
            "stage": "FIGO; the edition each stage speaks is the per-case staging_edition column",
            "value_sets": (
                "CAP, ICCR, WHO, and AJCC reference standards; each concept cites its registry source "
                "in the column spec's cited_to"
            ),
        },
        "sources": sources,
        "reproduction": (
            "A laboratory holding the same reports and confirming the same values against this concept "
            "list re-derives an identical table. Every absent value carries a coded reason from "
            "docs/prd.md section 4.3, so a missing value is never a bare blank."
        ),
    }


def _provenance(case_barcode: str, checklist) -> list[dict]:
    """One field-level provenance record per checklist field: the value, its confidence and status, the
    coded-absence markers, and the verbatim evidence span (docs/prd.md section 3 case record)."""
    records: list[dict] = []
    for field_name, field in checklist.all_fields().items():
        evidence = field.evidence
        records.append(
            {
                "case_barcode": case_barcode,
                "field_name": field_name,
                "value": _primitive(field.value),
                "confidence": field.confidence,
                "status": field.status.value,
                "applicable": field.applicable,
                "not_applicable_reason": field.not_applicable_reason,
                "evidence_quote": evidence.quote if evidence else None,
                "evidence_char_start": evidence.char_start if evidence else None,
                "evidence_char_end": evidence.char_end if evidence else None,
                "evidence_page": evidence.page_number if evidence else None,
                "evidence_source": evidence.source if evidence else None,
            }
        )
    return records


# The export is the confirmed dataset (#231, decision D1): the API exports only cases a reviewer has
# confirmed (or already exported), so the screen's "Confirmed records" title is truthful. build_export
# still projects any set when `statuses` is None, for internal callers and the projection tests.
EXPORTABLE_STATUSES: tuple[str, ...] = ("confirmed", "exported")


def build_export(
    conn: Connection,
    *,
    staging_edition: Optional[str] = None,
    project: Optional[str] = None,
    statuses: Optional[tuple[str, ...]] = None,
) -> dict:
    """Project every case in the store onto the concept list and assemble the cohort export.

    Returns the column spec, one projected row per case, the field-level provenance, and the reproduction
    bundle. `staging_edition` names the edition the researcher chose at export (docs/prd.md section 4.2);
    the base projection carries the edition each case recorded, and #40 owns re-projecting a stage under a
    chosen edition as a candidate set. `project` scopes the export to one project's cases (issue #149);
    omitted, every case in the store is projected. `statuses`, when given, keeps only cases in those
    statuses; the API passes EXPORTABLE_STATUSES so the served export is the confirmed dataset (#231)."""
    from endopath import storage

    concepts = mapping.compile_concepts()
    coverage = mapping.cap_checklist_coverage()
    labels = _concept_labels()

    rows: list[dict] = []
    provenance: list[dict] = []
    sources: list[dict] = []
    join_sources: list[dict] = []
    per_case_editions: list[str] = []

    # The joined GDC outcome records, grouped by participant, in one read so the per-case outcome projection
    # stays free of the N+1 the cohort export was restructured to avoid (issue #131).
    longitudinal_by_participant = _load_longitudinal(conn)
    # The reviewer join decisions (#180), so a rejected record is excluded from the projection and the
    # confirmed linkages are attributed in the bundle (#217).
    longitudinal_joins_by_case = _load_longitudinal_joins(conn)

    # One batched read of the whole cohort rather than a get_case per barcode (issue #131): a per-case
    # N+1 is ~100s over remote Postgres and 500s the export; this is a few queries. Scoped to the project
    # when one is named (issue #149).
    for case, patient_filename in storage.load_all_cases(conn, project=project):
        if statuses is not None and case.status.value not in statuses:
            continue  # the export is the confirmed dataset (#231): an unreviewed case is not a record
        projected = _project_case(case.checklist, concepts=concepts, coverage=coverage)

        row: dict[str, Any] = {
            "case_barcode": case.case_barcode,
            "report_date": case.report_date.isoformat() if case.report_date else None,
            "staging_edition": case.staging_edition.value,
            "status": case.status.value,
        }
        for concept in concepts:
            item = projected[concept.concept_id]
            row[concept.concept_id] = item.value
            row[f"{concept.concept_id}{ABSENT_REASON_SUFFIX}"] = item.absent_reason

        # Re-project the FIGO stage under the chosen edition (#40). derive_stage returns a determined
        # code, a constrained candidate set, or an indeterminate reason, each rendered distinctly rather
        # than collapsed to one confident stage. The raw quantities stay in their own columns (invariant 1).
        target_edition = staging_edition or case.staging_edition.value
        stage_verdict = projection.project_stage(case.checklist, target_edition)
        stage_value, stage_reason = projection.stage_cell(stage_verdict)
        row["figo_stage"] = stage_value
        row[f"figo_stage{ABSENT_REASON_SUFFIX}"] = stage_reason
        row["figo_stage__projection"] = stage_verdict

        # The join-sourced outcome block: the disease outcome joined from the GDC clinical record by
        # participant barcode, projected onto the outcome columns (`longitudinal.py`). Every cell is a value
        # or a coded absence, never a bare blank; a participant the join did not reach reads determined_by_join.
        participant_id = (
            case.case_barcode[:12] if len(case.case_barcode) >= 12 else case.case_barcode
        )
        outcome_records = longitudinal_by_participant.get(participant_id, [])
        # Drop records a reviewer rejected (#217): a rejection removes the record from the outcome columns
        # and from the bundle's contributing ids, so a linkage the reviewer disowned has no effect. A record
        # with no decision is unreviewed, not rejected, and still projects (see `_rejected_record_ids`).
        case_joins = longitudinal_joins_by_case.get(case.case_barcode, [])
        rejected = _rejected_record_ids(case_joins)
        if rejected:
            outcome_records = [r for r in outcome_records if r.record_id not in rejected]
        outcomes = longitudinal.project_outcomes(outcome_records)
        for key in longitudinal.OUTCOME_COLUMN_KEYS:
            cell = outcomes[key]
            row[key] = cell.value
            row[f"{key}{ABSENT_REASON_SUFFIX}"] = cell.absent_reason
        source_entry = {
            "case_barcode": case.case_barcode,
            "participant_id": participant_id,
            "record_ids": longitudinal.contributing_record_ids(outcome_records),
        }
        # The reviewer-attributed provenance behind the join-sourced columns (#176, #217): who decided each
        # linkage, in what role, with what disposition. Present only when the case carries a decision.
        if case_joins:
            source_entry["linkage_decisions"] = case_joins
        join_sources.append(source_entry)
        rows.append(row)

        provenance.extend(_provenance(case.case_barcode, case.checklist))
        sources.append(
            {
                "case_barcode": case.case_barcode,
                "source_document": patient_filename,
                "evidence_checksum": _evidence_checksum(case.checklist),
            }
        )
        per_case_editions.append(case.staging_edition.value)

    from endopath import provenance as provenance_mod

    bundle = _bundle(
        concepts,
        sources,
        staging_edition=staging_edition,
        per_case_editions=per_case_editions,
    )
    # The per-call LLM ledger (#116): how many model calls the cohort cost, in tokens and mean latency,
    # so a second centre sees what produced the values, not only the values.
    bundle["llm_calls"] = provenance_mod.summary(conn)
    # The join provenance for the outcome columns (invariant 2 for a join-sourced value): the source of the
    # join, the outcome columns it fills, the named derivation rule, the survival gap this pull carries, and
    # the GDC record ids that fed each case's outcomes.
    bundle["join_sources"] = {
        "source": longitudinal.JOIN_SOURCE,
        "outcome_columns": longitudinal.OUTCOME_COLUMN_KEYS,
        "derivation_rules": [longitudinal.RECURRENCE_RULE, longitudinal.OVERALL_SURVIVAL_RULE],
        "data_quality": (
            "therapeutic_agents reads Tamoxifen for nearly every case. This is faithful to the GDC/TCGA "
            "harmonized clinical record, a known source artifact, not an error in this pipeline; it is "
            "surfaced with its provenance rather than silently dropped. The received_chemotherapy, "
            "received_radiation, and received_hormone_therapy flags read administration, not record "
            'presence: GDC emits a treatment_<type> template record marked treatment_or_therapy = "no" on '
            "cases where the therapy was considered but not given (most often Radiation Therapy, NOS), so a "
            'flag is True only where a matching record is marked "yes", False on an explicit "no", and '
            'not_stated where the matching records carry only "unknown". Reading presence instead reports '
            "radiation on 543/546 cases (99%); administration reports 264 (48%)."
        ),
        "per_case": join_sources,
    }
    return {
        "columns": _columns(concepts, labels),
        "rows": rows,
        "provenance": provenance,
        "bundle": bundle,
    }


# --- rendering the three files from one projection ------------------------------------------------


def _cell(value: Any) -> Any:
    """A projected value rendered for a spreadsheet cell: None to an empty string, a bool kept as a bool,
    everything else as-is. An absent value's cell is empty; its reason rides the parallel column."""
    if value is None:
        return ""
    return value


def to_csv(export: dict) -> str:
    """The projected table as CSV: the column keys as the header, one row per case, an empty cell for an
    absent value with its reason in the parallel column."""
    keys = [column["key"] for column in export["columns"]]
    buffer = io.StringIO()
    writer = csv.DictWriter(buffer, fieldnames=keys, extrasaction="ignore")
    writer.writeheader()
    for row in export["rows"]:
        writer.writerow({key: _cell(row.get(key)) for key in keys})
    return buffer.getvalue()


def to_json(export: dict) -> str:
    """The whole export as self-describing JSON: the bundle, the column spec, the rows, and the field-level
    provenance, so the file alone re-derives the table."""
    return json.dumps(
        {
            "bundle": export["bundle"],
            "columns": export["columns"],
            "rows": export["rows"],
            "provenance": export["provenance"],
        },
        indent=2,
        default=str,
    )


def to_xlsx_bytes(export: dict) -> bytes:
    """The projected table as an XLSX workbook: a `cases` sheet of the projected rows, a `provenance`
    sheet of the field-level evidence, and a `bundle` sheet of the reproduction metadata as key/value
    rows."""
    from openpyxl import Workbook

    workbook = Workbook()

    cases = workbook.active
    cases.title = "cases"
    keys = [column["key"] for column in export["columns"]]
    cases.append([column["label"] for column in export["columns"]])
    for row in export["rows"]:
        cases.append([_cell(row.get(key)) for key in keys])

    provenance_sheet = workbook.create_sheet("provenance")
    provenance_headers = [
        "case_barcode",
        "field_name",
        "value",
        "confidence",
        "status",
        "applicable",
        "not_applicable_reason",
        "evidence_quote",
        "evidence_page",
        "evidence_source",
    ]
    provenance_sheet.append(provenance_headers)
    for record in export["provenance"]:
        provenance_sheet.append([_cell(record.get(header)) for header in provenance_headers])

    bundle_sheet = workbook.create_sheet("bundle")
    bundle_sheet.append(["key", "value"])
    for key, value in _flatten(export["bundle"]):
        bundle_sheet.append([key, value])

    buffer = io.BytesIO()
    workbook.save(buffer)
    return buffer.getvalue()


def _flatten(value: Any, prefix: str = "") -> list[tuple[str, str]]:
    """Flatten the nested bundle into dotted key/value rows for the XLSX bundle sheet."""
    rows: list[tuple[str, str]] = []
    if isinstance(value, dict):
        for key, inner in value.items():
            rows.extend(_flatten(inner, f"{prefix}.{key}" if prefix else str(key)))
    elif isinstance(value, list):
        if all(not isinstance(item, (dict, list)) for item in value):
            rows.append((prefix, ", ".join(str(item) for item in value)))
        else:
            for index, item in enumerate(value):
                rows.extend(_flatten(item, f"{prefix}[{index}]"))
    else:
        rows.append((prefix, "" if value is None else str(value)))
    return rows


def render_files(export: dict) -> dict:
    """The three download files from one projection: CSV and JSON as text, XLSX as base64, each with its
    filename and media type. One projection feeds the preview and all three files, so they never diverge."""
    return {
        "csv": {
            "filename": "endometrial_cohort.csv",
            "media_type": "text/csv",
            "content": to_csv(export),
        },
        "json": {
            "filename": "endometrial_cohort.json",
            "media_type": "application/json",
            "content": to_json(export),
        },
        "xlsx": {
            "filename": "endometrial_cohort.xlsx",
            "media_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            "content_base64": base64.b64encode(to_xlsx_bytes(export)).decode("ascii"),
        },
    }