Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/components/SummaryBox.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// USWDS-aligned Statutory Summary Box (usa-summary-box)
interface Props {
classification: string;
uscTitle: number;
uscTitle: number | string;
uscSection: string;
chapter: number;
currentThrough: string;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const statutes = defineCollection({
loader: glob({ pattern: '**/*.md', base: './content-data/statutes' }),
schema: z.object({
title: z.string(),
usc_title: z.number(),
usc_title: z.union([z.string(), z.number()]).transform((val) => String(val)),
usc_section: z.string(),
chapter: z.number(),
current_through: z.string(),
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/data/title-names.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const TITLE_NAMES: Record<number, string> = {
export const TITLE_NAMES: Record<number | string, string> = {
1: 'General Provisions',
2: 'The Congress',
3: 'The President',
Expand Down Expand Up @@ -53,4 +53,10 @@ export const TITLE_NAMES: Record<number, string> = {
52: 'Voting and Elections',
53: 'Reserved',
54: 'National Park Service and Related Programs',
'5a': 'Government Organization and Employees (Appendix)',
'11a': 'Bankruptcy (Appendix)',
'18a': 'Crimes and Criminal Procedure (Appendix)',
'26a': 'Internal Revenue Code (Appendix)',
'28a': 'Judiciary and Judicial Procedure (Appendix)',
'50a': 'War and National Defense (Appendix)',
};
2 changes: 1 addition & 1 deletion apps/web/src/pages/browse/[title].astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export async function getStaticPaths() {
const entries = await getCollection('statutes');

// Group by usc_title
const byTitle = new Map<number, typeof entries>();
const byTitle = new Map<string, typeof entries>();
for (const entry of entries) {
const titleNum = entry.data.usc_title;
if (!byTitle.has(titleNum)) {
Expand Down
8 changes: 5 additions & 3 deletions apps/web/src/pages/browse/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { TITLE_NAMES } from '../../data/title-names';
const allStatutes = await getCollection('statutes');

// Group statutes by title number
const byTitle = new Map<number, typeof allStatutes>();
const byTitle = new Map<string, typeof allStatutes>();
for (const entry of allStatutes) {
const titleNum = entry.data.usc_title;
if (!byTitle.has(titleNum)) {
Expand All @@ -16,8 +16,10 @@ for (const entry of allStatutes) {
byTitle.get(titleNum)!.push(entry);
}

// Sort titles numerically
const sortedTitles = [...byTitle.entries()].sort((a, b) => a[0] - b[0]);
// Sort titles using natural alphanumeric collation (e.g. 18 before 18a before 19)
const sortedTitles = [...byTitle.entries()].sort(([a], [b]) =>
a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' })
);

const totalSections = allStatutes.length;
const totalTitles = byTitle.size;
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/pages/statute/[...slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ export async function getStaticPaths() {
}

// Pre-compute sorted chapter list per title for cross-chapter nav
const titleChapters = new Map<number, number[]>();
const titleChapters = new Map<string, number[]>();
for (const [key] of byTitleChapter) {
const [titleStr, chapterStr] = key.split('-');
const titleNum = parseInt(titleStr ?? '0', 10);
const titleNum = titleStr ?? '';
const chapterNum = parseInt(chapterStr ?? '0', 10);
if (!titleChapters.has(titleNum)) titleChapters.set(titleNum, []);
titleChapters.get(titleNum)!.push(chapterNum);
Expand Down
22 changes: 19 additions & 3 deletions packages/transformer/src/__tests__/transformer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,22 @@ describe('FrontmatterSchema', () => {
expect(result.data.current_through).toBe('Unknown');
}
});

it('accepts alphanumeric title like 18a and transforms to string', () => {
const result = FrontmatterSchema.safeParse({
title: 'Section 1',
usc_title: '18a',
usc_section: '1',
chapter: 1,
current_through: 'PL 119-1',
classification: '18a U.S.C. § 1',
generated_at: 'now',
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.usc_title).toBe('18a');
}
});
});

describe('buildSectionPath', () => {
Expand Down Expand Up @@ -460,9 +476,9 @@ describe('XmlToMarkdownAdapter', () => {
// Appendix sections live under title-18a, distinct from main title-18
expect(section?.path).toBe('statutes/title-18a/chapter-1/section-1.md');
expect(section?.path).not.toContain('title-18/');
// usc_title frontmatter remains numeric (18) despite the "18a" path
expect(section?.content).toContain('usc_title: 18');
expect(section?.content).not.toContain('usc_title: 18a');
// usc_title frontmatter preserves appendix designation ("18a")
expect(section?.content).toContain('usc_title: "18a"');
expect(section?.content).not.toContain('usc_title: 18\n');
});

it('preserves inline element text in mixed content (cross-references)', () => {
Expand Down
20 changes: 15 additions & 5 deletions packages/transformer/src/markdown-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type SectionStatus = z.infer<typeof SectionStatusSchema>;
/** Zod schema for YAML frontmatter validation */
export const FrontmatterSchema = z.object({
title: z.string().min(1),
usc_title: z.number().int().positive(),
usc_title: z.union([z.string().min(1), z.number().int().positive()]).transform((val) => String(val)),
usc_section: z.string().min(1),
chapter: z.number().int().nonnegative(),
current_through: z.string().default('Unknown'),
Expand Down Expand Up @@ -172,11 +172,22 @@ function yamlQuote(value: string): string {
}

/** Generate YAML frontmatter string from validated data */
export function generateFrontmatter(data: Frontmatter): string {
export function generateFrontmatter(data: {
title: string;
usc_title: string | number;
usc_section: string;
chapter: number;
current_through: string;
classification: string;
generated_at: string;
status: SectionStatus;
}): string {
const titleStr = String(data.usc_title);
const formattedTitle = /^\d+$/.test(titleStr) ? titleStr : yamlQuote(titleStr);
const lines = [
'---',
`title: ${yamlQuote(data.title)}`,
`usc_title: ${data.usc_title}`,
`usc_title: ${formattedTitle}`,
`usc_section: ${yamlQuote(data.usc_section)}`,
`chapter: ${data.chapter}`,
`current_through: ${yamlQuote(data.current_through)}`,
Expand Down Expand Up @@ -382,13 +393,12 @@ export function generateMarkdownForSection(
const heading = extractHeading(sectionChildren);
const now = new Date().toISOString();

const uscTitle = parseInt(titleNum, 10) || 0;
const chapterInt = parseInt(chapterNum, 10) || 0;

const sectionTitle = `Section ${sectionNum}${heading ? ' - ' + heading : ''}`;
const frontmatter = FrontmatterSchema.parse({
title: sectionTitle,
usc_title: Math.max(uscTitle, 1),
usc_title: String(titleNum).trim() || '1',
usc_section: sectionNum,
chapter: chapterInt,
current_through: currentThrough || 'Unknown',
Expand Down
Loading