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.
1218 lines
101 KiB
HTML
1218 lines
101 KiB
HTML
<!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 · 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> — 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 — 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 · 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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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 & 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>
|