import: specs for the PDF, XLSX, and chart parsers; chart header on the first non-blank row
check / check (push) Canceled after 0s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-13 21:03:03 -06:00
1 parent e52b88433c
commit 38a24c7609
3 files changed
+44 -1

No files matched your search

+18 -1
View File
@@ -56,6 +56,21 @@ pub enum ChartCsvError {
/// Parse a chart CSV. Returns rows in file order (parents should come first,
/// but order is not validated here).
///
/// ```text
/// GOAL: read a chart of accounts a person hands the agent.
/// SOURCE: docs/mcp-interface.md (books.import_chart_csv); CONVENTION for
/// the layout.
/// INVARIANTS:
/// 1. The first non-blank row is the header, exactly code,name,type,
/// parent_code with an optional fifth tax_category (any case);
/// anything else is BadHeader.
/// 2. Every later row has the header's width (BadColumnCount), a type
/// among asset, liability, equity, revenue, expense, any case
/// (BadType), and a non-blank code and name (Blank, with its row).
/// 3. A blank parent or tax category is None; blank lines are skipped;
/// an empty file is no rows.
/// ```
pub fn parse_chart_csv(text: &str) -> Result<Vec<ChartRow>, ChartCsvError> {
let mut reader = csv::ReaderBuilder::new()
.has_headers(false)
@@ -64,13 +79,15 @@ pub fn parse_chart_csv(text: &str) -> Result<Vec<ChartRow>, ChartCsvError> {
.from_reader(text.as_bytes());
let mut out = Vec::new();
let mut width = 4;
let mut header_seen = false;
for (idx, record) in reader.records().enumerate() {
let record = record.map_err(|e| ChartCsvError::Csv(e.to_string()))?;
let fields: Vec<&str> = record.iter().collect();
if fields.iter().all(|f| f.is_empty()) {
continue;
}
if idx == 0 {
if !header_seen {
header_seen = true;
let lower: Vec<String> = fields.iter().map(|f| f.to_lowercase()).collect();
width = if lower == ["code", "name", "type", "parent_code"] {
4
+13
View File
@@ -98,6 +98,19 @@ fn typeset(st: &Statement, meta: &ReportMeta) -> Result<PagedDocument, PdfError>
}
/// Render a laid-out statement (see [`crate::report`]) as a PDF.
///
/// ```text
/// GOAL: a statement a CPA can print and file.
/// SOURCE: CONVENTION (layout); docs/spec/sources/irs-records.md (records
/// "capable of being processed", reproducible).
/// INVARIANTS:
/// 1. Deterministic: the same statement and metadata render to the same
/// bytes.
/// 2. Every label is data, never markup: a name holding Typst syntax
/// ("#panic()", "*", "$", "[") prints as text and NEVER runs.
/// 3. Every row reaches the page: template_data carries each row's label,
/// kind, and amounts split by amount_parts, in order.
/// ```
pub fn statement_pdf(st: &Statement, meta: &ReportMeta) -> Result<Vec<u8>, PdfError> {
let doc = typeset(st, meta)?;
typst_pdf::pdf(&doc, &typst_pdf::PdfOptions::default())
+13
View File
@@ -163,6 +163,19 @@ fn number(
}
/// Render a report to XLSX bytes.
///
/// ```text
/// GOAL: a statement as a spreadsheet a CPA can sum and check.
/// SOURCE: docs/spec/sources/xlsx-numbers.md (a cell number is a binary
/// double; Excel honors 15 significant digits).
/// INVARIANTS:
/// 1. An amount is a number cell, in an accounting format (thousands
/// separators, negatives in parentheses), unless Excel couldn't show
/// it exactly (finer than a cent, or past 15 digits): then its exact
/// text, right-aligned. NEVER a number Excel would show wrong.
/// 2. Every statement row is a row, in order, with its label.
/// 3. The output is a valid XLSX (a zip holding a workbook).
/// ```
pub fn report_to_xlsx(report: &XlsxReport<'_>, meta: &ReportMeta) -> Result<Vec<u8>, XlsxError> {
let mut workbook = Workbook::new();
let sheet = workbook.add_worksheet();