Merge pull request 'fix: rewrite citation formatters — Bluebook, APA, JAMA' (#308) from feat/csl-citation-format into main
Some checks failed
CI / skinny-install (bcda) (push) Successful in 28s
CI / skinny-install (aco) (push) Successful in 49s
CI / skinny-install (api) (push) Successful in 27s
CI / lint-test (push) Failing after 1m24s
CI / skinny-install (bib) (push) Successful in 33s
CI / skinny-install (bls) (push) Successful in 25s
CI / skinny-install (cli) (push) Successful in 29s
CI / skinny-install (opps) (push) Successful in 35s
CI / skinny-install (pfs) (push) Successful in 30s
CI / skinny-install (rex) (push) Successful in 31s
Infra CI / notebooks (push) Successful in 6s
Infra CI / zotero (push) Successful in 6s
Infra CI / docs (push) Failing after 6s
Infra CI / api (push) Successful in 6s
Infra CI / mc (push) Successful in 9s
CI / skinny-install (ccw) (push) Successful in 34s
CI / skinny-install (cms) (push) Successful in 30s
CI / skinny-install (conf) (push) Successful in 26s
CI / skinny-install (perf) (push) Successful in 28s
Deploy / build-scan-report (push) Failing after 1m53s
Some checks failed
CI / skinny-install (bcda) (push) Successful in 28s
CI / skinny-install (aco) (push) Successful in 49s
CI / skinny-install (api) (push) Successful in 27s
CI / lint-test (push) Failing after 1m24s
CI / skinny-install (bib) (push) Successful in 33s
CI / skinny-install (bls) (push) Successful in 25s
CI / skinny-install (cli) (push) Successful in 29s
CI / skinny-install (opps) (push) Successful in 35s
CI / skinny-install (pfs) (push) Successful in 30s
CI / skinny-install (rex) (push) Successful in 31s
Infra CI / notebooks (push) Successful in 6s
Infra CI / zotero (push) Successful in 6s
Infra CI / docs (push) Failing after 6s
Infra CI / api (push) Successful in 6s
Infra CI / mc (push) Successful in 9s
CI / skinny-install (ccw) (push) Successful in 34s
CI / skinny-install (cms) (push) Successful in 30s
CI / skinny-install (conf) (push) Successful in 26s
CI / skinny-install (perf) (push) Successful in 28s
Deploy / build-scan-report (push) Failing after 1m53s
This commit was merged in pull request #308.
This commit is contained in:
@@ -85,7 +85,7 @@ def stg_beneficiary_xref(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 3.1 "Matching MBIs" (p.14):
|
||||
:pincite:`VTWVB384 §3.1 p.14` — Matching MBIs:
|
||||
|
||||
"A beneficiary's MBI is unique to that beneficiary but may change
|
||||
over time. CCLFs 1 through 9, A, and B, as they are provided each
|
||||
@@ -94,7 +94,7 @@ def stg_beneficiary_xref(
|
||||
can be used each month to cross reference records associated with the
|
||||
current MBI to records associated with previous MBIs."
|
||||
|
||||
CCLF IP Section 5.1.1 "Creation of the Most Recent MBI field" (p.19):
|
||||
:pincite:`VTWVB384 §5.1.1 p.19` — Creation of the Most Recent MBI field:
|
||||
|
||||
"The beneficiary XREF file (CCLF9) contains a complete history of
|
||||
MBIs ever used to identify a beneficiary, for beneficiaries who have
|
||||
@@ -129,7 +129,7 @@ def stg_institutional_header(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 2.2.1 "Part A Claims Header File" (p.8-9):
|
||||
:pincite:`VTWVB384 §2.2.1 p.8-9` — Part A Claims Header File:
|
||||
|
||||
"The Part A Claims Header File (CCLF1) contains header-level claim
|
||||
information for institutional services covered by Part A and some
|
||||
@@ -141,7 +141,7 @@ def stg_institutional_header(
|
||||
MBI resolution: BENE_MBI_ID is joined to CCLF9._stg_beneficiary_xref
|
||||
on PRVS_NUM to resolve to CURRENT_BENE_MBI_ID via COALESCE.
|
||||
|
||||
CCLF IP Section 5.3.1 "Calculating Total Part A and B Expenditures" (p.23):
|
||||
:pincite:`VTWVB384 §5.3.1 p.23` — Calculating Total Part A and B Expenditures:
|
||||
|
||||
"Identify the canceled claims in the Part A Header file. These claims
|
||||
are identified by CLM_ADJSMT_TYPE_CD=1. Change the sign of the
|
||||
@@ -179,7 +179,7 @@ def int_institutional_header_adr(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 4.4 "Debit/Credit Method" (p.19):
|
||||
:pincite:`VTWVB384 §4.4 p.19` — Debit/Credit Method:
|
||||
|
||||
"The Debit/Credit method gives a full account/history of claims
|
||||
processed over time. The variable CLM_ADJSMT_TYPE_CD identifies:
|
||||
@@ -187,12 +187,12 @@ def int_institutional_header_adr(
|
||||
1 = Cancellation Claim
|
||||
2 = Adjustment Claim (adjustment to an original claim)"
|
||||
|
||||
CCLF IP Section 5.1.2 "Natural Key for Part A" (p.20):
|
||||
:pincite:`VTWVB384 §5.1.2 p.20` — Natural Key for Part A:
|
||||
|
||||
Part A natural key: CLM_BLG_PRVDR_OSCAR_NUM, CLM_FROM_DT,
|
||||
CLM_THRU_DT, Most Recent MBI (MR_MBI).
|
||||
|
||||
CCLF IP Section 5.2.1 "Related Claims in Part A Header File" (p.21-22):
|
||||
:pincite:`VTWVB384 §5.2.1 p.21-22` — Related Claims in Part A Header File:
|
||||
|
||||
"This group of claims consists of records that are all related to one
|
||||
another — they all involve a claim for services provided to a
|
||||
@@ -235,7 +235,7 @@ def stg_revenue_center(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 2.2.2 "Part A Claim Revenue Center Detail File" (p.9):
|
||||
:pincite:`VTWVB384 §2.2.2 p.9` — Part A Claim Revenue Center Detail File:
|
||||
|
||||
"The Part A Claims Revenue Center Detail File (CCLF2) contains
|
||||
line-item level detail for each claim from the Part A Claims Header
|
||||
@@ -243,7 +243,7 @@ def stg_revenue_center(
|
||||
service was received, and for outpatient claims the payment amount
|
||||
and allowed charge amount for individual services."
|
||||
|
||||
CCLF IP Section 3.5 "Part A Header vs Revenue Center Expenditures" (p.16):
|
||||
:pincite:`VTWVB384 §3.5 p.16` — Part A Header vs Revenue Center Expenditures:
|
||||
|
||||
"Both the Part A Header file (CCLF1) and the Part A Revenue Center
|
||||
file (CCLF2) contain a payment field (CLM_PMT_AMT and
|
||||
@@ -282,7 +282,7 @@ def int_diagnosis_pivot(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 2.2.4 "Part A Diagnosis Code File" (p.9):
|
||||
:pincite:`VTWVB384 §2.2.4 p.9` — Part A Diagnosis Code File:
|
||||
|
||||
"The Part A Diagnosis Code File (CCLF4) contains the diagnosis code
|
||||
for the principal diagnosis as well as all secondary diagnoses that
|
||||
@@ -336,7 +336,7 @@ def int_procedure_pivot(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 2.2.3 "Part A Procedure Code File" (p.9):
|
||||
:pincite:`VTWVB384 §2.2.3 p.9` — Part A Procedure Code File:
|
||||
|
||||
"The Part A Procedure Code File (CCLF3) contains detailed information
|
||||
regarding claims from the Part A Claims Header File, such as the type
|
||||
@@ -402,7 +402,7 @@ def int_institutional_medical_claim(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 2.2 "Part A Claims Data" (p.8):
|
||||
:pincite:`VTWVB384 §2.2 p.8` — Part A Claims Data:
|
||||
|
||||
"Part A claim data files contain claims submitted by facilities such
|
||||
as hospitals, SNFs, HHAs, rehabilitation facilities, and dialysis
|
||||
@@ -560,7 +560,7 @@ def stg_physician_claim(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 2.2.6 "Part B Physician File" (p.10):
|
||||
:pincite:`VTWVB384 §2.2.6 p.10` — Part B Physician File:
|
||||
|
||||
"The Part B Physician File (CCLF5) consists of claim-line records but
|
||||
includes both claim-level and line-level information. At the claim
|
||||
@@ -570,7 +570,7 @@ def stg_physician_claim(
|
||||
amount, allowed charge amount, line-level diagnosis code, units of
|
||||
service, primary payer, provider TIN, and rendering NPI number."
|
||||
|
||||
CCLF IP Section 5.3.1 (p.23):
|
||||
:pincite:`VTWVB384 §5.3.1 p.23` — Section 5.3.1:
|
||||
|
||||
"Identify all the canceled records (line items) in the Part B
|
||||
Physician file. The canceled line items are identified by
|
||||
@@ -603,16 +603,16 @@ def int_physician_claim_adr(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 5.1.2 "Natural Key for Part B Physician/DME" (p.20):
|
||||
:pincite:`VTWVB384 §5.1.2 p.20` — Natural Key for Part B Physician/DME:
|
||||
|
||||
Part B natural key: CLM_CNTL_NUM, Most Recent MBI (MR_MBI).
|
||||
|
||||
CCLF IP Section 5.2.2 "Related Claims in the Part B Physician File" (p.22):
|
||||
:pincite:`VTWVB384 §5.2.2 p.22` — Related Claims in the Part B Physician File:
|
||||
|
||||
"In the Part B Physician File, you will find original claims,
|
||||
cancellation claims, and adjustment claims."
|
||||
|
||||
CCLF IP Section 3.2 "Dropping Denied Claims" (p.15):
|
||||
:pincite:`VTWVB384 §3.2 p.15` — Dropping Denied Claims:
|
||||
|
||||
"For Part B Physician/DME claims, some individual line-items can be
|
||||
denied, whereas other line-items are not denied. Part B claims need
|
||||
@@ -652,7 +652,7 @@ def int_physician_medical_claim(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 2.2.6 "Part B Physician File" (p.10):
|
||||
:pincite:`VTWVB384 §2.2.6 p.10` — Part B Physician File:
|
||||
|
||||
CCLF5 contains both claim-level and line-level data in a single
|
||||
file. Unlike Part A (which has separate header/line/dx/px files),
|
||||
@@ -763,7 +763,7 @@ def stg_dme_claim(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 2.2.7 "Part B DME File" (p.10):
|
||||
:pincite:`VTWVB384 §2.2.7 p.10` — Part B DME File:
|
||||
|
||||
"The Part B DME File (CCLF6) consists of claim-line records but
|
||||
includes both claim-level and line-level information. Claim-level
|
||||
@@ -773,7 +773,7 @@ def stg_dme_claim(
|
||||
amount, ordering NPI number, and 'paid to' NPI number."
|
||||
|
||||
Cancellation adjustment applied to CLM_LINE_CVRD_PD_AMT and
|
||||
CLM_LINE_ALOWD_CHRG_AMT per IP Section 5.3.1.
|
||||
CLM_LINE_ALOWD_CHRG_AMT per :pincite:`VTWVB384 §5.3.1` —
|
||||
"""
|
||||
xref = cclf___stg_beneficiary_xref
|
||||
return cclf__cclf6.join(
|
||||
@@ -799,7 +799,7 @@ def int_dme_claim_adr(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 5.2.3 "Related Claims in the Part B DME File" (p.23):
|
||||
:pincite:`VTWVB384 §5.2.3 p.23` — Related Claims in the Part B DME File:
|
||||
|
||||
"In the Part B DME File, you will find original claims, cancellation
|
||||
claims, and adjustment claims. A variety of related claims are found
|
||||
@@ -840,7 +840,7 @@ def int_dme_medical_claim(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 2.2.7 "Part B DME File" (p.10):
|
||||
:pincite:`VTWVB384 §2.2.7 p.10` — Part B DME File:
|
||||
|
||||
Key field mappings from CCLF6 (IP Table 19):
|
||||
- CUR_CLM_UNIQ_ID -> claim_id
|
||||
@@ -940,7 +940,7 @@ def medical_claim(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 2.2 "Part A Claims Data" + Section 2.2.6-2.2.7 (p.8-10):
|
||||
:pincite:`VTWVB384 §2.2 p.8-10` — Part A Claims Data:
|
||||
|
||||
The three claim types are combined into a single medical_claim table:
|
||||
|
||||
@@ -954,7 +954,7 @@ def medical_claim(
|
||||
- Professional/DME (CCLF6): Durable Medical Equipment, Prosthetics,
|
||||
Orthotics, and Supplies. claim_type = 'professional'.
|
||||
|
||||
CCLF IP Section 5.4 "Part A vs Part B Claims" (p.27):
|
||||
:pincite:`VTWVB384 §5.4 p.27` — Part A vs Part B Claims:
|
||||
|
||||
"The Part A claims files will include Medicare provider payments for
|
||||
some services covered under both Part A and Part B. To distinguish,
|
||||
@@ -984,7 +984,7 @@ def stg_pharmacy_claim(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 2.3 "Part D Claims Data" (p.10):
|
||||
:pincite:`VTWVB384 §2.3 p.10` — Part D Claims Data:
|
||||
|
||||
"The Part D File (CCLF7) contains prescription drug information at
|
||||
the beneficiary level. Data elements include the NDC, quantity
|
||||
@@ -1015,7 +1015,7 @@ def int_pharmacy_claim_adr(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 3.3 "Part D Data Limitations" (p.15):
|
||||
:pincite:`VTWVB384 §3.3 p.15` — Part D Data Limitations:
|
||||
|
||||
"The Part D claims contained in the CCLF are 'final action' claims,
|
||||
unlike the other claims-related files which are debit/credit claims.
|
||||
@@ -1025,7 +1025,7 @@ def int_pharmacy_claim_adr(
|
||||
for any given set of Related Claims the most recent claim and
|
||||
delete/ignore all of the previous related claims for that event."
|
||||
|
||||
CCLF IP Section 5.2.4 "Related Claims in the Part D File" (p.23):
|
||||
:pincite:`VTWVB384 §5.2.4 p.23` — Related Claims in the Part D File:
|
||||
|
||||
"In the Part D File, you will find original claims, cancellation
|
||||
claims, and adjustment claims."
|
||||
@@ -1057,7 +1057,7 @@ def pharmacy_claim(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 2.3 "Part D Claims Data" (p.10):
|
||||
:pincite:`VTWVB384 §2.3 p.10` — Part D Claims Data:
|
||||
|
||||
Key field mappings from CCLF7 (IP Table 20):
|
||||
- CUR_CLM_UNIQ_ID -> claim_id
|
||||
@@ -1137,14 +1137,14 @@ def stg_beneficiary_demographics(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 2.4.1 "Beneficiary Demographics File" (p.10-12):
|
||||
:pincite:`VTWVB384 §2.4.1 p.10-12` — Beneficiary Demographics File:
|
||||
|
||||
"This file contains the beneficiary's current MBI, first/middle/last
|
||||
name, ZIP code, date of birth, sex, race, age, Medicare Status Code,
|
||||
dual eligibility status, hospice begin/end dates, and date of death
|
||||
if a decedent."
|
||||
|
||||
CCLF IP Section 3.1 (p.14):
|
||||
:pincite:`VTWVB384 §3.1 p.14` — Section 3.1:
|
||||
|
||||
MBI resolution via CCLF9 applied before deduplication to ensure a
|
||||
single row per resolved beneficiary identity.
|
||||
@@ -1181,7 +1181,7 @@ def eligibility(
|
||||
|
||||
CCLF IP Reference
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
CCLF IP Section 2.4.1 "Beneficiary Demographics File" (p.10):
|
||||
:pincite:`VTWVB384 §2.4.1 p.10` — Beneficiary Demographics File:
|
||||
|
||||
Key field mappings from CCLF8 (IP Table 21):
|
||||
- BENE_MBI_ID (resolved) -> person_id, member_id
|
||||
|
||||
@@ -1,33 +1,39 @@
|
||||
"""Citation formatting for government/regulatory item types.
|
||||
"""Citation formatting for bibliography item types.
|
||||
|
||||
Hardcoded formatters for the 5 item types — no CSL dependency needed
|
||||
since these follow predictable patterns.
|
||||
Implements three citation styles:
|
||||
|
||||
Supported styles:
|
||||
- ``bluebook`` — Bluebook Law Review format (primary style for this project)
|
||||
- ``apa`` — APA 7th edition
|
||||
- ``jama`` — JAMA style (journal articles only)
|
||||
|
||||
- ``apa`` — APA 7th edition (default)
|
||||
- ``bluebook`` — Bluebook legal citation (regulations only)
|
||||
Follows the CSL field mapping conventions documented in Tierney (2026),
|
||||
"Automating Bluebook Citations in Legal Scholarship" (Zotero key QPPJ8LXZ).
|
||||
|
||||
Examples::
|
||||
Bluebook examples::
|
||||
|
||||
Rule (APA):
|
||||
Centers for Medicare & Medicaid Services. (2025).
|
||||
CY 2026 PFS Final Rule. *Federal Register*, *90*, 98452.
|
||||
Rule:
|
||||
Medicare and Medicaid Programs; CY 2026 Payment Policies Under
|
||||
the Physician Fee Schedule, 90 Fed. Reg. 101174 (Dec. 31, 2025).
|
||||
|
||||
Manual (APA):
|
||||
Centers for Medicare & Medicaid Services. (n.d.).
|
||||
*Claims Processing Manual* (Pub. 100-04, Ch. 12).
|
||||
|
||||
Regulation (Bluebook):
|
||||
Regulation:
|
||||
42 C.F.R. § 414.22 (2025).
|
||||
|
||||
Download (APA):
|
||||
Centers for Medicare & Medicaid Services. (2026).
|
||||
*RVU26A*. https://...
|
||||
Manual:
|
||||
Ctrs. for Medicare & Medicaid Servs., Claims Processing Manual,
|
||||
CMS Pub. 100-04, Ch. 12 (2025).
|
||||
|
||||
Source (APA):
|
||||
CMS Innovation Center. (2025).
|
||||
*PY 2026 Financial Guarantees...*. https://...
|
||||
Journal article:
|
||||
Porter & Watts, Visual Rulemaking, 91 N.Y.U. L. Rev. 1183 (2016).
|
||||
|
||||
APA examples::
|
||||
|
||||
Rule:
|
||||
Centers for Medicare & Medicaid Services. (2025). CY 2026 PFS
|
||||
Final Rule. *Federal Register*, *90*, 101174. https://...
|
||||
|
||||
Regulation:
|
||||
42 C.F.R. § 414.22 (2025). *Payment for Part B Medical
|
||||
and Other Health Services*. https://ecfr.gov/...
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -44,6 +50,87 @@ if TYPE_CHECKING:
|
||||
Source,
|
||||
)
|
||||
|
||||
# ── Month abbreviations (Bluebook Table T12) ─────────────────────────
|
||||
|
||||
_MONTHS = {
|
||||
1: "Jan.",
|
||||
2: "Feb.",
|
||||
3: "Mar.",
|
||||
4: "Apr.",
|
||||
5: "May",
|
||||
6: "June",
|
||||
7: "July",
|
||||
8: "Aug.",
|
||||
9: "Sept.",
|
||||
10: "Oct.",
|
||||
11: "Nov.",
|
||||
12: "Dec.",
|
||||
}
|
||||
|
||||
|
||||
def _parse_date(date_str: str) -> tuple[str, str, str]:
|
||||
"""Parse ISO date to (year, month_abbr, day). Returns empty strings for missing parts."""
|
||||
if not date_str or len(date_str) < 4:
|
||||
return ("", "", "")
|
||||
year = date_str[:4]
|
||||
month = ""
|
||||
day = ""
|
||||
if len(date_str) >= 7:
|
||||
try:
|
||||
m = int(date_str[5:7])
|
||||
month = _MONTHS.get(m, "")
|
||||
except ValueError:
|
||||
pass
|
||||
if len(date_str) >= 10:
|
||||
try:
|
||||
day = str(int(date_str[8:10]))
|
||||
except ValueError:
|
||||
pass
|
||||
return (year, month, day)
|
||||
|
||||
|
||||
def _year_or_nd(date_str: str) -> str:
|
||||
"""Extract 4-digit year from an ISO date, or 'n.d.'."""
|
||||
year, _, _ = _parse_date(date_str)
|
||||
return year or "n.d."
|
||||
|
||||
|
||||
def _bluebook_date(date_str: str) -> str:
|
||||
"""Format a date as Bluebook parenthetical: (Month Day, Year) or (Year)."""
|
||||
year, month, day = _parse_date(date_str)
|
||||
if not year:
|
||||
return ""
|
||||
if month and day:
|
||||
return f"{month} {day}, {year}"
|
||||
if month:
|
||||
return f"{month} {year}"
|
||||
return year
|
||||
|
||||
|
||||
def _parse_authors(extra: str) -> list[str]:
|
||||
"""Extract author last names from the extra field 'Authors: Last First; ...' line."""
|
||||
for line in extra.split("\n"):
|
||||
if line.startswith("Authors: "):
|
||||
raw = line[9:].strip()
|
||||
authors = []
|
||||
for a in raw.split("; "):
|
||||
parts = a.strip().split()
|
||||
if parts:
|
||||
authors.append(parts[0])
|
||||
return authors
|
||||
return []
|
||||
|
||||
|
||||
def _parse_extra_field(extra: str, field: str) -> str:
|
||||
"""Extract a named field from the extra text (e.g., 'DOI: 10.xxx')."""
|
||||
for line in extra.split("\n"):
|
||||
if line.startswith(f"{field}: "):
|
||||
return line[len(field) + 2 :].strip()
|
||||
return ""
|
||||
|
||||
|
||||
# ── Public API ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def format_citation(item: Item, *, style: str = "apa") -> str:
|
||||
"""Format a single item as a citation string."""
|
||||
@@ -67,17 +154,40 @@ def format_bibliography(items: list[Item], *, style: str = "apa") -> str:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ── Per-type formatters ──────────────────────────────────────────
|
||||
|
||||
|
||||
def _year_or_nd(date_str: str) -> str:
|
||||
"""Extract 4-digit year from an ISO date, or 'n.d.'."""
|
||||
if date_str and len(date_str) >= 4:
|
||||
return date_str[:4]
|
||||
return "n.d."
|
||||
# ── Rule (Federal Register) ──────────────────────────────────────────
|
||||
|
||||
|
||||
def _format_rule(item: Rule, *, style: str = "apa") -> str:
|
||||
if style == "bluebook":
|
||||
return _format_rule_bluebook(item)
|
||||
return _format_rule_apa(item)
|
||||
|
||||
|
||||
def _format_rule_bluebook(item: Rule) -> str:
|
||||
"""Bluebook: Title, Volume Fed. Reg. Page (Date).
|
||||
|
||||
Example: Medicare and Medicaid Programs; CY 2026 Payment Policies
|
||||
Under the Physician Fee Schedule, 90 Fed. Reg. 101174 (Dec. 31, 2025).
|
||||
"""
|
||||
parts = []
|
||||
parts.append(f"{item.title},")
|
||||
|
||||
if item.fr_volume and item.fr_page:
|
||||
parts.append(f"{item.fr_volume} Fed. Reg. {item.fr_page}")
|
||||
elif item.fr_volume:
|
||||
parts.append(f"{item.fr_volume} Fed. Reg. ___")
|
||||
|
||||
date = _bluebook_date(item.date_published)
|
||||
if date:
|
||||
parts.append(f"({date}).")
|
||||
else:
|
||||
parts.append(".")
|
||||
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def _format_rule_apa(item: Rule) -> str:
|
||||
"""APA: Institution. (Year). Title. *Federal Register*, *Volume*, Page. URL"""
|
||||
year = _year_or_nd(item.date_published)
|
||||
parts = [f"Centers for Medicare & Medicaid Services. ({year})."]
|
||||
parts.append(f"{item.title}.")
|
||||
@@ -93,7 +203,37 @@ def _format_rule(item: Rule, *, style: str = "apa") -> str:
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
# ── Manual (CMS IOM) ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _format_manual(item: Manual, *, style: str = "apa") -> str:
|
||||
if style == "bluebook":
|
||||
return _format_manual_bluebook(item)
|
||||
return _format_manual_apa(item)
|
||||
|
||||
|
||||
def _format_manual_bluebook(item: Manual) -> str:
|
||||
"""Bluebook: Ctrs. for Medicare & Medicaid Servs., Manual Name,
|
||||
CMS Pub. Number, Ch. N (Year).
|
||||
"""
|
||||
parts = ["Ctrs. for Medicare & Medicaid Servs.,"]
|
||||
|
||||
name = item.manual_name or item.title
|
||||
parts.append(f"{name},")
|
||||
|
||||
if item.pub_number:
|
||||
parts.append(f"CMS Pub. {item.pub_number},")
|
||||
|
||||
if item.chapter:
|
||||
parts.append(f"Ch. {item.chapter}")
|
||||
|
||||
year = _year_or_nd(item.date_published)
|
||||
parts.append(f"({year}).")
|
||||
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def _format_manual_apa(item: Manual) -> str:
|
||||
year = _year_or_nd(item.date_published)
|
||||
institution = item.institution or "Centers for Medicare & Medicaid Services"
|
||||
parts = [f"{institution}. ({year})."]
|
||||
@@ -114,14 +254,29 @@ def _format_manual(item: Manual, *, style: str = "apa") -> str:
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
# ── Regulation (CFR) ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _format_regulation(item: Regulation, *, style: str = "apa") -> str:
|
||||
if style == "bluebook":
|
||||
return _format_regulation_bluebook(item)
|
||||
return _format_regulation_apa(item)
|
||||
|
||||
|
||||
def _format_regulation_bluebook(item: Regulation) -> str:
|
||||
"""Bluebook: Title C.F.R. § Section (Year).
|
||||
|
||||
Examples:
|
||||
42 C.F.R. § 414.22 (2025).
|
||||
42 C.F.R. pt. 414 (2025).
|
||||
"""
|
||||
year = _year_or_nd(item.effective_date)
|
||||
if item.cfr_section:
|
||||
return f"{item.cfr_title} C.F.R. § {item.cfr_section} ({year})."
|
||||
return f"{item.cfr_title} C.F.R. pt. {item.cfr_part} ({year})."
|
||||
|
||||
# APA
|
||||
|
||||
def _format_regulation_apa(item: Regulation) -> str:
|
||||
year = _year_or_nd(item.effective_date)
|
||||
parts = []
|
||||
if item.cfr_section:
|
||||
@@ -135,6 +290,9 @@ def _format_regulation(item: Regulation, *, style: str = "apa") -> str:
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
# ── Download (CMS data files) ────────────────────────────────────────
|
||||
|
||||
|
||||
def _format_download(item: Download, *, style: str = "apa") -> str:
|
||||
year = _year_or_nd(item.date_published)
|
||||
website = item.website_title or "Centers for Medicare & Medicaid Services"
|
||||
@@ -147,7 +305,93 @@ def _format_download(item: Download, *, style: str = "apa") -> str:
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
# ── Source (generic / journal articles) ───────────────────────────────
|
||||
|
||||
|
||||
def _format_source(item: Source, *, style: str = "apa") -> str:
|
||||
extra = item.extra or ""
|
||||
doc_type = ""
|
||||
try:
|
||||
doc_type = item.doc_type
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if doc_type == "journal-article" or _parse_extra_field(extra, "PMID"):
|
||||
if style == "jama":
|
||||
return _format_journal_jama(item)
|
||||
return _format_journal_apa(item)
|
||||
|
||||
return _format_source_generic(item, style=style)
|
||||
|
||||
|
||||
def _format_journal_apa(item: Source) -> str:
|
||||
"""APA: Author(s). (Year). Title. *Journal*, *Volume*(Issue), Pages. doi:DOI"""
|
||||
extra = item.extra or ""
|
||||
authors = _parse_authors(extra)
|
||||
year = _year_or_nd(item.date_published)
|
||||
|
||||
# Author string
|
||||
if len(authors) >= 3:
|
||||
author_str = f"{authors[0]} et al."
|
||||
elif len(authors) == 2:
|
||||
author_str = f"{authors[0]} & {authors[1]}"
|
||||
elif authors:
|
||||
author_str = authors[0]
|
||||
else:
|
||||
author_str = item.institution or "Unknown"
|
||||
|
||||
parts = [f"{author_str}. ({year})."]
|
||||
parts.append(f"{item.title}.")
|
||||
|
||||
journal = _parse_extra_field(extra, "Journal") or item.institution
|
||||
if journal:
|
||||
parts.append(f"*{journal}*.")
|
||||
|
||||
doi = _parse_extra_field(extra, "DOI")
|
||||
if doi:
|
||||
parts.append(f"https://doi.org/{doi}")
|
||||
elif item.url:
|
||||
parts.append(item.url)
|
||||
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def _format_journal_jama(item: Source) -> str:
|
||||
"""JAMA: Author(s). Title. *Journal*. Year. doi:DOI"""
|
||||
extra = item.extra or ""
|
||||
authors = _parse_authors(extra)
|
||||
year = _year_or_nd(item.date_published)
|
||||
|
||||
if len(authors) >= 3:
|
||||
author_str = f"{authors[0]} et al"
|
||||
elif len(authors) == 2:
|
||||
author_str = f"{authors[0]}, {authors[1]}"
|
||||
elif authors:
|
||||
author_str = authors[0]
|
||||
else:
|
||||
author_str = item.institution or "Unknown"
|
||||
|
||||
parts = [f"{author_str}."]
|
||||
parts.append(f"{item.title}.")
|
||||
|
||||
journal = _parse_extra_field(extra, "Journal") or item.institution
|
||||
if journal:
|
||||
parts.append(f"*{journal}*.")
|
||||
|
||||
parts.append(f"{year}.")
|
||||
|
||||
doi = _parse_extra_field(extra, "DOI")
|
||||
if doi:
|
||||
parts.append(f"doi:{doi}")
|
||||
|
||||
pmid = _parse_extra_field(extra, "PMID")
|
||||
if pmid and not doi:
|
||||
parts.append(f"PMID: {pmid}")
|
||||
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def _format_source_generic(item: Source, *, style: str = "apa") -> str:
|
||||
year = _year_or_nd(item.date_published)
|
||||
institution = item.institution or "Unknown"
|
||||
parts = [f"{institution}. ({year})."]
|
||||
@@ -159,6 +403,9 @@ def _format_source(item: Source, *, style: str = "apa") -> str:
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
# ── Generic fallback ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _format_generic(item: Item, *, style: str = "apa") -> str:
|
||||
year = _year_or_nd(item.date_published)
|
||||
institution = item.institution or "Unknown"
|
||||
|
||||
@@ -48,7 +48,7 @@ if TYPE_CHECKING:
|
||||
# Zotero key charset: 23456789ABCDEFGHIJKLMNPQRSTUVWXYZ (no 0, 1, O, L)
|
||||
_PINCITE_RE = re.compile(
|
||||
r":pincite:`"
|
||||
r"([23456789A-HJ-NP-Z]{8})" # group 1: item key
|
||||
r"([23456789A-HJ-NP-Z]{8})" # group 1: item key (strict Zotero charset)
|
||||
r"(?:\s+(.+?))?" # group 2: locator (optional)
|
||||
r"`"
|
||||
r"(?:\s*(?:--|—)\s*(.+))?" # group 3: note (optional)
|
||||
|
||||
@@ -5,8 +5,27 @@ from __future__ import annotations
|
||||
from bib.format import format_bibliography, format_citation
|
||||
from bib.item import Download, Item, Manual, Regulation, Rule, Source
|
||||
|
||||
# ── Rule ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestFormatRule:
|
||||
def test_bluebook_full(self) -> None:
|
||||
r = Rule(
|
||||
title="Medicare and Medicaid Programs; CY 2026 PFS Final Rule",
|
||||
date_published="2025-12-31",
|
||||
fr_volume="90",
|
||||
fr_page="101174",
|
||||
)
|
||||
cite = format_citation(r, style="bluebook")
|
||||
assert "90 Fed. Reg. 101174" in cite
|
||||
assert "(Dec. 31, 2025)" in cite
|
||||
assert cite.endswith(".")
|
||||
|
||||
def test_bluebook_no_page(self) -> None:
|
||||
r = Rule(title="Test Rule", fr_volume="90", date_published="2025-01-01")
|
||||
cite = format_citation(r, style="bluebook")
|
||||
assert "90 Fed. Reg. ___" in cite
|
||||
|
||||
def test_apa_with_volume_and_page(self) -> None:
|
||||
r = Rule(
|
||||
title="CY 2026 PFS Final Rule",
|
||||
@@ -44,7 +63,25 @@ class TestFormatRule:
|
||||
assert "*90*" in cite
|
||||
|
||||
|
||||
# ── Manual ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestFormatManual:
|
||||
def test_bluebook_full(self) -> None:
|
||||
m = Manual(
|
||||
title="Physician Fee Schedule",
|
||||
manual_name="Claims Processing Manual",
|
||||
pub_number="100-04",
|
||||
chapter="12",
|
||||
date_published="2025-01-01",
|
||||
)
|
||||
cite = format_citation(m, style="bluebook")
|
||||
assert "Ctrs. for Medicare & Medicaid Servs." in cite
|
||||
assert "Claims Processing Manual" in cite
|
||||
assert "CMS Pub. 100-04" in cite
|
||||
assert "Ch. 12" in cite
|
||||
assert "(2025)" in cite
|
||||
|
||||
def test_apa_full(self) -> None:
|
||||
m = Manual(
|
||||
title="Physician Fee Schedule",
|
||||
@@ -68,15 +105,33 @@ class TestFormatManual:
|
||||
assert "https://example.com/manual" in cite
|
||||
|
||||
def test_apa_uses_institution(self) -> None:
|
||||
m = Manual(
|
||||
title="Test",
|
||||
institution="Custom Institution",
|
||||
)
|
||||
m = Manual(title="Test", institution="Custom Institution")
|
||||
cite = format_citation(m)
|
||||
assert "Custom Institution" in cite
|
||||
|
||||
|
||||
# ── Regulation ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestFormatRegulation:
|
||||
def test_bluebook_with_section(self) -> None:
|
||||
r = Regulation(
|
||||
cfr_title="42",
|
||||
cfr_section="414.22",
|
||||
effective_date="2025-01-01",
|
||||
)
|
||||
cite = format_citation(r, style="bluebook")
|
||||
assert cite == "42 C.F.R. § 414.22 (2025)."
|
||||
|
||||
def test_bluebook_part_only(self) -> None:
|
||||
r = Regulation(
|
||||
cfr_title="42",
|
||||
cfr_part="414",
|
||||
effective_date="2025-01-01",
|
||||
)
|
||||
cite = format_citation(r, style="bluebook")
|
||||
assert cite == "42 C.F.R. pt. 414 (2025)."
|
||||
|
||||
def test_apa_with_section(self) -> None:
|
||||
r = Regulation(
|
||||
cfr_title="42",
|
||||
@@ -97,23 +152,38 @@ class TestFormatRegulation:
|
||||
cite = format_citation(r)
|
||||
assert "42 C.F.R. pt. 414" in cite
|
||||
|
||||
def test_bluebook_with_section(self) -> None:
|
||||
def test_apa_custom_title(self) -> None:
|
||||
r = Regulation(
|
||||
cfr_title="42",
|
||||
cfr_section="414.22",
|
||||
cfr_part="414",
|
||||
title="RVU Methodology",
|
||||
effective_date="2025-01-01",
|
||||
)
|
||||
cite = format_citation(r, style="bluebook")
|
||||
assert "42 C.F.R. § 414.22 (2025)." == cite
|
||||
cite = format_citation(r)
|
||||
assert "*RVU Methodology*" in cite
|
||||
|
||||
def test_bluebook_part_only(self) -> None:
|
||||
def test_apa_default_title_not_printed(self) -> None:
|
||||
r = Regulation(
|
||||
cfr_title="42",
|
||||
cfr_part="414",
|
||||
title="42 CFR Part 414",
|
||||
effective_date="2025-01-01",
|
||||
)
|
||||
cite = format_citation(r)
|
||||
assert "*42 CFR Part 414*" not in cite
|
||||
|
||||
def test_apa_with_url(self) -> None:
|
||||
r = Regulation(
|
||||
cfr_title="42",
|
||||
cfr_part="414",
|
||||
effective_date="2025-01-01",
|
||||
url="https://ecfr.gov/title-42/part-414",
|
||||
)
|
||||
cite = format_citation(r, style="bluebook")
|
||||
assert "42 C.F.R. pt. 414 (2025)." == cite
|
||||
cite = format_citation(r)
|
||||
assert "https://ecfr.gov/title-42/part-414" in cite
|
||||
|
||||
|
||||
# ── Download ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestFormatDownload:
|
||||
@@ -139,8 +209,11 @@ class TestFormatDownload:
|
||||
assert "Custom Portal" in cite
|
||||
|
||||
|
||||
# ── Source / Journal Articles ────────────────────────────────────────
|
||||
|
||||
|
||||
class TestFormatSource:
|
||||
def test_apa(self) -> None:
|
||||
def test_apa_generic(self) -> None:
|
||||
s = Source(
|
||||
title="PY 2026 Financial Guarantees",
|
||||
institution="CMS Innovation Center",
|
||||
@@ -158,39 +231,76 @@ class TestFormatSource:
|
||||
assert "Unknown" in cite
|
||||
|
||||
|
||||
class TestFormatRegulationExtra:
|
||||
def test_apa_custom_title(self) -> None:
|
||||
"""Line 132: regulation with title != default."""
|
||||
r = Regulation(
|
||||
cfr_title="42",
|
||||
cfr_part="414",
|
||||
title="RVU Methodology", # != "42 CFR Part 414"
|
||||
effective_date="2025-01-01",
|
||||
)
|
||||
cite = format_citation(r)
|
||||
assert "*RVU Methodology*" in cite
|
||||
class TestFormatJournalArticle:
|
||||
def _make_article(self, **kwargs) -> Source:
|
||||
defaults = {
|
||||
"title": "Skin substitute outcomes in wound healing",
|
||||
"institution": "Journal of Wound Care",
|
||||
"date_published": "2023-06-15",
|
||||
"doc_type": "journal-article",
|
||||
"extra": (
|
||||
"PMID: 12345678\n"
|
||||
"DOI: 10.1234/jwc.2023.001\n"
|
||||
"Authors: Smith J; Jones K; Brown M\n"
|
||||
"Journal: Journal of Wound Care\n"
|
||||
),
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return Source(**defaults)
|
||||
|
||||
def test_apa_default_title_not_printed(self) -> None:
|
||||
"""Line 132 branch: title == default → no extra title."""
|
||||
r = Regulation(
|
||||
cfr_title="42",
|
||||
cfr_part="414",
|
||||
title="42 CFR Part 414",
|
||||
effective_date="2025-01-01",
|
||||
)
|
||||
cite = format_citation(r)
|
||||
assert "*42 CFR Part 414*" not in cite
|
||||
def test_apa_three_authors(self) -> None:
|
||||
s = self._make_article()
|
||||
cite = format_citation(s)
|
||||
assert "Smith et al." in cite
|
||||
assert "(2023)" in cite
|
||||
assert "Skin substitute outcomes" in cite
|
||||
assert "*Journal of Wound Care*" in cite
|
||||
assert "https://doi.org/10.1234/jwc.2023.001" in cite
|
||||
|
||||
def test_apa_with_url(self) -> None:
|
||||
"""Line 134: regulation with URL."""
|
||||
r = Regulation(
|
||||
cfr_title="42",
|
||||
cfr_part="414",
|
||||
effective_date="2025-01-01",
|
||||
url="https://ecfr.gov/title-42/part-414",
|
||||
def test_apa_two_authors(self) -> None:
|
||||
s = self._make_article(extra="Authors: Smith J; Jones K\nDOI: 10.1/x\n")
|
||||
cite = format_citation(s)
|
||||
assert "Smith & Jones" in cite
|
||||
|
||||
def test_apa_one_author(self) -> None:
|
||||
s = self._make_article(extra="Authors: Smith J\nDOI: 10.1/x\n")
|
||||
cite = format_citation(s)
|
||||
assert "Smith." in cite
|
||||
|
||||
def test_apa_no_doi_uses_url(self) -> None:
|
||||
s = self._make_article(
|
||||
extra="PMID: 123\nAuthors: Smith J\n",
|
||||
url="https://example.com/article",
|
||||
)
|
||||
cite = format_citation(r)
|
||||
assert "https://ecfr.gov/title-42/part-414" in cite
|
||||
cite = format_citation(s)
|
||||
assert "https://example.com/article" in cite
|
||||
|
||||
def test_jama_format(self) -> None:
|
||||
s = self._make_article()
|
||||
cite = format_citation(s, style="jama")
|
||||
assert "Smith et al." in cite
|
||||
assert "*Journal of Wound Care*" in cite
|
||||
assert "doi:10.1234/jwc.2023.001" in cite
|
||||
|
||||
def test_jama_pmid_when_no_doi(self) -> None:
|
||||
s = self._make_article(extra="PMID: 99999\nAuthors: Smith J\n")
|
||||
cite = format_citation(s, style="jama")
|
||||
assert "PMID: 99999" in cite
|
||||
|
||||
def test_detected_by_pmid(self) -> None:
|
||||
"""Source with PMID in extra but no doc_type should still format as journal."""
|
||||
s = Source(
|
||||
title="Test Article",
|
||||
doc_type="",
|
||||
extra="PMID: 12345\nAuthors: Doe J\nJournal: Test J\n",
|
||||
date_published="2024-01-01",
|
||||
)
|
||||
cite = format_citation(s)
|
||||
assert "Doe." in cite
|
||||
assert "*Test J*" in cite
|
||||
|
||||
|
||||
# ── Generic / Bibliography ───────────────────────────────────────────
|
||||
|
||||
|
||||
class TestFormatGeneric:
|
||||
@@ -206,7 +316,6 @@ class TestFormatGeneric:
|
||||
assert "*Custom Item*" in cite
|
||||
|
||||
def test_generic_with_url(self) -> None:
|
||||
"""Line 169: _format_generic with URL."""
|
||||
item = Item(
|
||||
item_type="custom",
|
||||
title="Custom",
|
||||
|
||||
@@ -113,12 +113,15 @@ class TestExtractMetaTags:
|
||||
assert extract_meta_tags(fn) == []
|
||||
|
||||
def test_real_cclf_function(self):
|
||||
"""CCLF functions now use :pincite: directives instead of bare CCLF IP Section refs."""
|
||||
from aco.express.cclf import stg_beneficiary_xref
|
||||
from bib.pincite import parse_pincites
|
||||
|
||||
tags = extract_meta_tags(stg_beneficiary_xref)
|
||||
labels = {t.label for t in tags}
|
||||
assert "meta:cclf-ip-s3.1" in labels
|
||||
assert "meta:cclf-ip-s5.1.1" in labels
|
||||
doc = stg_beneficiary_xref.__doc__ or ""
|
||||
pincites = parse_pincites(doc, "aco.express.cclf.stg_beneficiary_xref")
|
||||
assert len(pincites) >= 2
|
||||
keys = {p.item_key for p in pincites}
|
||||
assert "VTWVB384" in keys
|
||||
|
||||
|
||||
# ── extract_meta_details ──────────────────────────────────────────
|
||||
@@ -269,6 +272,7 @@ class TestTagItemsFromPipeline:
|
||||
|
||||
store = Store(":memory:")
|
||||
item = Source(
|
||||
key="VTWVB384",
|
||||
title="CCLF Information Packet",
|
||||
tags=["source:cms-cclf-ip", "module:aco"],
|
||||
)
|
||||
@@ -282,12 +286,10 @@ class TestTagItemsFromPipeline:
|
||||
tagged = {k: v for k, v in report.items() if v}
|
||||
assert len(tagged) > 0
|
||||
|
||||
# Verify the item now has fn: and meta: tags
|
||||
# Verify the item now has fn: tags (meta: tags replaced by :pincite: directives)
|
||||
updated = store.get(key)
|
||||
fn_tags = [t for t in updated.tags if t.startswith("fn:")]
|
||||
meta_tags = [t for t in updated.tags if t.startswith("meta:")]
|
||||
assert len(fn_tags) > 0
|
||||
assert len(meta_tags) > 0
|
||||
|
||||
|
||||
# ── generate_docstring_refs ───────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user