Skip to content

Commit 686e1de

Browse files
feat(transformer): add 'status' frontmatter field for section state (#90) (#91)
Transformer: - Add SectionStatusSchema enum (active|repealed|reserved|omitted|transferred|renumbered) - Add detectSectionStatus() function to derive status from heading text - Generate status field in YAML frontmatter for every section - Export new types and function Content Collection: - Add status field to schema with 'active' default for backwards compat Frontend (statute + browse): - Use entry.data.status as primary check with title-based fallback - Eliminates fragile string matching as sole detection method - Single source of truth for status in the transformer Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0f59aae commit 686e1de

5 files changed

Lines changed: 39 additions & 13 deletions

File tree

apps/web/src/content.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const statutes = defineCollection({
1111
current_through: z.string(),
1212
classification: z.string(),
1313
generated_at: z.string().optional(),
14+
status: z.enum(['active', 'repealed', 'reserved', 'omitted', 'transferred', 'renumbered']).default('active'),
1415
}),
1516
});
1617

apps/web/src/pages/browse/[title]/[chapter].astro

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,12 @@ const base = import.meta.env.BASE_URL;
6666
<ul class="divide-y divide-gray-200 rounded-lg border border-gray-200 dark:divide-gray-800 dark:border-gray-800">
6767
{sortedSections.map((entry) => {
6868
const sTitle = entry.data.title;
69-
const isRepealed = sTitle.includes('Repealed');
70-
const isReserved = sTitle.includes('Reserved');
71-
const isOmitted = sTitle.includes('Omitted');
72-
const isTransferred = sTitle.includes('Transferred') && !sTitle.includes('Transferred or reemployed');
73-
const isRenumbered = sTitle.includes('Renumbered');
69+
const status = entry.data.status ?? 'active';
70+
const isRepealed = status === 'repealed' || sTitle.includes('Repealed');
71+
const isReserved = status === 'reserved' || sTitle.includes('Reserved');
72+
const isOmitted = status === 'omitted' || sTitle.includes('Omitted');
73+
const isTransferred = status === 'transferred' || (sTitle.includes('Transferred') && !sTitle.includes('Transferred or reemployed'));
74+
const isRenumbered = status === 'renumbered' || sTitle.includes('Renumbered');
7475
const isInactive = isRepealed || isReserved || isOmitted || isTransferred || isRenumbered;
7576
return (
7677
<li>

apps/web/src/pages/statute/[...slug].astro

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,13 @@ const formattedDate = generated_at
6262
? new Date(generated_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
6363
: undefined;
6464
65-
// Repealed / Reserved / Omitted / Transferred / Renumbered detection
66-
const isRepealed = entry.data.title.includes('Repealed');
67-
const isReserved = entry.data.title.includes('Reserved');
68-
const isOmitted = entry.data.title.includes('Omitted');
69-
const isTransferred = entry.data.title.includes('Transferred') && !entry.data.title.includes('Transferred or reemployed');
70-
const isRenumbered = entry.data.title.includes('Renumbered');
65+
// Section status — use frontmatter field with title-based fallback for pre-status data
66+
const status = entry.data.status ?? 'active';
67+
const isRepealed = status === 'repealed' || entry.data.title.includes('Repealed');
68+
const isReserved = status === 'reserved' || entry.data.title.includes('Reserved');
69+
const isOmitted = status === 'omitted' || entry.data.title.includes('Omitted');
70+
const isTransferred = status === 'transferred' || (entry.data.title.includes('Transferred') && !entry.data.title.includes('Transferred or reemployed'));
71+
const isRenumbered = status === 'renumbered' || entry.data.title.includes('Renumbered');
7172
---
7273

7374
<BaseLayout

packages/transformer/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ export {
1010
nestingDepthFor,
1111
reformatInlineLists,
1212
FrontmatterSchema,
13+
SectionStatusSchema,
14+
detectSectionStatus,
1315
} from './markdown-generator.js';
14-
export type { Frontmatter, MarkdownFile } from './markdown-generator.js';
16+
export type { Frontmatter, MarkdownFile, SectionStatus } from './markdown-generator.js';
1517
export { USLM_ELEMENTS, USLM_NAMESPACE, INDENT_PER_LEVEL, MAX_NESTING_DEPTH } from './constants.js';
1618
export { createLogger } from '@civic-source/shared';
1719
export { extractTextFromNodes, findElements, getAttributes, getElementName } from './xml-utils.js';

packages/transformer/src/markdown-generator.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import { z } from 'zod';
22
import { USLM_ELEMENTS, INDENT_PER_LEVEL, MAX_NESTING_DEPTH } from './constants.js';
33
import { extractTextFromNodes, findElements } from './xml-utils.js';
44

5+
/** Section status — derived from heading text during transformation */
6+
export const SectionStatusSchema = z.enum([
7+
'active', 'repealed', 'reserved', 'omitted', 'transferred', 'renumbered',
8+
]);
9+
10+
export type SectionStatus = z.infer<typeof SectionStatusSchema>;
11+
512
/** Zod schema for YAML frontmatter validation */
613
export const FrontmatterSchema = z.object({
714
title: z.string().min(1),
@@ -11,10 +18,21 @@ export const FrontmatterSchema = z.object({
1118
current_through: z.string().default('Unknown'),
1219
classification: z.string().min(1),
1320
generated_at: z.string().min(1),
21+
status: SectionStatusSchema.default('active'),
1422
});
1523

1624
export type Frontmatter = z.infer<typeof FrontmatterSchema>;
1725

26+
/** Detect section status from heading text */
27+
export function detectSectionStatus(heading: string): SectionStatus {
28+
if (heading.includes('Repealed')) return 'repealed';
29+
if (heading.includes('Reserved')) return 'reserved';
30+
if (heading.includes('Omitted')) return 'omitted';
31+
if (heading.includes('Renumbered')) return 'renumbered';
32+
if (heading.includes('Transferred') && !heading.includes('Transferred or reemployed')) return 'transferred';
33+
return 'active';
34+
}
35+
1836
/** A generated markdown file with its path and content */
1937
export interface MarkdownFile {
2038
/** Relative path: statutes/title-{n}/chapter-{n}/section-{n}.md */
@@ -139,6 +157,7 @@ export function generateFrontmatter(data: Frontmatter): string {
139157
`current_through: "${data.current_through}"`,
140158
`classification: "${data.classification}"`,
141159
`generated_at: "${data.generated_at}"`,
160+
`status: "${data.status}"`,
142161
'---',
143162
'',
144163
];
@@ -282,14 +301,16 @@ export function generateMarkdownForSection(
282301
const uscTitle = parseInt(titleNum, 10) || 0;
283302
const chapterInt = parseInt(chapterNum, 10) || 0;
284303

304+
const sectionTitle = `Section ${sectionNum}${heading ? ' - ' + heading : ''}`;
285305
const frontmatter = FrontmatterSchema.parse({
286-
title: `Section ${sectionNum}${heading ? ' - ' + heading : ''}`,
306+
title: sectionTitle,
287307
usc_title: Math.max(uscTitle, 1),
288308
usc_section: sectionNum,
289309
chapter: chapterInt,
290310
current_through: currentThrough || 'Unknown',
291311
classification: `${titleNum} U.S.C. \u00A7 ${sectionNum}`,
292312
generated_at: now,
313+
status: detectSectionStatus(sectionTitle),
293314
});
294315

295316
let body = generateSectionBody(sectionChildren);

0 commit comments

Comments
 (0)