Files
stack/dev/scripts/dossier_page/template.html
kert 37fb8d2d68 feat(dossier): lex.<domain> is the dossier app's address, with a banner that explains the name (S0b, R79)
bootstrap_sso.py: "lex" joins SUBDOMAINS; --dns-only added the tunnel
ingress entry (25 hostnames, all 24 existing kept) and a proxied CNAME.

Traefik: lex.<domain>/ is rewritten (replacePath) to the static page
Gitea serves, /assets/dossier/ passes through, and a priority-10
catch-all redirects every other lex path to "/" via noop@internal, so
Gitea's own pages never appear on lex. git-sso is first on every lex
chain. The old git.<domain>/dossier now 302s to lex before SSO, so
sign-in lands on lex; the unused dossier-page rewrite is gone and the
git-host /assets/dossier/ routers stay behind SSO.

Page: title "lex · Home or Residence Services"; the navbar reads
"fhirworx | lex" with links back to git.<domain>; the banner says what
lex is (Latin for law; a legal-epidemiology workbench) and keeps the
build status as a smaller second line.
2026-11-12 17:56:03 -05:00

1218 lines
101 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>lex · Home or Residence Services</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700;800&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,600;1,8..60,400&family=JetBrains+Mono:wght@400;500;600&display=swap">
<style>
/* Layout: the fhirworx Gitea shell (navy navbar, cream ground, 3px ink rules) around a three-pane
workspace: the book in the left rail, the selected object with one tab per view, the evidence drawer. */
:root {
--bg: #F7F5F0; /* fhirworx cream body */
--surface: #FFFFFF; /* card */
--surface-2: #EDEBE6; /* box header / secondary */
--hover: #F3F0EA;
--fg: #1A1A18;
--fg-2: #454542;
--muted: #6E6E6A;
--rule: #B8B3A4;
--rule-soft: #E0DCD1;
--ink-rule: #1A1A18; /* the 3px masthead rule */
--nav-bg: #1C2B3A;
--nav-fg: #F0F4F8;
--nav-hover: #253748;
--primary: #1C2B3A;
--link: #2D5F7F;
--link-hover: #244B63;
--s-final: #2E6E9E; /* chart: final values (validated pair) */
--s-prop: #C8702A; /* chart: proposed values */
--s-ruc: #6E6E6A; /* chart: neutral reference marks */
--s-alt: #A8802A; /* chart: second series (validated with --s-final) */
--prop-bg: #FBF1E6;
--ok: #247058; --ok-bg: #E8F2DD;
--warn: #7F5E08; --warn-bg: #F8F0DC; --warn-line: #C9BA9B;
--bad: #C0392B; --bad-bg: #F6E5E4;
--info-bg: #E8F0F6; --info-line: #A5BCCD;
--st-support: #247058; --st-modify: #8F6C0E; --st-oppose: #C0392B; --st-unclear: #6E6E6A;
--chip-ink: #FFFFFF;
--f-display: "Playfair Display", Georgia, "Times New Roman", serif;
--f-body: "Source Serif 4", Georgia, "Times New Roman", serif;
--f-mono: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #0F1419; --surface: #1A2028; --surface-2: #1F262E; --hover: #232A34;
--fg: #E8E5DD; --fg-2: #CFCCC5; --muted: #9A968E; --rule: #3A4452; --rule-soft: #2A323A; --ink-rule: #E8E5DD;
--nav-bg: #0A0E13; --nav-fg: #E8F0F8; --nav-hover: #1C2B3A; --primary: #5684A3;
--link: #5C9CC0; --link-hover: #85ABC2;
--s-final: #4F9BD0; --s-prop: #D07A35; --s-ruc: #9A968E; --s-alt: #AD8838; --prop-bg: #2A2219;
--ok: #4CB28A; --ok-bg: #1F3A2A; --warn: #E5BE5A; --warn-bg: #3A3220; --warn-line: #8A7A30;
--bad: #E07A6E; --bad-bg: #3C1F1F; --info-bg: #1A2E44; --info-line: #3A6A8F;
--st-support: #4CB28A; --st-modify: #C9A347; --st-oppose: #E07A6E; --st-unclear: #9A968E; --chip-ink: #0F1419;
color-scheme: dark;
}
}
:root[data-theme="dark"] {
--bg: #0F1419; --surface: #1A2028; --surface-2: #1F262E; --hover: #232A34;
--fg: #E8E5DD; --fg-2: #CFCCC5; --muted: #9A968E; --rule: #3A4452; --rule-soft: #2A323A; --ink-rule: #E8E5DD;
--nav-bg: #0A0E13; --nav-fg: #E8F0F8; --nav-hover: #1C2B3A; --primary: #5684A3;
--link: #5C9CC0; --link-hover: #85ABC2;
--s-final: #4F9BD0; --s-prop: #D07A35; --s-ruc: #9A968E; --s-alt: #AD8838; --prop-bg: #2A2219;
--ok: #4CB28A; --ok-bg: #1F3A2A; --warn: #E5BE5A; --warn-bg: #3A3220; --warn-line: #8A7A30;
--bad: #E07A6E; --bad-bg: #3C1F1F; --info-bg: #1A2E44; --info-line: #3A6A8F;
--st-support: #4CB28A; --st-modify: #C9A347; --st-oppose: #E07A6E; --st-unclear: #9A968E; --chip-ink: #0F1419;
color-scheme: dark;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--fg); font-family: var(--f-body); font-size: 15px; line-height: 1.55; }
a { color: var(--link); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--link-hover); }
:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }
h1, h2, h3 { text-wrap: balance; }
.mono { font-family: var(--f-mono); }
.num { font-family: var(--f-mono); font-variant-numeric: tabular-nums; font-size: 12.5px; }
.eyebrow { font-family: var(--f-mono); font-size: 11px; text-transform: uppercase; letter-spacing: .12em; color: var(--muted); }
.muted { color: var(--muted); }
.nowrap { white-space: nowrap; }
.small { font-size: 13px; }
/* navbar: the fhirworx dark bar with its 3px ink line */
.nav { background: var(--nav-bg); color: var(--nav-fg); border-bottom: 3px solid var(--ink-rule); }
.nav-in { max-width: 1480px; margin: 0 auto; padding-left: max(16px, env(safe-area-inset-left, 0px)); padding-right: max(16px, env(safe-area-inset-right, 0px)); padding-block: 10px; display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.brand-group { display: inline-flex; align-items: baseline; gap: 12px; }
.brand { font-family: var(--f-display); font-weight: 700; font-size: 20px; letter-spacing: -.01em; color: var(--nav-fg); }
.app { font-family: var(--f-display); font-style: italic; font-weight: 600; font-size: 20px; letter-spacing: .01em; color: var(--nav-fg); padding-left: 12px; border-left: 1px solid color-mix(in srgb, var(--nav-fg) 40%, transparent); }
.nav-item { font-family: var(--f-mono); font-size: 12px; letter-spacing: .06em; color: var(--nav-fg); opacity: .78; padding: 4px 8px; }
.nav-item.on { opacity: 1; background: var(--nav-hover); }
a.brand, a.app, a.nav-item { text-decoration: none; }
a.brand:hover, a.app:hover, a.nav-item:hover { color: var(--nav-fg); opacity: 1; }
.nav-sp { flex: 1; }
/* banner: what lex is, then the build status as a smaller second line */
.banner { background: var(--surface-2); border-bottom: 1px solid var(--rule-soft); }
.banner-in { max-width: 1480px; margin: 0 auto; padding-left: max(16px, env(safe-area-inset-left, 0px)); padding-right: max(16px, env(safe-area-inset-right, 0px)); padding-block: 10px 9px; }
.banner-what { margin: 0; max-width: 96ch; font-size: 15px; line-height: 1.5; text-wrap: pretty; }
.banner-what .lex-word { font-family: var(--f-display); font-weight: 700; font-size: 17px; }
.banner-status { margin: 5px 0 0; font-size: 12.5px; color: var(--fg-2); display: flex; gap: 4px 10px; align-items: baseline; flex-wrap: wrap; }
.status-pill { font-family: var(--f-mono); font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .12em; color: var(--warn); background: var(--warn-bg); border: 1px solid var(--warn-line); padding: 1px 6px; }
.shell { max-width: 1480px; margin: 0 auto; padding-left: max(16px, env(safe-area-inset-left, 0px)); padding-right: max(16px, env(safe-area-inset-right, 0px)); display: grid; grid-template-columns: 236px minmax(0, 1fr) 330px; align-items: start; }
/* left rail: the book */
.rail { position: sticky; top: 0; max-height: 100vh; overflow: auto; padding: 22px 18px 24px 0; border-right: 1px solid var(--rule-soft); }
.rail input { width: 100%; font: inherit; font-size: 14px; padding: 7px 10px; border: 1px solid var(--rule); background: var(--surface); color: var(--fg); border-radius: 0; }
.rail h2 { font-family: var(--f-display); font-size: 1.15rem; margin: 18px 0 6px; }
.toc { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.toc li { display: flex; gap: 8px; align-items: baseline; justify-content: space-between; padding: 4px 6px; font-size: 14px; }
.toc li.cur { background: var(--surface); border-left: 3px solid var(--ink-rule); }
.toc li.cur a { color: var(--fg); font-weight: 600; text-decoration: none; }
.toc .tier { font-family: var(--f-mono); font-size: 10px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); white-space: nowrap; }
.toc li.planned span:first-child { color: var(--fg-2); }
.rail .note { font-size: 13px; color: var(--muted); margin-top: 14px; border-top: 1px solid var(--rule-soft); padding-top: 10px; }
.rail details summary { cursor: pointer; list-style: none; }
.rail details summary::-webkit-details-marker { display: none; }
.rail details:not([open]) summary h2::after { content: " +"; color: var(--muted); font-family: var(--f-mono); font-size: 14px; }
/* main pane */
.main { min-width: 0; padding: 22px 26px 64px; }
.mast .rule { border-top: 3px solid var(--ink-rule); margin-bottom: 12px; }
.mast-row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.mast h1 { font-family: var(--f-display); font-weight: 800; font-size: clamp(1.9rem, 3.6vw, 2.7rem); line-height: 1.1; letter-spacing: -.02em; margin: 2px 0 6px; }
.mast .tag { font-family: var(--f-mono); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; padding: 3px 8px; border: 1px solid var(--ok); color: var(--ok); white-space: nowrap; }
.mast .rule-thin { border-top: 1px solid var(--rule); margin-top: 12px; }
.tabs-wrap { overflow-x: auto; margin: 0 0 22px; border-bottom: 1px solid var(--rule); }
.tabs { display: flex; gap: 2px; flex-wrap: wrap; align-items: flex-end; }
.tabs .grp { font-family: var(--f-mono); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); padding: 0 6px 9px 10px; }
.tabs a { font-family: var(--f-mono); font-size: 12px; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); text-decoration: none; padding: 9px 10px 8px; border-bottom: 3px solid transparent; white-space: nowrap; }
.tabs a:hover { color: var(--fg); background: var(--hover); }
.tabs a.on { color: var(--fg); border-bottom-color: var(--ink-rule); font-weight: 600; }
.tabs a.soon { border-bottom-style: dashed; }
.tabs a.soon::after { content: "coming"; margin-left: 6px; font-size: 9px; padding: 1px 4px; border: 1px dashed var(--rule); color: var(--muted); vertical-align: 1px; }
.view h2 { font-family: var(--f-display); font-weight: 700; font-size: 1.55rem; letter-spacing: -.01em; margin: 0 0 4px; }
.view h3 { font-family: var(--f-display); font-weight: 700; font-size: 1.15rem; margin: 26px 0 8px; }
.view .lede { color: var(--fg-2); max-width: 72ch; margin: 0 0 16px; }
.sec { margin-top: 26px; }
/* tiles, as on the homelab landing */
.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); gap: 16px; }
.tile { background: var(--surface); border: 1px solid var(--rule); border-top: 3px solid var(--ink-rule); padding: 14px 16px 16px; min-width: 0; }
.tile .t-title { font-family: var(--f-display); font-weight: 700; font-size: 1.15rem; margin: 2px 0 8px; }
.tile .t-title a { color: var(--fg); text-decoration: none; }
.tile .t-title a:hover { color: var(--link); }
.facts { list-style: none; margin: 0; padding: 0; display: grid; gap: 7px; }
.facts li { font-size: 14px; line-height: 1.45; }
.cite { font-family: var(--f-mono); font-size: 11px; white-space: nowrap; text-decoration: none; color: var(--link); border: 1px solid var(--rule-soft); background: var(--surface); padding: 0 5px; margin-left: 2px; display: inline-block; line-height: 1.6; }
.cite::after { content: " \2197"; font-size: 10px; }
.cite:hover { border-color: var(--link); }
.cite.book { color: var(--fg-2); }
.cite.book::after { content: ""; }
.codes { display: flex; flex-wrap: wrap; gap: 4px; }
.code { font-family: var(--f-mono); font-size: 12px; padding: 1px 6px; border: 1px solid var(--rule); background: var(--surface); }
.code.del { text-decoration: line-through; color: var(--muted); border-style: dashed; }
.code.lnk { border-color: var(--link); color: var(--link); }
.hdr-grid { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 8px 16px; margin: 10px 0 4px; align-items: baseline; }
.hdr-grid > .eyebrow { padding-top: 3px; }
.panel { border: 1px solid var(--rule); border-left-width: 4px; background: var(--surface); padding: 12px 16px; }
.panel.warn { border-left-color: var(--warn); }
.panel.info { border-left-color: var(--link); }
.panel.soon { border-left-color: var(--muted); border-style: dashed; border-left-style: solid; }
.panel h3 { margin-top: 0 !important; }
.gaps { margin: 6px 0 0; padding-left: 18px; display: grid; gap: 5px; font-size: 14px; }
.pill { font-family: var(--f-mono); font-size: 10.5px; letter-spacing: .06em; text-transform: uppercase; padding: 1px 6px; border: 1px solid var(--rule); color: var(--fg-2); white-space: nowrap; display: inline-block; }
.pill.prelim { border-color: var(--warn); color: var(--warn); background: var(--warn-bg); }
.pill.cms { border-color: var(--link); color: var(--link); background: var(--info-bg); }
.pill.prop { border-color: var(--s-prop); border-style: dashed; color: var(--fg); background: var(--prop-bg); }
.pill.final { border-color: var(--s-final); color: var(--fg); }
.pill.model { border-style: dotted; color: var(--muted); }
.pill.soon { border-style: dashed; color: var(--muted); }
/* tables */
.tbl-wrap { overflow-x: auto; border: 1px solid var(--rule); background: var(--surface); }
table.tbl { border-collapse: collapse; width: 100%; font-size: 14px; }
.tbl th { font-family: var(--f-mono); font-size: 10.5px; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); font-weight: 500; text-align: left; padding: 8px 9px 6px; border-bottom: 2px solid var(--ink-rule); background: var(--surface); white-space: nowrap; vertical-align: bottom; }
.tbl td { padding: 6px 9px; border-bottom: 1px solid var(--rule-soft); vertical-align: top; }
.tbl tr:last-child td { border-bottom: 0; }
.tbl td.r, .tbl th.r { text-align: right; }
.tbl tbody tr[data-ev] { cursor: pointer; }
.tbl tbody tr[data-ev]:hover td { background: var(--hover); }
.tbl tr.sel td { background: var(--info-bg) !important; }
.tbl tr.sel td:first-child { box-shadow: inset 3px 0 0 var(--link); }
.tbl .grp-row td { background: var(--surface-2); font-family: var(--f-mono); font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--fg-2); padding-top: 7px; }
.tbl td.pcol, .tbl th.pcol { background: var(--prop-bg); }
.tbl th.pcol { border-left: 2px dashed var(--s-prop); }
.tbl td.pcol { border-left: 2px dashed var(--s-prop); font-style: italic; }
.tbl td.na { color: var(--muted); }
.chg { position: relative; }
.chg::before { content: ""; position: absolute; left: 3px; top: 50%; width: 5px; height: 5px; margin-top: -3px; background: var(--s-prop); border-radius: 50%; }
/* stance */
.st { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 20px; font-family: var(--f-mono); font-size: 11px; font-weight: 600; color: var(--chip-ink); }
.st.support { background: var(--st-support); }
.st.modify { background: var(--st-modify); }
.st.oppose { background: var(--st-oppose); }
.st.unclear { background: var(--st-unclear); }
.st.none { background: transparent; color: var(--rule); font-weight: 400; }
.legend { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center; font-size: 13px; color: var(--fg-2); }
.legend span { display: inline-flex; gap: 6px; align-items: center; }
.sw { width: 12px; height: 12px; display: inline-block; }
.bar { display: flex; height: 22px; gap: 2px; min-width: 0; }
.bar > div { display: flex; align-items: center; justify-content: center; font-family: var(--f-mono); font-size: 11px; color: var(--chip-ink); min-width: 18px; }
.two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 18px; align-items: start; }
.col-h { display: flex; gap: 8px; align-items: center; margin: 0 0 8px; flex-wrap: wrap; }
.col-h h3 { margin: 0 !important; }
.filters { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 0 0 14px; }
.fbtn { font-family: var(--f-mono); font-size: 11.5px; letter-spacing: .04em; padding: 4px 9px; border: 1px solid var(--rule); background: var(--surface); color: var(--fg-2); cursor: pointer; border-radius: 0; }
.fbtn:hover { border-color: var(--fg); }
.fbtn[aria-pressed="true"] { background: var(--primary); color: var(--surface); border-color: var(--primary); }
.fbtn[aria-pressed="true"] .muted { color: inherit; opacity: .75; }
.tbl .stick { position: sticky; left: 0; z-index: 1; background: var(--surface); box-shadow: 1px 0 0 var(--rule-soft); }
.tbl tr.sel td.stick { background: var(--info-bg); }
.cms-list { display: grid; gap: 0; border: 1px solid var(--rule); background: var(--surface); }
.cms-list > div { padding: 9px 12px; border-bottom: 1px solid var(--rule-soft); cursor: pointer; display: grid; gap: 4px; }
.cms-list > div:last-child { border-bottom: 0; }
.cms-list > div:hover { background: var(--hover); }
.cms-list > div.sel { background: var(--info-bg); box-shadow: inset 3px 0 0 var(--link); }
.cms-list q { font-size: 13.5px; quotes: "\201C" "\201D"; }
.charts { display: grid; gap: 16px; margin-top: 18px; }
.charts .chart-box svg { max-width: 760px; }
select.fsel { font-family: var(--f-mono); font-size: 12px; padding: 4px 6px; border: 1px solid var(--rule); background: var(--surface); color: var(--fg); border-radius: 0; }
/* timeline */
.tl-year { display: grid; grid-template-columns: 86px minmax(0, 1fr); gap: 12px; border-top: 1px solid var(--rule); padding: 12px 0 6px; }
.tl-year > h4 { font-family: var(--f-display); font-size: 1.3rem; margin: 0; line-height: 1.1; }
.tl-year > h4 small { display: block; font-family: var(--f-mono); font-size: 10.5px; color: var(--muted); font-weight: 400; letter-spacing: .06em; margin-top: 4px; }
.tl-items { display: grid; gap: 8px; min-width: 0; }
.tl-item { border: 1px solid var(--rule-soft); background: var(--surface); padding: 8px 12px; cursor: pointer; display: grid; gap: 4px; min-width: 0; }
.tl-item:hover { border-color: var(--rule); }
.tl-item.sel { border-color: var(--link); box-shadow: inset 3px 0 0 var(--link); }
.tl-item .top { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
.tl-item .ttl { font-weight: 600; }
.tl-item .sum { font-size: 13.5px; color: var(--fg-2); }
.type { font-family: var(--f-mono); font-size: 10px; letter-spacing: .08em; text-transform: uppercase; padding: 1px 5px; border: 1px solid var(--rule); color: var(--fg-2); white-space: nowrap; }
.type.lineage { border-color: var(--rule); }
.type.origin { border-color: var(--s-alt); }
.type.valuation { border-color: var(--s-final); }
.type.cms { border-color: var(--link); background: var(--info-bg); }
.type.link { border-color: var(--ok); }
/* story strip */
.steps { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 0; border: 1px solid var(--rule); background: var(--surface); }
.steps li { padding: 12px 14px; border-right: 1px solid var(--rule-soft); min-width: 0; display: grid; gap: 6px; align-content: start; cursor: pointer; }
.steps li:last-child { border-right: 0; }
.steps li:hover { background: var(--hover); }
.steps .n { font-family: var(--f-mono); font-size: 11px; color: var(--muted); letter-spacing: .08em; }
.steps .h { font-family: var(--f-display); font-weight: 700; font-size: 1.02rem; line-height: 1.25; }
.steps q { font-size: 13.5px; color: var(--fg-2); quotes: "\201C" "\201D"; }
.chart { width: 100%; height: auto; display: block; }
.chart text { fill: var(--fg-2); font-family: var(--f-mono); font-size: 11px; }
.chart .ax { stroke: var(--rule); stroke-width: 1; }
.chart .grid { stroke: var(--rule-soft); stroke-width: 1; }
.chart-box { border: 1px solid var(--rule); background: var(--surface); padding: 12px 12px 6px; min-width: 0; }
.tip { position: fixed; pointer-events: none; z-index: 30; background: var(--fg); color: var(--bg); font-family: var(--f-mono); font-size: 11.5px; padding: 5px 8px; max-width: 260px; }
/* evidence drawer */
.drawer { position: sticky; top: 0; max-height: 100vh; overflow: auto; padding: 22px 0 28px 20px; border-left: 1px solid var(--rule-soft); min-width: 0; }
.drawer .d-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.drawer .close { display: none; font-family: var(--f-mono); font-size: 12px; border: 1px solid var(--rule); background: var(--surface); color: var(--fg); padding: 3px 8px; cursor: pointer; }
.drawer h3 { font-family: var(--f-display); font-size: 1.12rem; margin: 6px 0 8px; line-height: 1.25; }
.ev-src { border-top: 1px solid var(--rule); padding-top: 10px; margin-top: 12px; display: grid; gap: 6px; }
.ev-src .src-t { font-size: 13px; color: var(--fg-2); }
.ev-src blockquote { margin: 0; padding: 8px 10px; background: var(--surface); border-left: 3px solid var(--rule); font-size: 14px; line-height: 1.5; }
.ev-src blockquote.cmt { border-left-color: var(--s-prop); }
.ev-dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 3px 10px; font-size: 13.5px; margin: 0; }
.ev-dl dt { font-family: var(--f-mono); font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); padding-top: 3px; }
.ev-dl dd { margin: 0; min-width: 0; }
.sketch { border: 1px dashed var(--rule); padding: 14px; background: var(--surface); position: relative; }
.sketch::before { content: "Sketch with real rows"; position: absolute; top: -9px; left: 10px; background: var(--bg); padding: 0 6px; font-family: var(--f-mono); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.ph { border: 1px dashed var(--rule); color: var(--muted); font-style: italic; padding: 6px 8px; font-size: 13.5px; }
button.dis { font-family: var(--f-mono); font-size: 11px; padding: 3px 8px; border: 1px solid var(--rule); background: var(--surface-2); color: var(--muted); cursor: not-allowed; }
@media (max-width: 1180px) {
.shell { grid-template-columns: 214px minmax(0, 1fr); }
.drawer { position: fixed; left: 0; right: 0; bottom: 0; top: auto; max-height: 52vh; z-index: 20; background: var(--bg); border-left: 0; border-top: 3px solid var(--ink-rule); padding: 14px max(16px, env(safe-area-inset-right, 0px)) calc(16px + env(safe-area-inset-bottom, 0px)) max(16px, env(safe-area-inset-left, 0px)); box-shadow: 0 -8px 24px rgba(0,0,0,.18); }
.drawer:not(.open) { display: none; }
.drawer .close { display: inline-block; }
body.drawer-open .main { padding-bottom: 54vh; }
}
@media (max-width: 760px) {
.shell { grid-template-columns: minmax(0, 1fr); }
.rail { position: static; max-height: none; border-right: 0; border-bottom: 1px solid var(--rule-soft); padding: 14px 0; }
.main { padding: 18px 0 64px; }
.two { grid-template-columns: minmax(0, 1fr); }
.tabs { flex-wrap: nowrap; min-width: max-content; }
.nav-note { display: none; }
.tl-year { grid-template-columns: minmax(0, 1fr); gap: 6px; }
.steps { grid-template-columns: minmax(0, 1fr); }
.steps li { border-right: 0; border-bottom: 1px solid var(--rule-soft); }
.hdr-grid { grid-template-columns: minmax(0, 1fr); gap: 2px 0; }
.hdr-grid > .eyebrow { padding-top: 8px; }
}
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
</style>
</head>
<body>
<header class="nav"><div class="nav-in">
<span class="brand-group"><a class="brand" href="https://git./*__DOMAIN__*//">fhirworx</a><a class="app" href="/" aria-current="page">lex</a></span>
<a class="nav-item" href="https://git./*__DOMAIN__*//explore/repos">Explore</a>
<span class="nav-item on">Dossier</span>
<span class="nav-sp"></span>
<span class="nav-item nav-note">Policy evidence &middot; CMS payment and quality</span>
</div></header>
<div class="banner" role="note"><div class="banner-in">
<p class="banner-what"><strong class="lex-word">lex</strong>&nbsp;— Latin for <em>law</em>. A legal-epidemiology workbench for CMS payment and quality rules: for each code family it traces what the rules said, how they changed, who argued for and against each change, and what happened after&nbsp;— to inform predictions, policy work and comment letters.</p>
<p class="banner-status"><span class="status-pill">Preview</span><span>Static snapshot of live data, built /*__BUILT__*/. Not the finished app.</span><span class="muted">Every value below comes from a stored row; each carries its source.</span></p>
</div></div>
<div class="shell">
<aside class="rail" aria-label="The book">
<label class="eyebrow" for="q">Search the book</label>
<input id="q" type="search" placeholder="Family, code or name" autocomplete="off">
<details id="toc-d" open>
<summary><h2>The book</h2></summary>
<div class="eyebrow" style="margin-top:4px">Reviewed</div>
<ul class="toc" id="toc-rev"></ul>
<div class="eyebrow" style="margin-top:12px">Planned &middot; primary and home-based care</div>
<ul class="toc" id="toc-plan"></ul>
<p class="note" id="toc-note"></p>
</details>
</aside>
<main class="main" id="main">
<div class="mast" id="mast"></div>
<div class="tabs-wrap"><nav class="tabs" id="tabs" aria-label="Views"></nav></div>
<section class="view" data-view="contents" hidden></section>
<section class="view" data-view="dossier" hidden></section>
<section class="view" data-view="timeline" hidden></section>
<section class="view" data-view="valuation" hidden></section>
<section class="view" data-view="payment" hidden></section>
<section class="view" data-view="politics" hidden></section>
<section class="view" data-view="sources" hidden></section>
<section class="view" data-view="grid" hidden></section>
<section class="view" data-view="evaluate" hidden></section>
<section class="view" data-view="predict" hidden></section>
<section class="view" data-view="draft" hidden></section>
<section class="view" data-view="review" hidden></section>
</main>
<aside class="drawer" id="drawer" aria-label="Evidence" aria-live="polite"></aside>
</div>
<div class="tip" id="tip" hidden></div>
<script id="data" type="application/json">/*__DATA__*/{}</script>
<script>
(function () {
"use strict";
const D = JSON.parse(document.getElementById("data").textContent);
const $ = (s, r) => (r || document).querySelector(s);
const $$ = (s, r) => Array.from((r || document).querySelectorAll(s));
const esc = (s) => String(s == null ? "" : s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
const money = (v) => (v == null ? "—" : "$" + v.toFixed(2));
const n2 = (v) => (v == null ? "—" : v.toFixed(2));
const int = (v) => (v == null ? "—" : Math.round(v).toLocaleString("en-US"));
const uniq = (a) => Array.from(new Set(a));
const M = D.meta;
const BASE = M.base, PRED = M.pred, LINKED = M.linked;
function compact(codes) {
const cs = uniq(codes).sort();
const out = [];
let i = 0;
while (i < cs.length) {
let j = i;
while (j + 1 < cs.length && /^\d+$/.test(cs[j]) && /^\d+$/.test(cs[j + 1]) && +cs[j + 1] === +cs[j] + 1) j++;
if (j - i >= 2) out.push(cs[i] + "–" + cs[j]); else out.push(...cs.slice(i, j + 1));
i = j + 1;
}
return out.join(", ");
}
const srcOf = (k) => D.sources[k] || null;
const ruleName = (k) => { const s = srcOf(k); return s ? (s.label || s.title) : k; };
function citeA(c) {
if (!c) return "";
return `<a class="cite" href="${esc(c.url)}" target="_blank" rel="noopener" title="${esc(ruleName(c.k))}">${esc(c.label)}</a>`;
}
function bookCite(b) {
if (!b) return "";
return `<span class="cite book" title="Licensed book on file; cited by edition and page">${esc(b.title)}${b.page ? ", p. " + esc(b.page) : ""}</span>`;
}
function rvuCite(r) {
if (!r) return "";
return `<a class="cite" href="${esc(r.url)}" target="_blank" rel="noopener" title="${esc(r.label)}">${esc(r.label.replace(" (RVU file)", ""))}</a>`;
}
const linkA = (url, text) => `<a href="${esc(url)}" target="_blank" rel="noopener">${esc(text)}</a>`;
/* ── evidence registry and drawer ─────────────────────────────── */
const EV = [];
const ev = (o) => { EV.push(o); return EV.length - 1; };
const drawer = $("#drawer");
function paraBlock(c) {
if (!c) return "";
const s = srcOf(c.k);
const t = c.p ? D.paras[c.k + ":" + c.p] : "";
const meta = s ? [s.label || s.title, s.fr ? "FR Doc " + s.fr.doc : "", s.date ? "published " + s.date : ""].filter(Boolean).join(" · ") : "";
return `<div class="ev-src"><div>${citeA(c)}</div><div class="src-t">${esc(meta)}</div>` +
(t ? `<blockquote>${esc(t)}</blockquote>` : `<div class="small muted">${c.p ? "" : "Printed-page link. This page carries a table, which has no paragraph anchors."}</div>`) + `</div>`;
}
function renderEvidence(e) {
let h = `<div class="d-head"><span class="eyebrow">Evidence${e.kind ? " · " + esc(e.kind) : ""}</span><button class="close" type="button" id="d-close">Close</button></div>`;
h += `<h3>${esc(e.title)}</h3>`;
if (e.badges) h += `<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px">${e.badges}</div>`;
if (e.fields && e.fields.length) h += `<dl class="ev-dl">${e.fields.map(([k, v]) => `<dt>${esc(k)}</dt><dd>${v}</dd>`).join("")}</dl>`;
if (e.html) h += e.html;
(e.cites || []).filter(Boolean).forEach((c) => { h += paraBlock(c); });
(e.extra || []).forEach((x) => { h += `<div class="ev-src">${x}</div>`; });
return h;
}
let selEls = [];
function showEvidence(id, el) {
const e = EV[id];
if (!e) return;
drawer.innerHTML = renderEvidence(e);
drawer.classList.add("open");
document.body.classList.add("drawer-open");
selEls.forEach((x) => x.classList.remove("sel"));
selEls = el ? [el] : [];
selEls.forEach((x) => x.classList.add("sel"));
const b = $("#d-close");
if (b) b.addEventListener("click", () => { drawer.classList.remove("open"); document.body.classList.remove("drawer-open"); });
}
document.addEventListener("click", (evt) => {
if (evt.target.closest("a, button, select, input, label")) return;
const el = evt.target.closest("[data-ev]");
if (el) showEvidence(+el.dataset.ev, el);
});
document.addEventListener("keydown", (evt) => {
if (evt.key !== "Enter" && evt.key !== " ") return;
const el = evt.target.closest && evt.target.closest("[data-ev]");
if (el && evt.target === el) { evt.preventDefault(); showEvidence(+el.dataset.ev, el); }
});
const evAttr = (id) => `data-ev="${id}" tabindex="0"`;
/* tooltip for charts */
const tip = $("#tip");
document.addEventListener("pointermove", (evt) => {
const t = evt.target.closest && evt.target.closest("[data-tip]");
if (!t) { tip.hidden = true; return; }
tip.textContent = t.getAttribute("data-tip");
tip.hidden = false;
const x = Math.min(evt.clientX + 14, window.innerWidth - 270);
tip.style.left = x + "px";
tip.style.top = evt.clientY + 14 + "px";
});
/* ── derived facts ─────────────────────────────────────────────── */
const ruleSources = Object.values(D.sources).filter((s) => s.fr);
const latestRule = ruleSources.slice().sort((a, b) => (a.date < b.date ? 1 : -1))[0];
const earliestCY = Math.min(...D.summaries.map((s) => s.cy), ...D.events.map((e) => e.year), ...D.rounds.map((r) => r.cy));
const pay = (code, cy, prop) => D.payment.find((p) => p.code === code && p.cy === cy && p.proposed === !!prop);
const memberCite = (code) => (D.members.find((m) => m.code === code) || {}).cite;
const linkOf = (t) => D.links.find((l) => l.target === t);
const cfOf = (cy, prop) => D.cfs.find((c) => c.cy === cy && c.proposed === !!prop);
const rounds23 = D.rounds.filter((r) => r.cy === 2023);
const r23f = rounds23.filter((r) => r.kind === "final");
const r23p = rounds23.filter((r) => r.kind === "proposed");
const r08f = D.rounds.filter((r) => r.cy === 2008 && r.kind === "final");
const r08p = D.rounds.filter((r) => r.cy === 2008 && r.kind === "proposed");
const pctCount = (rows, p) => rows.filter((r) => r.pct === p).length;
const acceptN = (rows) => rows.filter((r) => r.fin != null && r.ruc != null && Math.abs(r.fin - r.ruc) < 1e-9).length;
const C = (k, p) => {
const all = [];
D.rounds.forEach((r) => { if (r.cite) all.push(r.cite); Object.values(r.cites || {}).forEach((a) => a.forEach((c) => c && all.push(c))); });
D.summaries.forEach((s) => { all.push(s.comment, s.response); });
D.links.forEach((l) => { all.push(l.cite, l.alias_cite); });
D.members.forEach((m) => all.push(m.cite));
D.events.forEach((e) => all.push(e.cite));
D.origins.forEach((o) => { all.push(o.cite, o.ref_cite); });
D.guidance.forEach((g) => all.push(g.src));
return all.find((c) => c && c.k === k && c.p === p) || null;
};
const C1676 = C("PETM225D", 1676), C1685 = C("PETM225D", 1685), C878 = C("MZ24MX5S", 878), C1672 = C("PETM225D", 1672);
const C1679 = C("PETM225D", 1679), C1681 = C("PETM225D", 1681), C2241 = C("54GTKR5G", 2241), C2242 = C("54GTKR5G", 2242);
const C877 = C("MZ24MX5S", 877);
const pageCite = (k, pg) => { const s = srcOf(k); return s && s.fr ? { k, p: 0, pg, label: `${s.fr.vol} FR ${pg}`, url: `${s.fr.html}#page-${pg}` } : null; };
const C08p = r08p.length ? r08p[0].cite : null;
const STANCES = ["support", "modify", "oppose", "unclear"];
const ST_LETTER = { support: "S", modify: "M", oppose: "O", unclear: "U" };
const EVENT_ORDER = ["valuation", "coding", "telehealth", "G0318", "other"];
const EVENT_LABEL = { valuation: "Work RVU valuation", coding: "Coding changes", telehealth: "Telehealth list", G0318: "G0318 prolonged add-on (new)", other: "Other aspects of the family's codes" };
const evSlug = (k) => k.split(":").slice(2).join(":");
const docketRow = D.reaction.find((r) => r.period === M.docket);
const stancesBy = {};
D.stances.forEach((s) => { (stancesBy[s.key] = stancesBy[s.key] || []).push(s); });
const withPos = Object.keys(stancesBy).length;
const named = D.commenters.filter((c) => c.name).length;
const TYPE_LABEL = { specialty_society: "Specialty society", health_system: "Health system", individual_clinician: "Individual clinician", other: "Other organization", patient_group: "Patient group", payer: "Payer", vendor: "Vendor" };
const titleCase = (s) => s ? s.toLowerCase().replace(/\b[a-z]/g, (m) => m.toUpperCase()) : "";
const ACTION = { finalize_as_proposed: "finalized as proposed", finalize_modified: "finalized with changes", not_finalized: "not finalized", acknowledged: "acknowledged", unclear: "response not classified" };
const quant = (q) => (q === "unspecified" ? "commenters (number not stated)" : q === "one" ? "one commenter" : q + " commenters");
const stancePill = (s) => s ? `<span class="pill">${esc(s)}</span>` : "";
const PLANNED = [
["Office/outpatient E/M", ""],
["G2211 and modifiers B1/B2", "G2211, B1, B2"],
["Chronic care management (CCM)", "99490, 99491, 99487–99489, 99437, 99439, G2058"],
["Principal care management (PCM)", "99424–99427, G2064, G2065"],
["Transitional care management (TCM)", "99495–99496"],
["Advanced primary care management (APCM)", "G0556–G0558"],
["Care plan oversight and home health certification", "G0179–G0182, 99339, 99340, 99374–99380"],
["Behavioral health integration and CoCM", "99484, 99492–99494"],
["Remote physiologic and therapeutic monitoring (RPM/RTM)", ""],
["Advance care planning (ACP)", "99497–99498"],
["Prolonged services", "99354–99357, 99417, 99418, G0316–G0318"],
["Virtual check-ins and communication-technology services", "G2010, G2012, G2252"],
];
/* ── rail ──────────────────────────────────────────────────────── */
function renderRail() {
$("#toc-rev").innerHTML = `<li class="cur" data-name="home visits home or residence services ${BASE.join(" ")} ${PRED.join(" ")} g0318"><a href="#dossier">Home or Residence Services</a><span class="tier">Built</span></li>`;
$("#toc-plan").innerHTML = PLANNED.map(([n, c]) => `<li class="planned" data-name="${esc((n + " " + c).toLowerCase())}"><span>${esc(n)}</span><span class="tier">Planned</span></li>`).join("");
$("#toc-note").innerHTML = `The machine-built tier covers everything else: ${M.derived_families.toLocaleString("en-US")} derived code families, ${M.derived_single.toLocaleString("en-US")} of them single codes, built unattended and labeled “machine-built, not reviewed”.`;
$("#q").addEventListener("input", (e) => {
const q = e.target.value.trim().toLowerCase();
$$(".toc li").forEach((li) => { li.hidden = !!q && !li.dataset.name.includes(q); });
});
if (window.matchMedia("(max-width: 760px)").matches) $("#toc-d").open = false;
}
/* ── masthead and tabs ─────────────────────────────────────────── */
const VIEWS = [
["book", [["contents", "Contents"]]],
["HOME-VISITS", [["dossier", "Dossier"], ["timeline", "Timeline"], ["valuation", "Valuation"], ["payment", "Payment"], ["politics", "Politics"], ["sources", "Sources"]]],
["coming", [["grid", "Grid"], ["evaluate", "Evaluate"], ["predict", "Predict"], ["draft", "Draft"], ["review", "Review"]]],
];
function renderTabs() {
$("#tabs").innerHTML = VIEWS.map(([g, vs]) => `<span class="grp">${esc(g)}</span>` + vs.map(([id, t]) => `<a href="#${id}" data-tab="${id}" class="${g === "coming" ? "soon" : ""}">${t}</a>`).join("")).join("");
}
function renderMast(view) {
if (view === "contents") {
$("#mast").innerHTML = `<div class="rule"></div><div class="mast-row"><span class="eyebrow">The book · CMS payment and quality regulation</span></div>
<h1>Contents</h1><div class="rule-thin"></div>`;
return;
}
$("#mast").innerHTML = `<div class="rule"></div>
<div class="mast-row"><span class="eyebrow">${esc(M.family)} · code family dossier · phase 1</span><span class="tag">Reviewed dossier</span></div>
<h1>${esc(M.name)}</h1>
<div class="small muted">Home or residence E/M visits ${compact(BASE)} · domiciliary codes deleted for CY2023 · linked add-on G0318</div>
<div class="rule-thin"></div>`;
}
/* ── #contents ─────────────────────────────────────────────────── */
function domains() {
const st = D.origins.find((o) => o.kind === "statute" && /1833\(x\)/.test(o.ref) && o.codes.includes("99350"));
const mssp = D.origins.find((o) => o.kind === "regulation" && /425\.400/.test(o.ref));
const tele = D.events.filter((e) => e.kind === "telehealth_list" && e.codes.some((c) => BASE.includes(c)));
const out = [
["Home-based care", "The family's curated name: " + M.name, null],
["Primary care", st ? `${st.ref} (${st.summaries[0] ? st.summaries[0].text : ""})` : "", st && st.cite],
["APM linkage", mssp ? `${mssp.ref}, Medicare Shared Savings Program` : "", mssp && mssp.cite],
["Telehealth", tele.length ? `Telehealth list events in CY${uniq(tele.map((t) => t.year)).join(", CY")}` : "", tele.length ? tele[tele.length - 1].cite : null],
];
return out.filter((d) => d[1]);
}
function renderContents() {
const dm = domains();
const lr = latestRule;
const sec = ["Timeline", "Valuation CY2008, CY2023", `Payment ${M.rvu_years[0]}–${M.rvu_years[1]}`, "Politics CY2023 docket", "Sources"];
const homeId = ev({ kind: "family", title: M.name, fields: [["Key", esc(M.family)], ["Tier", "Reviewed"], ["Codes", esc(compact(BASE))], ["Deleted CY2023", esc(compact(PRED))]], extra: dm.map((d) => `<div><b>${esc(d[0])}</b>: ${esc(d[1])} ${citeA(d[2])}</div>`) });
let h = `<p class="lede">One dossier per code family. Reviewed dossiers are hand-curated and pass the review queue; everything else is machine-built and labeled so.</p>
<div class="tbl-wrap"><table class="tbl"><thead><tr><th style="min-width:210px">Family</th><th>Tier</th><th>Phase</th><th>Value domains</th><th>Coverage</th><th>Last rule activity</th><th>Open comment period</th></tr></thead><tbody>
<tr ${evAttr(homeId)}><td><a href="#dossier"><b>${esc(M.name)}</b></a><div class="small muted mono">${esc(compact(BASE))}</div></td>
<td><span class="pill final">Reviewed</span></td><td class="num">1</td>
<td><div class="codes">${dm.map((d) => `<span class="pill" title="${esc(d[1])}">${esc(d[0])}</span>`).join("")}</div></td>
<td class="small" style="min-width:150px">${sec.map((x) => `<div>${esc(x)}</div>`).join("")}<div class="muted">gaps stated on the dossier</div></td>
<td class="small">${esc(lr.label)}<br>${lr.fr ? `<a class="cite" href="${esc(lr.fr.html)}" target="_blank" rel="noopener">${esc(lr.cite)}</a>` : ""} <span class="muted">${esc(lr.date)}</span></td>
<td class="small muted">Tracker comes in S6</td></tr>
<tr class="grp-row"><td colspan="7">Planned · reviewed tier · primary care and home-based care</td></tr>`;
PLANNED.forEach(([n, c]) => {
h += `<tr><td>${esc(n)}<div class="small muted mono">${esc(c || "codes set at curation")}</div></td><td><span class="pill soon">Planned</span></td><td class="num">1</td><td colspan="4" class="muted small">Not built. Membership and value domains are set at curation.</td></tr>`;
});
h += `<tr class="grp-row"><td colspan="7">Machine-built tier</td></tr>
<tr><td colspan="7" class="small">Everything else: ${M.derived_families.toLocaleString("en-US")} derived code families, ${M.derived_single.toLocaleString("en-US")} of them single codes that fold into their CPT heading. Built in nightly batches and labeled “machine-built, not reviewed”, with gaps stated.</td></tr>
</tbody></table></div>
<div class="filters" style="margin-top:14px"><button class="dis" disabled>Propose a new family</button><span class="small muted">Goes to curation. Comes with S2.</span></div>`;
$('[data-view="contents"]').innerHTML = h;
}
/* ── #dossier ──────────────────────────────────────────────────── */
function factLi(text, cites) { return `<li>${text} ${(cites || []).filter(Boolean).map(citeA).join(" ")}</li>`; }
function renderDossier() {
const g0318 = linkOf("G0318"), g2211 = linkOf("G2211"), b1 = linkOf("B1"), b2 = linkOf("B2");
const st27 = BASE.map((c) => (pay(c, 2027, false) || {}).status);
const allA = st27.every((s) => s === "A");
const rf27 = pay(BASE[0], 2027, false);
const predFr = PRED.filter((c) => (D.members.find((m) => m.code === c) || {}).cite);
const predRvu = PRED.filter((c) => !(D.members.find((m) => m.code === c) || {}).cite);
const hdr = `
<div class="hdr-grid">
<span class="eyebrow">Active codes</span><div class="codes">${BASE.map((c) => `<span class="code">${c}</span>`).join("")} ${citeA(memberCite(BASE[0]))}</div>
<span class="eyebrow">Deleted for CY2023</span><div class="codes">${PRED.map((c) => `<span class="code del">${c}</span>`).join("")}</div>
<span></span><div class="small">${esc(compact(predFr))}: deleted by the CPT Editorial Panel, cited ${citeA(C1672)}. ${esc(compact(predRvu))}: last RVU file year 2022.</div>
<span class="eyebrow">Linked</span><div class="small" style="display:grid;gap:3px">
<div><span class="code lnk">G0318</span> prolonged add-on from CY${g0318.since}, proposed as ${esc(g0318.aliases.join(", "))} ${citeA(g0318.alias_cite)} finalized ${citeA(g0318.cite)}</div>
<div><span class="code lnk">G2211</span> add-on billable with the family from CY${g2211.since} ${citeA(g2211.cite)}</div>
<div><span class="code lnk">B1</span> <span class="code lnk">B2</span> modifiers from CY${b1.since} ${citeA(b1.cite)} ${citeA(b2.cite)}</div>
</div>
<span class="eyebrow">Status</span><div class="small">Payment status ${allA ? "A (active) for all 8 active codes" : esc(st27.join(", "))} in the CY2027 final RVU file ${rvuCite({ label: "PFS CY2027 Addendum B", url: rf27 ? rf27.url : "" })}</div>
<span class="eyebrow">Years</span><div class="small">Rules cited CY${earliestCY}–CY${latestRule.cy} · RVU files ${M.rvu_years[0]}–${M.rvu_years[1]} · utilization ${M.util_years[0]}–${M.util_years[1]}</div>
</div>`;
// section summaries, every line from rows
const tlCount = TL.length;
const statutes = uniq(D.origins.filter((o) => o.kind === "statute").map((o) => o.ref.replace(/ \(as added.*\)/, "").replace(/ of the Act$/, "")));
const regs = uniq(D.origins.filter((o) => o.kind === "regulation").map((o) => o.ref));
const statCite = uniq(D.origins.filter((o) => o.kind === "statute").map((o) => o.ref.replace(/ \(as added.*\)/, "").replace(/ of the Act$/, ""))).map((r) => (D.origins.find((o) => o.kind === "statute" && o.ref.startsWith(r)) || {}).cite);
const p15 = pay("99350", 2015, false), p27 = pay("99350", 2027, false), p27p = pay("99350", 2027, true);
const cf27 = cfOf(2027, false);
const cms23 = D.summaries.filter((s) => s.cy === 2023);
const nRules = ruleSources.length;
const cfrG = uniq(D.guidance.filter((g) => g.kind === "cfr" && !g.unresolved).map((g) => g.locator));
const iomG = uniq(D.guidance.filter((g) => g.kind === "iom").map((g) => g.locator));
const tiles = [
["timeline", "Timeline", [
factLi(`${tlCount} dated entries from lineage, origin, valuation rounds, CMS's comment summaries and links, CY${earliestCY}–CY${latestRule.cy}.`),
factLi(`Latest: ${esc(latestRule.label)}, B1 and B2 modifiers finalized.`, [b1.cite]),
]],
["timeline", "Origin", [
factLi(`CPT Editorial Panel, February 2021 meeting: domiciliary codes ${esc(compact(predFr.filter((c) => c !== "99343")))} and home code 99343 deleted, effective 2023.`, [C1672]),
factLi(`Statute cited: ${statutes.map(esc).join("; ")}.`, statCite),
factLi(`Regulation cited: ${regs.length} CFR sections (${regs.map(esc).join(", ")}).`),
]],
["valuation", "Valuation", [
factLi(`CY2023: the RUC recommended the survey 25th percentile for ${pctCount(r23f, "25th")} codes and the median for ${esc(r23f.filter((r) => r.pct === "median").map((r) => r.code).join(", "))}.`, [C1676]),
factLi(`CMS proposed the RUC values for ${r23p.length} of 8 codes and finalized them as proposed: final equals the RUC recommendation for ${acceptN(r23f)} of ${r23f.length}.`, [C878, C1685]),
factLi(`CY2008: CMS proposed the RUC values (“Agree”), then refined all ${r08f.length} in the final rule; final equals the RUC recommendation for ${acceptN(r08f)} of ${r08f.length}.`, [C08p, C2242]),
]],
["payment", "Payment", [
factLi(`99350 national non-facility payment: ${money(p15 && p15.pay_nf)} (CY2015 final) → ${money(p27 && p27.pay_nf)} (CY2027 final); CY2027 proposed ${money(p27p && p27p.pay_nf)}.`, []).replace("</li>", [p15, p27, p27p].filter(Boolean).map((r) => ` <a class="cite" href="${esc(r.url)}" target="_blank" rel="noopener" title="${esc(r.label)}">${esc(r.cite)}</a>`).join("") + "</li>"),
factLi(`Conversion factor CY2027 final $${cf27.cf.toFixed(4)}.`).replace("</li>", ` <a class="cite" href="https://www.federalregister.gov/citation/${cf27.cite.replace(/ /g, "-")}" target="_blank" rel="noopener">${esc(cf27.cite)}</a></li>`),
factLi(`Final RVU files ${M.rvu_years[0]}–${M.rvu_years[1]}; proposed RVUs for the CY2026 and CY2027 proposed rules.`),
]],
["politics", "Politics", [
factLi(`${D.commenters.length} commenters identified on the CY2023 docket ${linkA("https://www.regulations.gov/docket/" + M.docket, M.docket)}; ${withPos} took a position on at least one event <span class="pill prelim">preliminary</span>.`),
factLi(`CMS's summary: “${esc(firstSentence(D.paras["PETM225D:1679"] || ""))}”`, [C1679]),
factLi(`${D.summaries.length} CMS Comment/Response pairs about the family, CY${Math.min(...D.summaries.map((s) => s.cy))}–CY${Math.max(...D.summaries.map((s) => s.cy))}; ${cms23.length} in the CY2023 final rule.`),
]],
["sources", "Guidance and sources", [
factLi(`CFR pointers: ${cfrG.length} locators, e.g. ${esc(cfrG.slice(0, 2).join(", "))}. Manual pointers: ${esc(iomG.join(", "))}.`),
factLi(`${nRules} Federal Register rules with paragraph anchors, ${D.commenters.length} public comments, ${Object.values(D.sources).filter((s) => s.type === "source" && /Medicare Physician/.test(s.title)).length} utilization files.`),
]],
];
const gaps = [
"Federal Register PFS rules before CY2002 are not captured yet.",
`RVU files before 2015 are not captured yet; the payment table starts at CY${M.rvu_years[0]}.`,
"Comment dockets before the CY2018 rule are not captured yet; earlier rules show CMS's own summaries only.",
`Stance counts are preliminary. ${evalLine(docketRow)}`,
"Commenters are identified for the CY2023 docket only; other years are pending.",
"Proposed-rule RVUs are captured for the CY2026 and CY2027 proposed rules only.",
`regulations.gov posts a personal name for ${named} of the ${D.commenters.length} CY2023 commenters; the rest are shown by organization as posted.`,
`${D.review.length} extracted rows are held for review (see Review).`,
];
$('[data-view="dossier"]').innerHTML = hdr + `
<div class="sec"><h3>Sections</h3><div class="tiles">${tiles.map(([href, t, facts]) => `<div class="tile"><div class="eyebrow">Section</div><div class="t-title"><a href="#${href}">${t}</a></div><ul class="facts">${facts.join("")}</ul></div>`).join("")}</div></div>
<div class="sec panel warn"><h3>Gaps</h3><ul class="gaps">${gaps.map((g) => `<li>${esc(g)}</li>`).join("")}</ul></div>
<div class="filters" style="margin-top:14px"><button class="dis" disabled>Flag this</button><span class="small muted">Sends a row to the review queue. Comes with S2.</span></div>`;
}
/* ── timeline assembly ─────────────────────────────────────────── */
const TL = [];
const KIND_TITLE = {
deleted: "Deletion named in the rule", cpt_panel: "CPT Editorial Panel action named in the rule", telehealth_list: "Medicare telehealth list paragraph",
cpt_deleted: "Absent from the CPT book", cpt_changed: "CPT change noted in the CPT book", appeared: "First year in the RVU file", disappeared: "No longer in the RVU file",
descriptor_change: "RVU-file short descriptor changed", revalued: "Non-facility total RVUs changed by more than 10% from the prior year", status_change: "Payment status changed", created: "Created", crosswalk: "Crosswalk",
};
const ORIGIN_TITLE = { statute: "Statute", regulation: "Regulation", coverage: "Manual and coverage", cpt_panel: "CPT Editorial Panel" };
function buildTimeline() {
// links
D.links.forEach((l) => {
const kind = l.kind === "prolonged-add-on" ? "prolonged add-on code" : l.kind === "add-on" ? "add-on code" : "modifier";
const t = `${l.target} linked as ${kind}` + (l.aliases.length ? ` (proposed as ${l.aliases.join(", ")})` : "");
TL.push({ cy: l.since, type: "link", title: t, codes: [l.target], cites: [l.alias_cite, l.cite].filter(Boolean) });
});
// lineage: FR rows merged per paragraph; RVU notes grouped per year and kind
const fr = {}, rvu = {};
D.events.forEach((e) => {
if (e.source === "fr") {
const k = e.year + "|" + e.cite.label;
(fr[k] = fr[k] || { cy: e.year, kinds: [], codes: [], cite: e.cite }).kinds.push(e.kind);
fr[k].codes.push(...e.codes);
} else if (e.source === "rvu") {
const k = e.year + "|" + e.kind;
(rvu[k] = rvu[k] || { cy: e.year, kind: e.kind, rows: [], rvu: e.rvu }).rows.push(e);
} else {
TL.push({ cy: e.year, type: "lineage", title: KIND_TITLE[e.kind] || e.kind, sum: e.note ? `Book note: “${e.note}”` : "", codes: e.codes, book: e.book });
}
});
Object.values(fr).forEach((g) => TL.push({ cy: g.cy, type: "lineage", title: uniq(g.kinds).map((k) => KIND_TITLE[k] || k).join("; "), codes: uniq(g.codes), cites: [g.cite] }));
Object.values(rvu).forEach((g) => {
const codes = uniq(g.rows.flatMap((r) => r.codes));
const notes = g.rows.filter((r) => r.note).map((r) => `${r.codes.join(", ")}: ${r.note}`);
TL.push({ cy: g.cy, type: "lineage", title: KIND_TITLE[g.kind] || g.kind, sum: notes.join(" · "), codes, rvu: g.rvu });
});
// origins
D.origins.forEach((o) => {
const s = o.cite ? srcOf(o.cite.k) : null;
const cy = o.year || (s ? s.cy : null);
if (!cy) return;
const sums = o.summaries.map((x) => x.text).filter(Boolean);
TL.push({ cy, type: "origin", title: `${ORIGIN_TITLE[o.kind] || o.kind}: ${o.ref}`, undated: !o.year, sum: sums.join(" · "), model: sums.length > 0, codes: o.codes, cites: [o.cite, o.ref_cite].filter(Boolean), book: o.book, target: o.target, extractors: o.extractors });
});
// valuation rounds
const rg = {};
D.rounds.forEach((r) => { const k = r.cy + "|" + r.kind; (rg[k] = rg[k] || { cy: r.cy, kind: r.kind, rows: [] }).rows.push(r); });
Object.values(rg).forEach((g) => {
const dec = {};
g.rows.forEach((r) => { dec[r.decision] = (dec[r.decision] || 0) + 1; });
const pcts = {};
g.rows.forEach((r) => { if (r.pct) pcts[r.pct] = (pcts[r.pct] || 0) + 1; });
const ptxt = Object.keys(pcts).length ? "; RUC survey percentile " + Object.entries(pcts).map(([p, n]) => `${p} ×${n}`).join(", ") : "";
const cites = uniq(g.rows.map((r) => r.cite && r.cite.label)).map((l) => g.rows.find((r) => r.cite && r.cite.label === l).cite);
TL.push({ cy: g.cy, type: "valuation", title: `Work RVU round, ${ruleName(g.rows[0].key)}`, sum: `${g.rows.length} codes; CMS decision ${Object.entries(dec).map(([d, n]) => `“${d}” ×${n}`).join(", ")}${ptxt}.`, codes: g.rows.map((r) => r.code), cites, round: g });
});
// CMS summaries
D.summaries.forEach((s) => {
const q = firstSentence(s.ctext);
TL.push({ cy: s.cy, type: "cms", title: `CMS's summary: ${quant(s.q)}, stance “${s.stance}”; CMS ${ACTION[s.action] || s.action}`, sum: q ? "“" + (q.length > 220 ? q.slice(0, 220).replace(/\s+\S*$/, "") + " …" : q) + "”" : "", codes: s.codes, cites: [s.comment, s.response], cms: s });
});
const ORDER = { origin: 0, link: 1, lineage: 2, valuation: 3, cms: 4 };
TL.sort((a, b) => a.cy - b.cy || ORDER[a.type] - ORDER[b.type]);
TL.forEach((t) => {
const fields = [["Year", "CY" + t.cy + (t.undated ? " (year of the citing rule)" : "")], ["Codes", esc(compact(t.codes || [])) || "—"]];
if (t.sum) fields.push([t.model ? "Model summary" : "Detail", esc(t.sum)]);
if (t.extractors) fields.push(["Extracted by", esc(t.extractors.join(", "))]);
const extra = [];
if (t.book) extra.push(bookCite(t.book) + `<div class="small muted">Licensed book on file. Cited by edition and page; text not reproduced.</div>`);
if (t.rvu) extra.push(rvuCite(t.rvu) + `<div class="small muted">${esc(t.rvu.label)}, published with the ${esc(t.rvu.cite)} final rule.</div>`);
if (t.target) extra.push(linkA(t.target.url, t.target.label + " on eCFR"));
t.ev = ev({ kind: t.type === "cms" ? "CMS's summary" : t.type, title: t.title, fields, cites: t.cites, extra, badges: t.cms ? `<span class="pill cms">CMS's summary</span><span class="pill model">read by local model</span>` : "" });
});
}
/* ── #timeline ─────────────────────────────────────────────────── */
const TL_TYPES = [["all", "All"], ["lineage", "Lineage"], ["origin", "Origin"], ["valuation", "Valuation"], ["cms", "CMS's summaries"], ["link", "Links"]];
const TYPE_NAME = { lineage: "Lineage", origin: "Origin", valuation: "Valuation", cms: "CMS's summary", link: "Link" };
let tlType = "all", tlCode = "";
function renderTimeline() {
const host = $('[data-view="timeline"]');
if (!host.dataset.init) {
const counts = {};
TL.forEach((t) => { counts[t.type] = (counts[t.type] || 0) + 1; });
host.innerHTML = `<h2>Timeline</h2><p class="lede">Dated entries from the family's lineage, origin, valuation rounds and CMS's own comment summaries. Select an entry to read its source.</p>
<div class="filters" id="tl-f">${TL_TYPES.map(([k, t]) => `<button class="fbtn" type="button" data-k="${k}" aria-pressed="${k === tlType}">${t} <span class="muted">${k === "all" ? TL.length : counts[k] || 0}</span></button>`).join("")}
<label class="eyebrow" for="tl-code" style="margin-left:8px">Code</label>
<select class="fsel" id="tl-code"><option value="">All codes</option>${[...BASE, ...PRED, ...LINKED, "G2211", "B1", "B2"].map((c) => `<option>${c}</option>`).join("")}</select></div>
<div id="tl-list"></div>`;
host.dataset.init = "1";
$("#tl-f").addEventListener("click", (e) => { const b = e.target.closest("[data-k]"); if (!b) return; tlType = b.dataset.k; $$("#tl-f [data-k]").forEach((x) => x.setAttribute("aria-pressed", x.dataset.k === tlType)); renderTimelineList(); });
$("#tl-code").addEventListener("change", (e) => { tlCode = e.target.value; renderTimelineList(); });
}
renderTimelineList();
}
function renderTimelineList() {
const items = TL.filter((t) => (tlType === "all" || t.type === tlType) && (!tlCode || (t.codes || []).includes(tlCode)));
const years = uniq(items.map((t) => t.cy));
$("#tl-list").innerHTML = years.length ? years.map((y) => {
const its = items.filter((t) => t.cy === y);
const rule = ruleSources.filter((s) => s.cy === y).map((s) => s.kind).join(" + ");
return `<div class="tl-year"><h4>CY${y}<small>${its.length} ${its.length === 1 ? "entry" : "entries"}${rule ? " · " + esc(rule) + " rule" : ""}</small></h4><div class="tl-items">${its.map((t) => `
<div class="tl-item" ${evAttr(t.ev)}><div class="top"><span class="type ${t.type}">${TYPE_NAME[t.type]}</span><span class="ttl">${esc(t.title)}</span></div>
${t.sum ? `<div class="sum">${t.model ? '<span class="pill model">model summary</span> ' : ""}${esc(t.sum)}</div>` : ""}
<div class="top small"><span class="mono muted">${esc(compact(t.codes || []))}</span>${(t.cites || []).map(citeA).join("")}${t.book ? bookCite(t.book) : ""}${t.rvu ? rvuCite(t.rvu) : ""}${t.target ? `<a class="cite" href="${esc(t.target.url)}" target="_blank" rel="noopener">${esc(t.target.label)}</a>` : ""}</div></div>`).join("")}</div></div>`;
}).join("") : `<p class="muted">No entries match.</p>`;
}
/* ── #valuation ────────────────────────────────────────────────── */
function step(n, head, quote, cites, id) {
return `<li ${evAttr(id)}><span class="n">${n}</span><span class="h">${head}</span>${quote ? `<q>${esc(quote)}</q>` : ""}<span>${cites.filter(Boolean).map(citeA).join(" ")}</span></li>`;
}
function firstSentence(t, kw) {
if (!t) return "";
let s = t.replace(/^… /, "").replace(/^(Comment|Response):\s*/, "");
if (kw) { const i = s.indexOf(kw); if (i >= 0) s = s.slice(i); }
const m = s.match(/^(.+?\.)(\s|$)/);
const out = m ? m[1] : s;
return out.length > 300 ? out.slice(0, 300).replace(/\s+\S*$/, "") + " …" : out;
}
function dotPlot() {
const codes = uniq([...BASE, ...PRED].filter((c) => D.rounds.some((r) => r.code === c)));
const W = 660, rowH = 24, top = 46, left = 64, right = 24, H = top + codes.length * rowH + 30;
const xmax = 4.5, X = (v) => left + (v / xmax) * (W - left - right);
let s = `<svg class="chart" viewBox="0 0 ${W} ${H}" role="img" aria-label="Work RVUs by code: CY2008 RUC recommendation and final, CY2023 RUC recommendation and final">`;
for (let v = 0; v <= 4; v++) s += `<line class="grid" x1="${X(v)}" x2="${X(v)}" y1="${top - 8}" y2="${H - 26}"/><text x="${X(v)}" y="${H - 10}" text-anchor="middle">${v.toFixed(1)}</text>`;
s += `<text x="${left}" y="${H - 10}" text-anchor="end" dx="-8">work RVU</text>`;
// legend
s += `<g transform="translate(${left},14)"><circle cx="6" cy="0" r="4.5" fill="var(--surface)" stroke="var(--s-ruc)" stroke-width="2"/><text x="16" y="4">CY2008 RUC recommendation</text>
<rect x="200" y="-5" width="10" height="10" fill="var(--s-prop)"/><text x="216" y="4">CY2008 final</text>
<circle cx="316" cy="0" r="5" fill="var(--s-final)"/><text x="326" y="4">CY2023 final (= RUC recommendation)</text></g>`;
codes.forEach((c, i) => {
const y = top + i * rowH + rowH / 2;
const deleted = PRED.includes(c);
s += `<text x="${left - 10}" y="${y + 4}" text-anchor="end"${deleted ? ' style="text-decoration:line-through"' : ""}>${c}</text>`;
s += `<line class="grid" x1="${left}" x2="${W - right}" y1="${y}" y2="${y}" stroke-dasharray="2 4"/>`;
const a = D.rounds.find((r) => r.code === c && r.cy === 2008 && r.kind === "final");
const b = D.rounds.find((r) => r.code === c && r.cy === 2023 && r.kind === "final");
if (a && a.ruc != null && a.fin != null) {
s += `<line x1="${X(a.ruc)}" x2="${X(a.fin)}" y1="${y}" y2="${y}" stroke="var(--s-prop)" stroke-width="2"/>`;
s += `<circle cx="${X(a.ruc)}" cy="${y}" r="4.5" fill="var(--surface)" stroke="var(--s-ruc)" stroke-width="2" data-tip="${c} · CY2008 RUC recommendation ${n2(a.ruc)}"/>`;
s += `<rect x="${X(a.fin) - 5}" y="${y - 5}" width="10" height="10" fill="var(--s-prop)" stroke="var(--surface)" stroke-width="1.5" data-tip="${c} · CY2008 final ${n2(a.fin)} (refined)"/>`;
}
if (b && b.fin != null) s += `<circle cx="${X(b.fin)}" cy="${y}" r="5.5" fill="var(--s-final)" stroke="var(--surface)" stroke-width="2" data-tip="${c} · CY2023 final ${n2(b.fin)} · RUC ${n2(b.ruc)} (${b.pct} percentile)"/>`;
});
return s + `</svg>`;
}
function roundTable(cy) {
const codes = uniq(D.rounds.filter((r) => r.cy === cy).map((r) => r.code)).sort();
const rowsP = D.rounds.filter((r) => r.cy === cy && r.kind === "proposed");
const rowsF = D.rounds.filter((r) => r.cy === cy && r.kind === "final");
let h = `<div class="tbl-wrap"><table class="tbl"><thead><tr><th>Code</th><th class="r">RUC rec.</th><th>Survey pct.</th>${cy === 2008 ? '<th class="r">Requested</th>' : '<th class="r">Survey total min</th>'}<th class="r pcol">CMS proposed</th><th class="pcol">Decision</th><th class="r">CMS final</th><th>Decision</th><th>Sources</th></tr></thead><tbody>`;
codes.forEach((c) => {
const p = rowsP.find((r) => r.code === c) || {}, f = rowsF.find((r) => r.code === c) || {};
const del = PRED.includes(c);
const cites = uniq([p.cite, f.cite, ...(f.cites ? Object.values(f.cites).flat() : []), ...(p.cites ? Object.values(p.cites).flat() : [])].filter(Boolean).map((x) => x.label)).map((l) => [p.cite, f.cite, ...(f.cites ? Object.values(f.cites).flat() : []), ...(p.cites ? Object.values(p.cites).flat() : [])].find((x) => x && x.label === l));
const id = ev({ kind: "valuation round", title: `${c}, CY${cy} work RVU round`, badges: `<span class="pill prop">proposed</span><span class="pill final">final</span>`,
fields: [["RUC recommendation", n2(f.ruc != null ? f.ruc : p.ruc)], ["Survey percentile", esc(f.pct || p.pct || "not stated")], ["Requested", n2(f.req != null ? f.req : p.req)], ["CMS proposed", n2(p.prop != null ? p.prop : f.prop)], ["Proposed decision", esc(p.decision || "—")], ["CMS final", n2(f.fin)], ["Final decision", esc(f.decision || "—")], ["Final rationale", esc(f.quote || "—")], ["Table", esc(f.table || p.table || "—")], ["Extracted by", esc(uniq([p.extractor, f.extractor].filter(Boolean)).join("; "))]],
cites });
const total = p.total != null ? p.total : f.total;
h += `<tr ${evAttr(id)}><td class="mono"${del ? ' style="text-decoration:line-through"' : ""}>${c}</td><td class="r num">${n2(f.ruc != null ? f.ruc : p.ruc)}</td><td class="small">${esc(f.pct || p.pct || "—")}</td>
<td class="r num">${cy === 2008 ? n2(f.req != null ? f.req : p.req) : total != null ? int(total) : "—"}</td>
<td class="r num pcol">${n2(p.prop)}</td><td class="pcol small">${esc(p.decision || "—")}</td><td class="r num">${n2(f.fin)}</td><td class="small">${esc(f.decision || "—")}</td>
<td class="nowrap">${[p.cite, f.cite].filter(Boolean).map(citeA).join(" ")}${cites.length > 2 ? ` <span class="small muted">+${cites.length - 2}</span>` : ""}</td></tr>`;
});
return h + `</tbody></table></div>`;
}
function renderValuation() {
const t1676 = D.paras["PETM225D:1676"] || "", t878 = D.paras["MZ24MX5S:878"] || "", t1685 = D.paras["PETM225D:1685"] || "", t1679 = D.paras["PETM225D:1679"] || "", t1681 = D.paras["PETM225D:1681"] || "";
const t2241 = D.paras["54GTKR5G:2241"] || "", t2242 = D.paras["54GTKR5G:2242"] || "";
const sId = (title, cites) => ev({ kind: "valuation", title, cites });
const p08 = r08p[0] || {};
const cy23 = [
step("1 · RUC", "RUC recommended the survey 25th percentile, the median for 99350", firstSentence(t1676, "The RUC recommended"), [C1676], sId("RUC recommendation, CY2023", [C1676])),
step("2 · CMS proposed", `CMS proposed the RUC values for all ${r23p.length} codes`, firstSentence(t878), [C878], sId("CMS proposed rule, CY2023", [C878, C877])),
step("3 · Commenters", "CMS's summary of the comments", firstSentence(t1679), [C1679, C1681], sId("CMS's summary of comments on the RVUs, CY2023 final rule", [C1679, C1681])),
step("4 · CMS final", "CMS finalized the work RVUs as proposed", firstSentence(t1685), [C1685], sId("CMS final rule, CY2023", [C1685])),
].join("");
const cy08 = [
step("1 · CMS proposed", `CMS proposed the RUC values (“${esc(p08.quote || "")}”)`, "", [C08p], ev({ kind: "valuation", title: "CMS proposed rule, CY2008", fields: [["Table", esc(p08.table || "")], ["Decision", esc(p08.quote || "")]], cites: [C08p] })),
step("2 · Commenters", "CMS's summary of the comments", firstSentence(t2241), [C2241], sId("CMS's summary of comments, CY2008 final rule", [C2241])),
step("3 · CMS final", `CMS refined all ${r08f.length} codes`, firstSentence(t2242), [C2242], sId("CMS final rule, CY2008", [C2242])),
].join("");
$('[data-view="valuation"]').innerHTML = `<h2>Valuation</h2>
<p class="lede">RUC recommendation against CMS's proposed and final work RVUs, per code per round. Two rounds are captured: CY2008 and CY2023. Proposed columns are hatched.</p>
<div class="legend" style="margin-bottom:12px"><span><span class="pill prop">proposed</span> from the proposed rule</span><span><span class="pill final">final</span> from the final rule</span></div>
<h3>CY2023: the 25th-percentile round</h3>
<ol class="steps">${cy23}</ol>
<div class="sec">${roundTable(2023)}</div>
<h3>CY2008: CMS refined the RUC values</h3>
<ol class="steps">${cy08}</ol>
<div class="sec">${roundTable(2008)}</div>
<h3>Work RVUs across the two rounds</h3>
<div class="chart-box">${dotPlot()}</div>
<p class="small muted">CY2008 codes not revalued in CY2023 were deleted for CY2023 (struck through). 99341 and 99342 are not in the CY2008 table.</p>`;
}
/* ── #payment ──────────────────────────────────────────────────── */
let payMetric = "pay_nf", payGroup = "base", payCode = "99350";
const YEARS = uniq(D.payment.map((p) => p.cy)).sort((a, b) => a - b);
const PCOLS = [];
YEARS.forEach((y) => {
if (D.payment.some((p) => p.cy === y && p.proposed)) PCOLS.push([y, true]);
PCOLS.push([y, false]);
});
const fmtM = (v) => (payMetric === "pay_nf" ? money(v) : n2(v));
function payChart(code) {
const rows = D.payment.filter((p) => p.code === code);
const fin = rows.filter((p) => !p.proposed && p.pay_nf != null);
const pro = rows.filter((p) => p.proposed && p.pay_nf != null);
const W = 640, H = 230, L = 58, R = 64, T = 18, B = 30;
const ys = [...fin, ...pro].map((p) => p.pay_nf);
if (!ys.length) return `<p class="small muted">No RVU-based payment for ${code} (status ${esc((rows[0] || {}).status || "")}).</p>`;
const x0 = YEARS[0], x1 = YEARS[YEARS.length - 1];
const step = ys.length ? Math.max(...ys) > 150 ? 50 : Math.max(...ys) > 60 ? 25 : 10 : 10;
const ymax = Math.ceil((Math.max(...ys) * 1.08) / step) * step;
const X = (y) => L + ((y - x0) / (x1 - x0)) * (W - L - R), Y = (v) => T + (1 - v / ymax) * (H - T - B);
let s = `<svg class="chart" viewBox="0 0 ${W} ${H}" role="img" aria-label="National non-facility payment for ${code}, final and proposed">`;
for (let v = 0; v <= ymax + 1e-9; v += step) s += `<line class="grid" x1="${L}" x2="${W - R}" y1="${Y(v)}" y2="${Y(v)}"/><text x="${L - 8}" y="${Y(v) + 4}" text-anchor="end">$${v}</text>`;
YEARS.forEach((y) => { if ((y - x0) % 2 === 0 || y === x1) s += `<text x="${X(y)}" y="${H - 10}" text-anchor="middle">${y}</text>`; });
s += `<line class="ax" x1="${L}" x2="${W - R}" y1="${Y(0)}" y2="${Y(0)}"/>`;
// final line segments (break where a year is missing)
let path = "", prev = null;
fin.forEach((p) => { path += (prev != null && p.cy === prev + 1 ? "L" : "M") + X(p.cy).toFixed(1) + " " + Y(p.pay_nf).toFixed(1); prev = p.cy; });
s += `<path d="${path}" fill="none" stroke="var(--s-final)" stroke-width="2"/>`;
fin.forEach((p) => { s += `<circle cx="${X(p.cy)}" cy="${Y(p.pay_nf)}" r="4" fill="var(--s-final)" stroke="var(--surface)" stroke-width="2" data-tip="${code} · CY${p.cy} final · ${money(p.pay_nf)} · CF $${p.cf.toFixed(4)}"/>`; });
pro.forEach((p) => { s += `<circle cx="${X(p.cy)}" cy="${Y(p.pay_nf)}" r="6.5" fill="none" stroke="var(--s-prop)" stroke-width="2" data-tip="${code} · CY${p.cy} proposed · ${money(p.pay_nf)} · CF $${p.cf.toFixed(4)}"/>`; });
const last = fin[fin.length - 1];
if (last) s += `<text x="${X(last.cy) + 10}" y="${Y(last.pay_nf) + 4}" style="fill:var(--fg)">${money(last.pay_nf)}</text>`;
return s + `</svg><div class="legend"><span><span class="sw" style="background:var(--s-final)"></span>Final (Addendum B × CF)</span><span><span class="sw" style="border:2px solid var(--s-prop);border-radius:50%"></span>Proposed (proposed-rule Addendum B × proposed CF)</span></div>`;
}
function volChart(code) {
const rows = D.util.filter((u) => u.code === code);
if (!rows.length) return `<p class="small muted">No national utilization rows for ${code}.</p>`;
const yrs = []; for (let y = M.util_years[0]; y <= M.util_years[1]; y++) yrs.push(y);
const W = 640, H = 150, L = 58, R = 64, T = 12, B = 26;
const vmax = Math.max(...rows.map((r) => r.services));
const mag = Math.pow(10, Math.floor(Math.log10(vmax)));
const step = vmax / mag > 5 ? mag * 2 : vmax / mag > 2 ? mag : mag / 2;
const ymax = Math.ceil(vmax / step) * step;
const bw = (W - L - R) / yrs.length;
const X = (y) => L + (y - yrs[0]) * bw, Y = (v) => T + (1 - v / ymax) * (H - T - B);
let s = `<svg class="chart" viewBox="0 0 ${W} ${H}" role="img" aria-label="Medicare allowed services for ${code} by year">`;
for (let v = 0; v <= ymax + 1e-9; v += step) s += `<line class="grid" x1="${L}" x2="${W - R}" y1="${Y(v)}" y2="${Y(v)}"/><text x="${L - 8}" y="${Y(v) + 4}" text-anchor="end">${v >= 1e6 ? (v / 1e6).toFixed(1) + "M" : v >= 1000 ? Math.round(v / 1000) + "k" : v}</text>`;
yrs.forEach((y) => {
const r = rows.find((x) => x.cy === y);
if (r) { const h = Y(0) - Y(r.services); s += `<path d="M${X(y) + 3} ${Y(0)} V${Y(r.services) + Math.min(4, h)} q0 -4 4 -4 H${X(y) + bw - 7} q4 0 4 4 V${Y(0)} Z" fill="var(--s-final)" data-tip="${code} · ${y} · ${int(r.services)} services"/>`; }
if ((y - yrs[0]) % 2 === 0 || y === yrs[yrs.length - 1]) s += `<text x="${X(y) + bw / 2}" y="${H - 8}" text-anchor="middle">${y}</text>`;
});
return s + `<line class="ax" x1="${L}" x2="${W - R}" y1="${Y(0)}" y2="${Y(0)}"/></svg>`;
}
function renderPayment() {
const host = $('[data-view="payment"]');
if (!host.dataset.init) {
host.innerHTML = `<h2>Payment</h2>
<p class="lede">National unadjusted non-facility payment = non-facility total RVUs × conversion factor, for every year on file. Final values come from each final rule's RVU file; proposed values from the proposed rule's file. They sit in separate columns and are never combined.</p>
<div class="filters" id="pay-f">
<span class="eyebrow">Show</span>
<button class="fbtn" type="button" data-m="pay_nf">Payment $</button><button class="fbtn" type="button" data-m="work">Work RVU</button><button class="fbtn" type="button" data-m="total_nf">Total RVU (non-facility)</button>
<span class="eyebrow" style="margin-left:10px">Codes</span>
<button class="fbtn" type="button" data-g="base">Active (8)</button><button class="fbtn" type="button" data-g="pred">Deleted CY2023 (12)</button><button class="fbtn" type="button" data-g="linked">Linked G0318</button>
</div>
<div class="legend" style="margin-bottom:8px"><span><span class="pill prop">proposed</span> hatched columns</span><span><span class="pill final">final</span> plain columns</span><span class="muted">Select a code to chart it.</span></div>
<div id="pay-tbl"></div>
<div class="charts"><div class="chart-box"><div class="col-h"><h3 id="pay-ch-h"></h3></div><div id="pay-ch"></div></div>
<div class="chart-box"><div class="col-h"><h3 id="vol-ch-h"></h3></div><div id="vol-ch"></div><p class="small muted" id="vol-src"></p></div></div>
<div class="sec" id="cf-notes"></div>`;
host.dataset.init = "1";
$("#pay-f").addEventListener("click", (e) => {
const b = e.target.closest("button"); if (!b) return;
if (b.dataset.m) payMetric = b.dataset.m;
if (b.dataset.g) { payGroup = b.dataset.g; const first = { base: "99350", pred: "99343", linked: "G0318" }[payGroup]; payCode = first; }
renderPayment();
});
}
$$("#pay-f [data-m]").forEach((b) => b.setAttribute("aria-pressed", b.dataset.m === payMetric));
$$("#pay-f [data-g]").forEach((b) => b.setAttribute("aria-pressed", b.dataset.g === payGroup));
const codes = payGroup === "base" ? BASE : payGroup === "pred" ? PRED : LINKED;
let h = `<div class="tbl-wrap" id="pay-scroll"><table class="tbl"><thead><tr><th class="stick">Code</th>${PCOLS.map(([y, p]) => `<th class="r${p ? " pcol" : ""}">CY${y}${p ? "<br>proposed" : "<br>final"}</th>`).join("")}</tr></thead><tbody>`;
codes.forEach((c) => {
const id = ev({ kind: "payment", title: `${c}: RVUs and national payment`, fields: [], html: payEvidence(c) });
h += `<tr ${evAttr(id)} data-code="${c}" class="${c === payCode ? "sel" : ""}"><td class="mono stick">${c}</td>${PCOLS.map(([y, p]) => {
const r = pay(c, y, p);
if (!r) return `<td class="r na${p ? " pcol" : ""}">—</td>`;
const v = r[payMetric];
const nopay = payMetric === "pay_nf" && v == null;
return `<td class="r num${p ? " pcol" : ""}${nopay ? " na" : ""}" title="${esc(nopay ? r.status_note : r.label)}">${nopay ? esc(r.status) : fmtM(v)}</td>`;
}).join("")}</tr>`;
});
h += `<tr><td class="mono stick small">Conv. factor</td>${PCOLS.map(([y, p]) => { const c = cfOf(y, p); return `<td class="r num${p ? " pcol" : ""}" title="${esc(c ? c.cite : "")}">${c ? c.cf.toFixed(2) : "—"}</td>`; }).join("")}</tr>`;
h += `<tr><td class="mono small stick">Source</td>${PCOLS.map(([y, p]) => { const c = cfOf(y, p); return `<td class="r${p ? " pcol" : ""}">${c ? `<a class="cite" href="https://www.federalregister.gov/citation/${c.cite.replace(/ /g, "-")}" target="_blank" rel="noopener">${esc(c.cite)}</a>` : ""}</td>`; }).join("")}</tr>`;
$("#pay-tbl").innerHTML = h + `</tbody></table></div><p class="small muted">Scroll the table sideways for earlier years. A status letter in place of a dollar value means the status is not paid from RVUs × CF (hover for the reason). A dash means the code is not in that year's file.</p>`;
const sc = $("#pay-scroll"); if (sc) sc.scrollLeft = sc.scrollWidth;
$$("#pay-tbl tr[data-code]").forEach((tr) => tr.addEventListener("click", () => { payCode = tr.dataset.code; drawPay(); $$("#pay-tbl tr[data-code]").forEach((x) => x.classList.toggle("sel", x === tr)); }));
drawPay();
const notes = D.cfs.filter((c) => c.note && c.note !== "non-APM standard CF");
$("#cf-notes").innerHTML = `<h3>Conversion factor notes</h3><ul class="gaps">${notes.map((c) => `<li><b>CY${c.cy} ${c.proposed ? "proposed" : "final"}</b> $${c.cf.toFixed(4)}: ${esc(c.note)} <span class="cite book">${esc(c.cite)}</span></li>`).join("")}</ul>`;
}
function payEvidence(c) {
const rows = D.payment.filter((p) => p.code === c);
return `<div class="tbl-wrap"><table class="tbl"><thead><tr><th>Year</th><th>Vintage</th><th>St.</th><th class="r">Work</th><th class="r">PE NF</th><th class="r">MP</th><th class="r">Total NF</th><th class="r">CF</th><th class="r">Pay NF</th></tr></thead><tbody>${rows.map((r) => `<tr><td class="num">${r.cy}</td><td>${r.proposed ? '<span class="pill prop">proposed</span>' : '<span class="pill final">final</span>'}</td><td class="mono">${esc(r.status)}</td><td class="r num">${n2(r.work)}</td><td class="r num">${n2(r.pe_nf)}</td><td class="r num">${n2(r.mp)}</td><td class="r num">${n2(r.total_nf)}</td><td class="r num">${r.cf.toFixed(4)}</td><td class="r num">${money(r.pay_nf)}</td></tr>`).join("")}</tbody></table></div>
<p class="small muted">Each row: ${esc((rows[0] || {}).label || "")} style RVU file of that vintage; final files cite the final rule, proposed files the proposed rule.</p>
<div class="small">${uniq(rows.map((r) => r.cite)).map((ct) => { const r = rows.find((x) => x.cite === ct); return `<a class="cite" href="${esc(r.url)}" target="_blank" rel="noopener">${esc(ct)}</a>`; }).join(" ")}</div>`;
}
function drawPay() {
$("#pay-ch-h").textContent = `${payCode}: national non-facility payment`;
$("#pay-ch").innerHTML = payChart(payCode);
$("#vol-ch-h").textContent = `${payCode}: Medicare allowed services`;
$("#vol-ch").innerHTML = volChart(payCode);
const keys = uniq(D.util.filter((u) => u.code === payCode).map((u) => u.key));
$("#vol-src").innerHTML = keys.length ? `National totals, all places of service. Source: Medicare Physician &amp; Other Practitioners by Geography and Service, ${keys.length} annual files ${keys.slice(-2).map((k) => linkA(srcOf(k).url, srcOf(k).title.replace(/.*(CY\d{4}).*/, "$1"))).join(", ")} …` : "";
}
/* ── #politics ─────────────────────────────────────────────────── */
function stanceBar(counts, total) {
const n = STANCES.reduce((a, s) => a + (counts[s] || 0), 0);
return `<div class="bar" role="img" aria-label="${STANCES.map((s) => `${s} ${counts[s] || 0}`).join(", ")}">${STANCES.filter((s) => counts[s]).map((s) => `<div class="st-${s}" style="flex:${counts[s]};background:var(--st-${s})" data-tip="${s}: ${counts[s]} of ${n}">${counts[s]}</div>`).join("")}</div>`;
}
function evalLine(dr) {
const e = dr && dr.eval;
if (!e) return "Accuracy not yet measured.";
const pos = Math.round(e.recall * e.n_positions);
return `Measured ${e.in_sample ? "on the tuning set" : "on a held-out set"} (${esc(e.docket)}, ${e.n_items} hand-coded comments, ${e.n_positions} positions): the model finds ${Math.round(e.recall * 100)}% of positions (${pos} of ${e.n_positions}); of those it finds, the direction is right ${Math.round(e.direction * 100)}% of the time.`;
}
function renderPolitics() {
const dr = docketRow || {};
const evs = EVENT_ORDER.map((e) => `${M.family}:2023:${e}`).filter((k) => (dr.events || {})[k]);
const ours = evs.map((k) => {
const c = dr.events[k];
const n = STANCES.reduce((a, s) => a + (c[s] || 0), 0);
const id = ev({ kind: "our count", title: `${EVENT_LABEL[evSlug(k)]}: our count`, badges: `<span class="pill prelim">preliminary</span>`, fields: STANCES.map((s) => [s, String(c[s] || 0)]).concat([["Commenters with a position", String(n)], ["Of identified commenters", String(D.commenters.length)]]),
extra: D.stances.filter((s) => s.event === k).slice(0, 40).map((s) => { const cm = D.commenters.find((x) => x.key === s.key) || {}; return `<div><span class="st ${s.stance}">${ST_LETTER[s.stance]}</span> <b>${esc(cm.name || cm.org || cm.reg_org || "Individual")}</b></div><blockquote class="cmt">${esc(s.evidence)}</blockquote>`; }) });
return `<tr ${evAttr(id)}><td>${esc(EVENT_LABEL[evSlug(k)])}</td><td class="r num">${n}</td><td style="min-width:160px">${stanceBar(c)}</td></tr>`;
}).join("");
const cms23 = D.summaries.filter((s) => s.cy === 2023);
const cmsList = cms23.map((s) => {
const id = ev({ kind: "CMS's summary", title: `CMS's summary: ${quant(s.q)}`, badges: `<span class="pill cms">CMS's summary</span><span class="pill model">quantifier and stance read by local model</span>`, fields: [["Quantifier", esc(s.q)], ["Stance", esc(s.stance)], ["CMS response", esc(ACTION[s.action] || s.action)], ["Codes named", esc(compact(s.codes)) || "—"]], cites: [s.comment, s.response] });
const q = firstSentence(s.ctext);
return `<div ${evAttr(id)}><q>${esc(q.length > 170 ? q.slice(0, 170).replace(/\s+\S*$/, "") + " …" : q)}</q><div class="small muted">${esc(quant(s.q))} · stance “${esc(s.stance)}” · CMS ${esc(ACTION[s.action] || s.action)} ${citeA(s.comment)}</div></div>`;
}).join("");
// stakeholder matrix: organization type × specialty, events across
const types = uniq(D.commenters.map((c) => c.type)).sort((a, b) => D.commenters.filter((c) => c.type === b).length - D.commenters.filter((c) => c.type === a).length);
let mx = `<div class="tbl-wrap"><table class="tbl"><thead><tr><th>Commenter</th><th>Name as posted</th><th>Specialty</th>${evs.map((k) => `<th title="${esc(EVENT_LABEL[evSlug(k)])}">${esc(evSlug(k) === "G0318" ? "G0318" : EVENT_LABEL[evSlug(k)].split(" ")[0])}</th>`).join("")}<th>Comment</th></tr></thead><tbody>`;
types.forEach((t) => {
const cs = D.commenters.filter((c) => c.type === t).sort((a, b) => (a.specialty || "~").localeCompare(b.specialty || "~") || (a.org || a.reg_org).localeCompare(b.org || b.reg_org));
mx += `<tr class="grp-row"><td colspan="${evs.length + 4}">${esc(TYPE_LABEL[t] || t)} · ${cs.length}</td></tr>`;
cs.forEach((c) => {
const mine = stancesBy[c.key] || [];
const who = c.org || c.reg_org || "";
const id = ev({ kind: "commenter", title: who || c.name || "Individual commenter", badges: `<span class="pill prelim">stances preliminary</span>`,
fields: [["Name as posted", esc(c.name || "not posted")], ["Organization", esc(c.org || "—")], ["On regulations.gov", esc(c.reg_org || "—")], ["Type", esc(TYPE_LABEL[c.type] || c.type)], ["Specialty", esc(titleCase(c.specialty) || "not stated")], ["regulations.gov category", esc(c.reg_category || "—")], ["Posted", esc(c.posted)], ["Comment", linkA(c.url, c.cid)], ["Identified by", esc(c.extractor)]],
extra: mine.map((s) => `<div><span class="st ${s.stance}">${ST_LETTER[s.stance]}</span> <b>${esc(EVENT_LABEL[evSlug(s.event)] || s.event)}</b>: ${esc(s.stance)}</div><div class="small muted">Model's reason: ${esc(s.reason)}</div><blockquote class="cmt">${esc(s.evidence)}</blockquote>`) });
mx += `<tr ${evAttr(id)}><td>${esc(who || "Individual")}</td><td class="small">${c.name ? esc(c.name) : '<span class="muted">—</span>'}</td><td class="small">${esc(titleCase(c.specialty)) || '<span class="muted">not stated</span>'}</td>${evs.map((k) => { const s = mine.find((x) => x.event === k); return `<td>${s ? `<span class="st ${s.stance}" title="${esc(s.stance)}">${ST_LETTER[s.stance]}</span>` : '<span class="st none">·</span>'}</td>`; }).join("")}<td><a class="cite" href="${esc(c.url)}" target="_blank" rel="noopener">${esc(c.cid.replace("CMS-2022-0113-", "…-"))}</a></td></tr>`;
});
});
mx += `</tbody></table></div>`;
// dockets over time
const dk = D.reaction.filter((r) => r.pk === "docket" && r.n > 0);
const frp = D.reaction.filter((r) => r.pk === "fr-pairs");
const dkt = `<div class="tbl-wrap"><table class="tbl"><thead><tr><th>Docket</th><th>Rule</th><th>Comment period closed</th><th class="r">Commenters naming a family code</th><th class="r">All commenters</th></tr></thead><tbody>${dk.map((r) => `<tr><td>${linkA(r.url, r.period)}</td><td class="mono small">${esc(r.rule || "—")}</td><td class="num">${esc(r.close || "")}</td><td class="r num">${int(r.n)}</td><td class="r num">${int(r.total)}</td></tr>`).join("")}</tbody></table></div>
<p class="small muted">Before regulations.gov text is on file, the count is CMS's own Comment/Response pairs that name a family code: ${frp.map((r) => `CY${r.year} ${r.n} of ${int(r.total)} ${citeA(r.cite)}`).join(" · ")}</p>`;
$('[data-view="politics"]').innerHTML = `<h2>Politics</h2>
<p class="lede">Who commented on the CY2023 rule (docket ${linkA("https://www.regulations.gov/docket/" + M.docket, M.docket)}), by organization type and specialty, with each one's position per event. Our counts and CMS's own summaries sit side by side and are never merged.</p>
<div class="panel warn small" style="margin-bottom:16px"><b>Preliminary.</b> Stances are read by a local model. ${evalLine(dr)} ${D.commenters.length} commenters identified for this family; ${withPos} took a position on at least one event; ${dr.classified != null ? dr.classified : "—"} took a firm one (support, modify or oppose). Names: regulations.gov posts a personal name for ${named}; organizations are shown as posted.</div>
<div class="two">
<div><div class="col-h"><h3>Our count</h3><span class="pill prelim">preliminary</span>${dr.eval ? `<span class="pill">finds ${Math.round(dr.eval.recall * 100)}% · direction ${Math.round(dr.eval.direction * 100)}%</span>` : ""}</div>
<div class="legend" style="margin-bottom:8px">${STANCES.map((s) => `<span><span class="st ${s}">${ST_LETTER[s]}</span>${s}</span>`).join("")}</div>
<div class="tbl-wrap"><table class="tbl"><thead><tr><th>Event, CY2023</th><th class="r">n</th><th>Stance</th></tr></thead><tbody>${ours}</tbody></table></div>
<p class="small muted">n = commenters with a position on that event; each counted once per event.</p></div>
<div><div class="col-h"><h3>CMS's summary</h3><span class="pill cms">CY2023 final rule</span></div>
<div class="cms-list">${cmsList}</div>
<p class="small muted">Quantifier and stance are read from CMS's text by a local model. Earlier years' summaries are on the Timeline.</p></div>
</div>
<h3>Stakeholders: organization type × specialty</h3>
<p class="small muted" style="margin-top:-4px">Select a commenter to read the model's reason and the verbatim quote behind each position. Columns: ${evs.map((k) => esc(EVENT_LABEL[evSlug(k)])).join(" · ")}.</p>
${mx}
<h3>Commenters per docket</h3>${dkt}
<div class="panel soon small" style="margin-top:16px"><b>Coming with S5:</b> expected stance from money and power stakes beside each actual stance, with a mark where they disagree, and the stakes panel.</div>`;
}
/* ── #sources ──────────────────────────────────────────────────── */
function renderSources() {
const uses = {};
const bump = (k) => { if (k) uses[k] = (uses[k] || 0) + 1; };
TL.forEach((t) => (t.cites || []).forEach((c) => bump(c.k)));
D.rounds.forEach((r) => bump(r.key));
D.guidance.forEach((g) => g.src && bump(g.src.k));
const groups = [
["Federal Register rules", (s) => !!s.fr, (s) => `<a class="cite" href="${esc(s.fr.html)}" target="_blank" rel="noopener">${esc(s.cite)}</a> <span class="small muted">FR Doc ${esc(s.fr.doc)} · ${esc(s.date)}</span>`, "Full text on file"],
["Regulations (eCFR)", (s) => s.type === "regulation", (s) => linkA(s.url, "eCFR"), "Link"],
["CMS manuals", (s) => s.type === "manual", (s) => linkA(s.url, "cms.gov PDF"), "Link"],
["Data files", (s) => s.type === "source" && /data\.cms\.gov/.test(s.url), (s) => linkA(s.url, "data.cms.gov"), "On file"],
["Books (licensed)", (s) => s.type === "source" && /openlibrary/.test(s.url), (s) => linkA(s.url, "catalog record"), "On file; paraphrased and cited by page"],
];
const all = Object.values(D.sources);
let h = `<h2>Sources</h2><p class="lede">Every source this dossier cites, with its status. ${all.length + D.commenters.length} in all.</p>`;
groups.forEach(([name, test, link, status]) => {
const ss = all.filter(test).sort((a, b) => (a.date || a.title).localeCompare(b.date || b.title));
if (!ss.length) return;
h += `<h3>${name} <span class="muted small">${ss.length}</span></h3><div class="tbl-wrap"><table class="tbl"><thead><tr><th>Title</th><th>Link</th><th>Status</th><th class="r">Cited</th></tr></thead><tbody>${ss.map((s) => `<tr><td>${s.label ? `<b>${esc(s.label)}</b><div class="small muted">${esc(s.title)}</div>` : esc(s.title)}</td><td style="min-width:150px">${link(s)}</td><td class="small" style="min-width:110px">${esc(status)}</td><td class="r num">${uses[s.key] || ""}</td></tr>`).join("")}</tbody></table></div>`;
});
h += `<h3>Public comments, CY2023 docket <span class="muted small">${D.commenters.length}</span></h3><div class="tbl-wrap"><table class="tbl"><thead><tr><th>Comment</th><th>Commenter</th><th>Posted</th><th>Status</th></tr></thead><tbody>${D.commenters.slice().sort((a, b) => a.cid.localeCompare(b.cid)).map((c) => `<tr><td>${linkA(c.url, c.cid)}</td><td>${esc(c.org || c.reg_org || c.name || "Individual")}${c.name && (c.org || c.reg_org) ? ` <span class="muted small">(${esc(c.name)})</span>` : ""}</td><td class="num">${esc(c.posted)}</td><td class="small">${c.mirror_ok ? "Record on file" : "Record not read"}</td></tr>`).join("")}</tbody></table></div>
<div class="filters" style="margin-top:14px"><button class="dis" disabled>Add source</button><span class="small muted">Paste a URL, DOI, FR citation, docket or comment id. Comes with S2.</span></div>`;
$('[data-view="sources"]').innerHTML = h;
}
/* ── coming views ──────────────────────────────────────────────── */
function soonHead(title, slice, does) {
return `<div class="col-h"><h2>${title}</h2><span class="pill soon">Coming · ${slice}</span></div><p class="lede">${does}</p>`;
}
function renderGrid() {
const codes = ["99341", "99343", "99347"], yrs = [2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027];
const vars = [["Payable status", (r) => (r ? r.status : "absent")], ["Work RVU", (r) => (r ? n2(r.work) : "—")]];
let h = soonHead("Surveillance grid", "S3", "Legal variables down, years across, per code. Each cell holds the coded value and its citation; changed cells are marked; selecting a cell opens the cited paragraph. The analyst codes or corrects a cell, and exports CSV with the codebook.");
h += `<div class="sketch"><div class="tbl-wrap"><table class="tbl"><thead><tr><th>Variable</th><th>Code</th>${yrs.map((y) => `<th class="r">CY${y}</th>`).join("")}</tr></thead><tbody>`;
vars.forEach(([v, f]) => {
codes.forEach((c, i) => {
let prev = null;
h += `<tr>${i === 0 ? `<td rowspan="${codes.length}"><b>${v}</b></td>` : ""}<td class="mono">${c}</td>${yrs.map((y) => { const r = pay(c, y, false); const val = f(r); const chg = prev != null && val !== prev; prev = val; return `<td class="r num${chg ? " chg" : ""}" title="${r ? esc(r.label + " · " + r.cite) : "not in the RVU file"}">${esc(val)}</td>`; }).join("")}</tr>`;
});
});
h += `</tbody></table></div><p class="small muted"><span class="chg" style="padding-left:12px"></span> marks a value that changed from the prior year. Values from each year's final RVU file; “absent” means the code is not in that file. The full codebook adds telehealth eligibility, supervision, place of service, frequency limits and the rest.</p></div>`;
$('[data-view="grid"]').innerHTML = h;
}
function renderEvaluate() {
const yrs = []; for (let y = M.util_years[0]; y <= M.util_years[1]; y++) yrs.push(y);
const sum = (codes, y) => D.util.filter((u) => u.cy === y && codes.includes(u.code)).reduce((a, u) => a + u.services, 0);
const data = yrs.map((y) => ({ y, a: sum(BASE, y), b: sum(PRED, y) }));
const W = 640, H = 230, L = 62, R = 20, T = 30, B = 26;
const vmax = Math.max(...data.map((d) => d.a + d.b));
const mag = Math.pow(10, Math.floor(Math.log10(vmax)));
const step = vmax / mag > 5 ? mag * 2 : vmax / mag > 2 ? mag : mag / 2, ymax = Math.ceil(vmax / step) * step;
const bw = (W - L - R) / yrs.length, X = (y) => L + (y - yrs[0]) * bw, Y = (v) => T + (1 - v / ymax) * (H - T - B);
let s = `<svg class="chart" viewBox="0 0 ${W} ${H}" role="img" aria-label="Allowed services: active home codes and deleted domiciliary codes by year">`;
for (let v = 0; v <= ymax; v += step) s += `<line class="grid" x1="${L}" x2="${W - R}" y1="${Y(v)}" y2="${Y(v)}"/><text x="${L - 8}" y="${Y(v) + 4}" text-anchor="end">${(v / 1e6).toFixed(1)}M</text>`;
s += `<g transform="translate(${L},12)"><rect width="10" height="10" y="-6" fill="var(--s-final)"/><text x="16" y="3">Active home codes (8)</text><rect x="190" width="10" height="10" y="-6" fill="var(--s-alt)"/><text x="206" y="3">Deleted for CY2023 (12)</text></g>`;
data.forEach((d) => {
const x = X(d.y) + 4, w = bw - 8;
if (d.a) s += `<rect x="${x}" y="${Y(d.a)}" width="${w}" height="${Y(0) - Y(d.a)}" fill="var(--s-final)" data-tip="${d.y} · ${compact(BASE)} · ${int(d.a)} services"/>`;
if (d.b) s += `<rect x="${x}" y="${Y(d.a + d.b)}" width="${w}" height="${Math.max(0, Y(d.a) - Y(d.a + d.b) - 2)}" fill="var(--s-alt)" data-tip="${d.y} · deleted codes · ${int(d.b)} services"/>`;
if ((d.y - yrs[0]) % 2 === 1 || d.y === yrs[yrs.length - 1]) s += `<text x="${X(d.y) + bw / 2}" y="${H - 8}" text-anchor="middle">${d.y}</text>`;
});
const xe = X(2023);
s += `<line x1="${xe}" x2="${xe}" y1="${T - 4}" y2="${Y(0)}" stroke="var(--fg)" stroke-width="1.5" stroke-dasharray="4 3"/><text x="${xe - 6}" y="${T + 8}" text-anchor="end" style="fill:var(--fg)">exposure: CY2023</text>`;
s += `<line class="ax" x1="${L}" x2="${W - R}" y1="${Y(0)}" y2="${Y(0)}"/></svg>`;
const exps = [
["CY2023 · domiciliary codes deleted and merged into the home codes", C1672],
["CY2023 · work RVUs revalued at the RUC recommendation", C1685],
["CY2021 · telehealth list paragraph naming 99334, 99335, 99347, 99348", (D.events.find((e) => e.year === 2021 && e.kind === "telehealth_list") || {}).cite],
["CY2027 · telehealth list paragraph naming the eight home codes", (D.events.find((e) => e.year === 2027 && e.kind === "telehealth_list" && e.cite && e.cite.k === "JCWARTJD") || {}).cite],
];
$('[data-view="evaluate"]').innerHTML = soonHead("Evaluate", "S4", "Pick an exposure from the calendar and an outcome series. The event study runs with control codes and shows the effect with its interval and its caveats: censoring and missing post-period years. A result can be saved as a finding.") +
`<div class="sketch"><div><div><h3 style="margin-top:0">Exposure</h3><ul class="facts">${exps.map(([t, c], i) => `<li><label><input type="radio" name="exp" ${i === 0 ? "checked" : ""} disabled> ${esc(t)}</label> ${citeA(c)}</li>`).join("")}</ul>
<h3>Outcome</h3><ul class="facts"><li><label><input type="radio" checked disabled> Medicare allowed services, national</label></li><li class="muted"><label><input type="radio" disabled> Payment and spending</label></li><li class="muted"><label><input type="radio" disabled> Workforce</label></li></ul></div>
<h3>Outcome series</h3><div class="chart-box">${s}<p class="small muted">Allowed services, national, all places of service. The outcome series only. The effect, its interval and the caveats are computed in S4; nothing here is an estimate.</p></div></div></div>`;
}
function renderPredict() {
const actions = {};
D.summaries.forEach((s) => { actions[s.action] = (actions[s.action] || 0) + 1; });
$('[data-view="predict"]').innerHTML = soonHead("Predict", "S5", "For each pending proposal that touches the family: base rates, expected stances, and this family's record of past predictions, scored. A prediction is stored with its evidence before the final rule and scored when the final rule lands.") +
`<div class="sketch"><h3 style="margin-top:0">Base rates from this family's rows</h3>
<div class="tbl-wrap"><table class="tbl"><thead><tr><th>Rate</th><th>Value</th><th>Source</th></tr></thead><tbody>
<tr><td>CMS final equals the RUC recommendation, CY2023 round</td><td class="num">${acceptN(r23f)} of ${r23f.length} codes</td><td>${citeA(C1685)}</td></tr>
<tr><td>CMS final equals the RUC recommendation, CY2008 round</td><td class="num">${acceptN(r08f)} of ${r08f.length} codes</td><td>${citeA(C2242)}</td></tr>
<tr><td>CMS response to family comment pairs, CY${Math.min(...D.summaries.map((s) => s.cy))}–CY${Math.max(...D.summaries.map((s) => s.cy))}</td><td class="small">${Object.entries(actions).sort((a, b) => b[1] - a[1]).map(([a, n]) => `${esc(ACTION[a] || a)} ${n}`).join(" · ")}</td><td class="small muted">CMS's summaries, read by local model</td></tr>
</tbody></table></div>
<h3>Prediction record</h3><div class="tbl-wrap"><table class="tbl"><thead><tr><th>Proposal</th><th>Prediction</th><th>Recorded</th><th>Outcome</th><th class="r">Brier</th></tr></thead><tbody><tr><td colspan="5" class="muted">No predictions recorded. The first can be made against the CY2028 proposed rule.</td></tr></tbody></table></div>
<div class="filters" style="margin-top:12px"><button class="dis" disabled>Record a prediction</button></div></div>`;
}
function renderDraft() {
const r08 = D.rounds.find((r) => r.code === "99344" && r.cy === 2008 && r.kind === "final");
const r23 = D.rounds.find((r) => r.code === "99344" && r.cy === 2023 && r.kind === "final");
const gu = D.util.filter((u) => u.code === "G0318");
const g = (dr) => (dr.events || {})[`${M.family}:2023:G0318`] || {};
const gc = g(docketRow || {});
$('[data-view="draft"]').innerHTML = soonHead("Synthesize and draft", "S6", "Findings link claims to quantitative and qualitative evidence in a joint display. Positions build on findings; a draft for an open docket starts from them, and the local model drafts only from cited rows. The app never submits a comment.") +
`<div class="sketch"><h3 style="margin-top:0">Joint display</h3><div class="tbl-wrap"><table class="tbl"><thead><tr><th>Claim</th><th>Numbers</th><th>Coded text</th></tr></thead><tbody>
<tr><td><div class="ph">The analyst writes the claim</div></td><td class="small">99344 work RVU ${n2(r08 && r08.fin)} CY2008 final ${citeA(C2242)} → ${n2(r23 && r23.fin)} CY2023 final ${citeA(C1685)}</td><td class="small"><span class="pill cms">CMS's summary</span> “${esc(firstSentence(D.paras["PETM225D:1681"] || ""))}” ${citeA(C1681)}</td></tr>
<tr><td><div class="ph">The analyst writes the claim</div></td><td class="small">G0318 allowed services ${gu.map((u) => `${u.cy}: ${int(u.services)} ${srcOf(u.key) ? `<a class="cite" href="${esc(srcOf(u.key).url)}" target="_blank" rel="noopener" title="${esc(srcOf(u.key).title)}">CMS data ${u.cy}</a>` : ""}`).join(", ")}</td><td class="small"><span class="pill prelim">our count, preliminary</span> CY2023 positions on G0318: ${STANCES.map((s) => `${s} ${gc[s] || 0}`).join(", ")}</td></tr>
</tbody></table></div>
<h3>Draft checks</h3><ul class="facts"><li><span class="pill soon">S6</span> Every number and factual claim carries a citation that passes the citation check.</li><li><span class="pill soon">S6</span> American English.</li><li><span class="pill soon">S6</span> Every source relied on has full text on file.</li></ul>
<p class="small muted">The CY2027 final rule has no comment period; the next PFS venue is the CY2028 proposed rule. ${latestRule.fr ? `<a class="cite" href="${esc(latestRule.fr.html)}" target="_blank" rel="noopener">${esc(latestRule.cite)}</a>` : ""}</p></div>`;
}
function renderReview() {
const ext = (rows, f) => { const m = {}; rows.forEach((r) => { const x = f(r); m[x] = (m[x] || 0) + 1; }); return Object.entries(m).map(([k, n]) => `${esc(k)} ${n}`).join(" · "); };
const flagged = D.review.map((r) => {
const id = ev({ kind: "held for review", title: r.what, fields: [["Why held", esc(r.why)]], cites: [r.cite], extra: r.quote ? [`<blockquote>${esc(r.quote)}</blockquote>`] : [] });
return `<tr ${evAttr(id)}><td>${esc(r.what)}</td><td class="small">${esc(r.why)}</td><td>${citeA(r.cite)}</td><td style="white-space:nowrap"><button class="dis" disabled>Accept</button> <button class="dis" disabled>Correct</button> <button class="dis" disabled>Reject</button></td></tr>`;
}).join("");
const queue = [
["Comment stances", D.stances.length, ext(D.stances, (s) => s.extractor)],
["Commenter identities", D.commenters.length, ext(D.commenters, (c) => c.extractor)],
["Valuation rounds", D.rounds.length, ext(D.rounds, (r) => r.extractor)],
["Origin rows", D.origins.reduce((a, o) => a + o.n_rows, 0), ext(D.origins.flatMap((o) => Array.from({ length: o.n_rows }, () => ({ x: o.extractors.length === 1 ? o.extractors[0] : "mixed" }))), (r) => r.x)],
["CMS's summaries (quantifier, stance)", D.summaries.length, ext(D.summaries, (s) => s.extractor)],
];
$('[data-view="review"]').innerHTML = soonHead("Review queue", "S2", "Model-extracted rows that are low-confidence or not yet reviewed: stances, identities, origin rows, valuation rounds and coded cells. Accept, correct or reject; a correction is its own row and never overwrites the extracted value. The agreement rate per extractor is the double-coding check.") +
`<div class="sketch"><h3 style="margin-top:0">Held while building this preview</h3><div class="tbl-wrap"><table class="tbl"><thead><tr><th>Row</th><th>Why held</th><th>Source</th><th>Action</th></tr></thead><tbody>${flagged}</tbody></table></div>
<h3>Extracted rows behind this dossier</h3><div class="tbl-wrap"><table class="tbl"><thead><tr><th>Kind</th><th class="r">Rows</th><th>Extractor</th><th>Agreement rate</th></tr></thead><tbody>${queue.map(([k, n, x]) => `<tr><td>${k}</td><td class="r num">${n}</td><td class="small mono">${x}</td><td class="small muted">computed in S2</td></tr>`).join("")}</tbody></table></div></div>`;
}
/* ── router ────────────────────────────────────────────────────── */
const ALL = VIEWS.flatMap(([, vs]) => vs.map(([id]) => id));
function route() {
const h = (location.hash || "").replace(/^#/, "");
const view = ALL.includes(h) ? h : "dossier";
$$(".view").forEach((s) => { s.hidden = s.dataset.view !== view; });
$$("#tabs a").forEach((a) => { a.classList.toggle("on", a.dataset.tab === view); if (a.dataset.tab === view) a.setAttribute("aria-current", "page"); else a.removeAttribute("aria-current"); });
renderMast(view);
if (view === "payment") {
const end = () => { const sc = $("#pay-scroll"); if (sc) sc.scrollLeft = sc.scrollWidth; };
end(); requestAnimationFrame(end);
if (document.fonts && document.fonts.ready) document.fonts.ready.then(end);
}
const on = $(`#tabs a[data-tab="${view}"]`);
if (on && on.scrollIntoView && h) on.scrollIntoView({ block: "nearest", inline: "nearest" });
}
buildTimeline();
renderRail();
renderTabs();
renderContents();
renderDossier();
renderTimeline();
renderValuation();
renderPayment();
renderPolitics();
renderSources();
renderGrid();
renderEvaluate();
renderPredict();
renderDraft();
renderReview();
showEvidence(ev({ kind: "key paragraph", title: "The RUC's CY2023 recommendation for the home or residence codes", cites: [C1676] }), null);
if (window.matchMedia("(max-width: 1180px)").matches) { drawer.classList.remove("open"); document.body.classList.remove("drawer-open"); }
window.addEventListener("hashchange", route);
route();
})();
</script>
</body>
</html>