Author: nigo (WeChat official account: 逆行的狗) Version: 1.0.0
Check audit reports (financial statements + notes), identify cross-reference errors, addition errors, and text formatting issues. Claude is the main controller—responsible for semantic understanding (locating statements, understanding note sections, determining column meanings, annotating table structures), while code handles deterministic arithmetic (summation, verification, reconcile). The two divide the work, eliminating the systematic false positives caused by the old tool's "hard splitting + keyword matching".
The most important principle: never do arithmetic by hand
An LLM directly performing numerical addition and subtraction will make calculation errors and cannot reproduce them—in audit verification, a calculation error is fatal. All numerical calculations, even just adding three numbers, call scripts/calculator.py. AI only decides "which numbers to pass" and "which equals which"; the code calculates the result.
Correct approach:
# 从报表/附注提取出数字后,调 calculator 验算
python3 scripts/calculator.py check "113,157,711.68" "113,157,711.68" # 核对相等
python3 scripts/calculator.py sum "1,234.56 5,678.90 100.00" # 求和
python3 scripts/calculator.py reconcile "100" "50" "30" "120" # 期初+增-减=期末
Incorrect approach:
# 心算"流动资产合计 = 货币资金 + 应收账款 + ..." ← 会算错,且别人无法核验
calculator supports thousands separators, parentheses for negative numbers, dashes (=0), full-width characters, and ten-thousand yuan units, with a default tolerance of 0.01, using round(diff, 6) to avoid floating-point boundary misjudgments. This is a principle already verified as effective in the previous version, and it is retained.
Workflow
Step 0 — Select inspection depth (scope)
Before starting, use the question tool to ask the user two things: inspection depth (single choice) + inspection type (multiple choice; if empty, execute according to the depth level).
Three depth levels:
| Level | Coverage | Time required | Applicable to |
|---|---|---|---|
| Quick check | Cross-references among the four statements + formatting (company name/page number/header) | A few minutes | Initial screening, only concerned with statement balance |
| Standard check (default) | Quick + horizontal and vertical addition within tables + high-frequency statement note cross-references + text (typos/grammatical errors) | A dozen or so minutes | Daily review, covering common errors |
| Deep check | Standard + reconcile all change statements in the notes + cross-account cross-references + AI structural annotation of all tables | More than half an hour, high token consumption | Major reports, final draft review |
If the user does not choose, default to standard. When deep check is selected, first warn that token consumption is relatively high and ask the user to confirm (a deep check of a 128-page report may consume a lot).
Inspection type multiple choice: cross-reference / horizontal and vertical addition / statement notes / text / formatting. Users can freely combine to override the depth level (selecting all ≈ deep check).
Why divide into levels: auditors most of the time only need to quickly verify cross-references; indiscriminate deep checking is both slow and token-intensive. Scope lets users make an informed choice and trigger as needed.
Automatic adjustment for notes-only: If it is identified that the document contains only notes and no four statements (such as the pure notes Word files of Shengtun/Bodaxincai), automatically skip cross-references between statements/statement notes, focus on cross-references within the notes + horizontal and vertical addition + text formatting, and inform the user that the scope has been adjusted. See "notes-only" below for details.
DeepSeek model selection (executed when AI is enabled): Standard/deep inspection enables DeepSeek acceleration by default (text typos, annotation table structure tagging, warning secondary review). When AI is involved, after asking depth + type, select the model first to avoid reasoning models dragging down batch processing:
- Check key validity:
python3 scripts/ai_worker.py --test-check-key(read ~/.deepseek/config.json; if no key, use--no-aipure code throughout) - Query available models:
python3 scripts/ai_worker.py --test-list-models(may fail to query when DeepSeek 503 service is busy; can skip and use default) - Use
questionto let the user select a model,default recommendation is flash-type models(non-reasoning, low latency per request, suitable for large-scale concurrency; reasoning types like v4-flash take 25s+ per request and will drag down batch processing). If the user has no preference, use flash directly. - After selection, add to run_check
--model <选定>to pass in, or write to the model field in ~/.deepseek/config.json for persistence.
Degradation principle: DeepSeek API service busy (503)/rate limiting are external issues. On repeated failures, degrade to --no-ai(pure code L1+scan+lexicon), text/annotation/warning review is backstopped by Claude's final review (Step 5 already requires Claude review, not fully dependent on the API).
Step 1 — Parse document
python3 scripts/parse_report.py "<文件或目录>" -o <输出目录>
The script automatically routes across four paths, selecting the best extraction path based on document type and content characteristics:
- PDF text-based (first-page text > 50 characters) → pdfplumber, numbers 100% accurate
- PDF scanned → mineru cloud API (
extract --model vlm), numbers require manual review - Word mainly text tables → python-docx, accurate
- Word image reports (many inline_shapes) → mineru cloud API
Word/PDF dual path (important): In practice, most cases are Word reports. For .docx/.pdf with the same basename, parse prioritizes .docx(deduplicated, Word tables are accurate with no word breaks); only use PDF when there is no Word. The two paths are processed separately:
- Word path (.docx): python-docx extracts table objects → markdown (tested successfully on all 90 Jinxing/87 Boda tables) + paragraph text;section locating(Note V-N accounts, Word has no fixed page numbers); tables automatically assigned to chapter (Heading1 major chapter + Heading2/short text details). Pseudo-tables (tab-stop layout not as table objects, few) are backstopped by AI.
- PDF path (.pdf):pdfplumber + page number locating(page=N, hyperlinks clickable).
Locating difference: Word uses sections ("Note V-3 Accounts Receivable"), PDF uses page numbers ("Page N"). Distinguished by the chapter (Word)/page (PDF) fields in result, export adapts automatically.
Output two files:
report.md: full-text markdown, each paragraph uses<!-- SOURCE file="..." page=N method=... -->to mark the source page number and extraction pathextracted_tables.json: raw two-dimensional table (headers + rows), no column mapping, no report classification, no continuation table merging
The parsing layer outputs only raw data; report locating, note identification, and column meaning determination are all completed semantically by Claude in subsequent steps. This is the core of eliminating the old tool's "split matching errors"—the parse layer does not overstep to do what should be done by humans. For the principles of the extraction strategy, see references/extraction.md。
mineru is a cloud API, data will be uploaded to mineru.net. Audit reports contain sensitive financial data; before using the mineru path for the first time, inform the user and obtain informed consent. See references/mineru_usage.md。
Step 2 — Understand the document structure (semantically, not by keywords)
Read report.md, understand the overall structure:
- Preparing entity, accounting period
- Report type: consolidated report (including consolidated + parent company, 8 statements in total) or standalone (4 statements)? Determine by checking whether the word "parent company" appears and the number of statements, not by a hardcoded keyword dictionary
- The locations of the four statements (consolidated balance sheet/income statement/cash flow statement/statement of changes in owners' equity, and the parent company versions) and their page ranges
- In a consolidated report, statements with the same name are repeated: in a consolidated report, titles such as "Balance Sheet" and "Statement of Changes in Owners' Equity" usuallyappear twice—the first time is the consolidated basis, the second time is the parent company basis, and the table headers oftencarry no "consolidated"/"parent company" prefix, so distinguish them by order of appearance (consolidated first, parent company second) and the preparing entity line.Both sets must be extracted, missing the parent company's 4 statements will leave the parent-company-level cross-checks completely absent. How to determine: if a statement with the same name appears ≥2 times in report.md → consolidated report, classify them in order as consolidated/parent company respectively.
- Structure of the notes chapters—which account each chapter discusses (e.g., "Note III, Monetary Funds" discusses monetary funds)
- Whether it is notes-only (no four statements) → follow the notes-only branch
- Detection of image-based statement pages (important): when reading report.md, watch for whether there aremultiple consecutive pages with blank text. In audit reports, the four statements (balance sheet, etc.) are often embedded in the PDF as images, and pdfplumber extracts these pages as blank (measured on Shengtun pages 7-16, 10 blank pages in total = all four statements are images). If parse_report's report.md has already marked
<!-- WARNING: ... 为图片/扫描页 -->, handle it accordingly; otherwise scan the text volume of each page yourself. When the four-statement area is found to be blank:- Re-extract that page range using mineru cloud OCR:
mineru-open-api extract "<报告.pdf>" --pages 7-16 -o <目录> --model vlm --language ch(requiresmineru-open-api authto configure a token first, see references/mineru_usage.md, including a privacy notice about uploading data to the cloud) - Merge the OCR result (md) back into the statements extraction
- When mineru is unavailable, clearly note in the report: "The four statements are images, pdfplumber failed to extract them, no cross-statement reconciliation was performed this time; manual review is recommended or configure a mineru token and rerun"—do not pretend it was done
- Re-extract that page range using mineru cloud OCR:
Recognize synonymous account variants through semantic understanding, without a mapping table: shareholders' equity = owners' equity, share capital = paid-in capital, statement of changes in shareholders' equity = statement of changes in owners' equity. Why use semantics instead of a keyword dictionary: every time a new report format is encountered, rules must be added, and keyword explosion is unsustainable; Claude can understand it in a single call and can handle variants.
Generate note_map.json (the locating cornerstone for statement-note cross-checks, mandatory in Step 2): when understanding the notes structure, generate in one pass a precise mapping of "account → notes detail table" note_map.json(written to the parse output directory). This is the three-layer division-of-labor architecture'slocating layer—Claude does semantic locating (accurate, done only once, low token), while data retrieval/arithmetic is left to code (DeepSeek locate + calculator).
Why locating must be done by Claude: the old tool relied on regex title matching + page-number proximity locating, and systematically failed on ① diverse title formats ② multiple accounts on the same page ③ continued tables spanning pages (see 旧报告检查工具_定位研究.md 4 major root causes). Claude can accurately locate to the statement level with a single semantic understanding.
Format ({科目: {...}}):
{"货币资金": {"table_ids": [220], "pages": [16], "field": "期末余额", "formula": "期末余额", "note": "报表数=附注期末余额"},
"应收账款": {"table_ids": [221,222], "pages": [18,19], "field": "账面价值", "formula": "账面余额-坏账准备=账面价值", "note": "报表数=附注账面价值"}}
table_ids: the statement IDs involved in the notes detail for this account (scope of the notes file,never mix in report statements such as the balance sheet/income statement, otherwise the statement-note reconciliation will "compare itself with itself")field: which field of the notes the statement figure equals (taken directly, e.g. "book value"/"ending balance")formula: the value extraction formula (when field cannot be obtained, compute according to this, e.g. "book balance - bad debt provision")- Locating method (table name is often empty, rely on multiple cross-references): account section page range + table page within that range + headers content + actual section text
- Minimum coverage: standard check covers at least 8 accounts (monetary funds/accounts receivable/notes receivable/prepayments/inventory/fixed assets/operating revenue/accounts payable), deep check covers all accounts presented in the statements.
subjects_index.json(coarse index of account → candidate tables generated by scan) can serve as locating reference/verification, but precise location is based on the note_map generated by Claude
Generate manifest.json (the structural contract for extracting the four statements, mandatory in Step 2): after understanding the positions/column layout of the four statements in the file, write it as manifest.json(written to the parse output directory). This is thestructural contract layerof the three-layer division-of-labor architecture — Claude semantically identifies the structure (accurately, only once), and data extraction is handed to apply_manifest for purely mechanical execution (zero guessing, new formats change JSON not code).
Automatic generation preferred:run_check --auto-manifest --auto-note-map , DeepSeek concurrently automatically generates manifest.json and note_map.json (looks at sheet headers/notes table content to determine column layout/account attribution), and the code performs structural validation + cross-check against the original text to reduce errors. If automatic generation fails or validation fails extensively → fall back to Claude manual writing (with a template library).
Why use manifest instead of relying on code guessing: Tianyi column layout (B/E/F) vs Macko (A/C/D) differ, Macko parent company BS main table/continued table column layouts differ, continued table item names contain spaces ("负 债 合 计"), non-standard names ("股东权益合计") — every time a new format is encountered, code guessing columns/guessing sheets/hardcoding ALIAS will break. manifest declares these differences in JSON, and apply_manifest extracts data according to the declarations.
Format (see references/manifest_template.md, including manifest samples for 4 reports):
{
"report_format": "merged_docx | split_xlsx | split_docx | split_pdf",
"company": "...", "period": "...",
"files": {"文件名": {"role": "statements|notes|opinion|cover", "loader": "xlsx|docx|pdf|doc"}},
"statements_map": {
"合并资产负债表": {
"source": "文件名", "loader": "xlsx|docx|pdf",
"sheets": ["sheet名"], "source_table": "表名(docx/pdf按此在extracted_tables定位)",
"kind": "linear | matrix",
"item_col": 0, "current_col": 2, "prior_col": 3,
"field_aliases": {"股东权益合计": "所有者权益合计"},
"merge_into": null
}
}
}
files.role+loader: separate-file roles (statements/notes/body/cover) + readers (xlsx=openpyxl / docx,pdf=read extracted_tables / doc=textutil conversion)item_col/current_col/prior_col: column layout (determine by looking at dump, do not guess)sheets+merge_into: BS continued table/segmented merge (split into two segments when the same table has different column layouts, merge with merge_into)source_table: for docx/pdf, locate the four statements in extracted_tables by table name/content (table name is often empty, rely on content matching "资产总计"+"流动资产合计"=BS)field_aliases: item name standardization ("股东权益合计"→"所有者权益合计")kind: linear|matrix: equity change statement matrix structure (sheets_main/sheets_prior + value_col)
Generation method: Claude looks at the dump of each statement file (xlsx sheet name+header+first 3 rows / docx extracted_tables four statement names+columns) and then manually writes the manifest (3-5 minutes). The manifests for Macko/Tianyi/Judong/Qingyan are already in their respective parse_output directories and can serve as templates.
Step 3 — Extract statements.json (apply_manifest mechanical execution)
Using the manifest.json from Step 2, call apply_manifest for purely mechanical data extraction:
python3 scripts/apply_manifest.py <parse输出目录>/manifest.json -o statements.json
apply_manifest reads the manifest's statements_map, and according to the declared source+loader(xlsx reads the sheet / docx,pdf read extracted_tables located by source_table) + item_col/current_col/prior_col extracts data, applies field_aliases to standardize item names, handles merge_into segmented merging, and produces statements.json.Zero guessing, zero code changes—all format differences are already declared in the manifest.
Output format (matching calculator_rules):
{
"合并资产负债表": {"流动资产合计": {"current": "113,157,711.68", "prior": "..."}, "资产总计": {"current": "...", "prior": "..."}},
"合并利润表": {...},
"合并现金流量表": {...},
"合并所有者权益变动表": {...},
"母公司资产负债表": {...}, ...
}
Field names must be exact:calculator_rules matches by field name (e.g. "期末现金及现金等价物余额"); inconsistent field names will cause the L1 check to report "missing required items". Before extraction, first check the required field list:
python3 scripts/calculator_rules.py
When the original report item names do not match the list,declare the mapping in the manifest's field_aliases(e.g. "股东权益合计"→"所有者权益合计", "负债和股东权益总计"→"负债和所有者权益总计"), do not change the code.
Step 4 — Code-side checks (including scan code verification)
python3 scripts/run_check.py <parse输出目录> \
--statements <parse输出目录>/statements.json \
--note-map <parse输出目录>/note_map.json \
--scope <scope> --scan --use-ai -o <parse输出目录>/results.json
Note:
--statementsand--note-mapsupport relative paths; the script will resolve them preferentially relative to<parse输出目录>; if not found, it will give a clear error.- It is recommended to write absolute paths or the
<parse输出目录>prefix in commands, to avoid path ambiguity under different working directories.
Optional parameters (for speeding up/skipping slow steps already run):
--max-workers N: DeepSeek concurrency count (default 60)--skip-annotation: skip AI structure annotation (reuse localannotations_cache.json)--skip-text-ai: skip AI text typo/grammar check--skip-ai-review: skip DeepSeek pre-review of warning/error (executed by default; the final review is still completed by Claude Step5)
The script performs deterministic checks (not dependent on LLM),--scan must be added—it lets the code share the heavy lifting of horizontal and vertical addition, greatly reducing your burden in Step 5:
--use-ai description (enabled by default):
- Default
--use-ai: enable DeepSeek concurrency (4 scenarios: ① text typos ② note table structure annotation ③table note cross-checking + note reconcile(locate by Step2's note_map→locate to fetch data→calculator comparison) ④ warning pre-review; all batched + concurrency default 60). Requires DeepSeek API key + a selected flash-class model (see Step 0). Text checks (typos/grammar) and note cross-checking data fetching can only be executed automatically with --use-ai; pure code scanning cannot cover them.--note-mapWhen not passed, reads by default<input_dir>/note_map.json(generated by Step2). - Optional
--no-ai: pure code scanning (no text AI check), used as a fallback when the DeepSeek service is busy/no key. In this case, text checks only use the built-in lexicon, and structure annotation/warning review are done manually by Claude in Step 5.- Automatic code fallback points:
run_check.pyin AI structure annotation (run_check.py:2368-2400), note cross-checking (run_check.py:2533-2562), text typos (run_check.py:2473-2510), warning secondary review (run_check.py:2640-2670) all four scenarios catch API/data-fetch exceptions; on failure, that scenario is skipped and a fallback notice is generated inresults.json, without interrupting the main flow.
- Automatic code fallback points:
- Model selection:
--model <name>is passed in; if not passed, the model field of ~/.deepseek/config.json is used. Prefer flash-class (non-reasoning), see Step 0 for details.
Checks performed(by scope):
- L1 inter-statement cross-checks: call
calculator_rules.pyfour functions, verify identities such as assets = liabilities + equity - Page number continuity、Company name consistency、Amount units、Typo/word list rough screening
- scan code verification(
--scan):- Vertical addition verification: for note tables containing "total/subtotal" rows, code sums the line items = total (excluding all total/subtotal/grand total rows and "of which:" sub-items). Of Venus's 301 tables, 64 can be code-verified; those that pass are directly recorded as info (you don't need to redo these tables)
- Horizontal addition verification: column names match known patterns (book balance - bad debt provision = book value, beginning + increase - decrease = ending, original cost - depreciation - impairment = value)
- Passes are recorded as info (reduces workload), imbalances are recorded as warning(possibly special table structures, need you to review with AI structure annotation in Step 5, distinguishing true errors vs special structures)
- Additional output
candidates.json(list of change statements/complex horizontal addition tables that code cannot judge, left for you to do AI structure annotation in Step 5) andsubjects_index.json(rough index of 41 accounts → candidate note tables,for your reference/validation when generating note_map.json in Step 2, precise location is based on the note_map generated by Claude)
Output results.json, each result contains: check_type / rule_name / severity / passed / description / expected / actual / difference / source_location / target_location / evidence / page / context / source_file。
Locator fields (used by auditors to verify the original text, required):
page: page number (pure number, e.g.45). Obtained from the<!-- SOURCE ... page=N -->comment in report.md or the note section location.context: original text excerpt (about 50 characters, the original text fragment of the line being checked), auditors see the original text directly in Excel without flipping through the PDF.source_file: automatically filled back with the original report path by run_check, you don't need to fill it in manually when writing semantic check results.rule_name: use meaningful account names (e.g.货币资金年末竖加、存货-原材料横加),do not include theid=Nprefix(id is the table sequence number during parsing, meaningless to auditors).
Step 5 — Semantic checks (must be executed, the core value of the skill)
run_check.py is onlydeterministic rough screening on the code side(L1 inter-statement identities + page numbers/units/typo word list). The following four types of semantic checks are the core capability that distinguishes this skill from ordinary scripts,must be executed, cannot be skipped——skipping equals doing only 20% of the work.
Agent final review output habits (D2.4):
- When the Agent goes back to the original text for verification in Step 5,it does not need to output every thinking step to the user. It can internally quickly group by table, batch judge, and directly correct
results.json。 - After correction is complete, directly execute Step 6 export, and finally inform the user once of the report save path and core conclusions (how many errors, how many doubtful items, and what they are). Expand details only when user decision or confirmation of doubtful items is needed.
⚠️ Claude final review mandatory (D2.1 core, most important)
- All error/warning must undergo Claude overall semantic reviewbefore output
- Final review must go back to the original text for verification; you cannot just look at the fields in results.json:
- Horizontal and vertical addition warning: read the complete headers+rows of that table in extracted_tables.json (locate it using the table ID in source_location),look at this table as a wholeand determine: ① whether this column is a percentage column (amounts should not be added) ② whether these rows are "of which" sub-items (double-counted) ③ whether it is a mixed calculation of two periods (line items and ≈ an integer multiple of the total) ④ whether line items are genuinely missing from or added to the sum (a real error).It is forbidden to just look at the expected/actual/ratio numbers and guess— the same ratio may be a real error or a cross-column issue; only by looking at the original table text can you distinguish them.
- Table note cross-check warning: use the table_ids in note_map to go to extracted_tables and look at the actual content of that table,confirm that the table_id points to the correct note table(not some other account table/risk summary table/bad debt table). The rough matching in subjects_index often points to the wrong table; note_map must be verified against the original text for positioning. When there is a data retrieval discrepancy, look at the total row of the note table vs the statement value.
- Typo error:grep report.md + decompress the docx XML and search the full text for that word, confirm that it actually exists in the original text (AI will fabricate words that are not in the original text, such as the "first-in first-out" hallucination). If it does not exist → delete (hallucination).
- Review rules:
- Confirm a real error(go back to the original text to confirm that the addition does not balance/the cross-check does not match/the typo really exists) → keep the error
- Special structure(percentage column, duplicated sub-items, subtraction items, different calibers, note_map pointing to the wrong table) → downgrade to questionable or delete
- False positive(AI hallucination, PDF space word splitting, DeepSeek over-reporting) → delete and do not display
- evidence must contain "Agent review: confirmed/questionable/deleted + basis for verification against the original text", and if there is no marker it is considered unreviewed and must be supplemented
Table note cross-check(check_type="table note"):run_check --use-ai has already been executed automatically(three-layer division of labor: note_map positioning in Step2 → DeepSeek locate data retrieval → calculator comparison of "statement value vs note value"). You only need to do the Claude final review + fallback:
- Spot-check the positioning accuracy of note_map: randomly compare a few accounts against the original note text, and confirm that the table_ids point to the correct note detail table (not a statement row)
- Final review of warnings with check_type=table note in results.json(statement value ≠ note value / data retrieval failed), distinguishing: ① real error (the values really do not match) ② caliber difference (the note is split by aging/category, and a different caliber is normal) ③ locate data retrieval failure (→ supplement manually). Review according to "confirmed real error/questionable/deleted", and add an "Agent review" marker to evidence
- Accounts with --no-ai or data retrieval failure are downgraded to Claude manual: locate the original note text according to note_map and extract the numbers, call
calculator checkfor comparison (never do arithmetic by hand; always use calculator)
- Minimum workload (hard threshold): one result per account covered by note_map (standard ≥8, deep full coverage of accounts presented in the statements). Note-only with no four statements → skip, and record one info entry explaining the reason
Horizontal and vertical addition(check_type="horizontal addition"/"vertical addition"): The scan in Step 4 has already performed code-based verification on tables containing total rows. Here you do the part that the scan cannot do:
Re-check by table group (efficiency is key; item-by-item is prohibited): Multiple vertical addition/horizontal addition warnings for the same table (e.g., an aging table has 4 columns each reporting 1 warning = 4 warnings)Merge into a single re-check—read the table's complete original text once, and judge all warnings for that table in batch. Group by source_location (table ID):
# 按表分组 warning,一张表一次复核
from collections import defaultdict
by_table = defaultdict(list)
for w in warnings: by_table[w.get("source_location")].append(w)
for table_loc, table_warns in by_table.items():
# 读这张表的完整 headers+rows(一次),批量判断该表所有 warning
Judgment criteria (a clear conclusion must be given; straddling the fence and leaving it doubtful is prohibited):
- Delete (confirmed false positive): Percentage column mistakenly included in vertical addition / "of which" sub-item duplicated / two periods mixed in the calculation (detail sum ≈ an integer multiple of the total) / cross-column comparison (orders of magnitude differ greatly) / note_map points to the wrong table / blank value in the total row
- Confirm error (true error): calculator confirms detail sum ≠ total, and the above structural reasons have been ruled out (same table, same column, details are indeed under-added/over-added)
- Pass (confirmed correct): difference <0.5% and it can be judged to be rounding (calculator confirms the difference is a few cents)
- Doubtful (only when it still cannot be judged after returning to the original text): the note basis and the statement basis are indeed different (e.g., notes split by aging, while the statement is net value), requiring the auditor to judge the basis—this is the only situation where doubt is allowed. Not understanding the table structure is not a reason for doubt; keep reading until you understand it or mark it for deletion.
Why a clear conclusion is mandatory: the auditor receiving the report must be able to act directly (change/do not change/manual review); too many "doubtful" items amount to no verification at all. DeepSeek will produce false positives, but after you return to the original text you should be able to judge; if you cannot judge, it means the original text was not read thoroughly enough.
Reconcile the movement table within the notes(check_type="within notes"): call calculator reconcile to verify beginning balance + increases − decreases = ending balance (fixed assets, employee benefits payable, taxes payable, etc., see rules.md C1). Do this for every table identified as having a four-column movement structure.
Text re-check(check_type="text"): read through report.md, looking for typos, grammatical errors, contradictions, and inconsistencies in data basis. This is the part LLMs are good at, whereas the script word list is only a rough screen.
Only after completing the above four categories can you proceed to Step 6. If a category truly has no target (e.g., a notes-only document has no four statements → no statement-note cross-reference object), record an info entry in results stating "statement-note cross-reference not performed (reason: notes-only, no four statements)",do not skip silently—let the user know which checks were done, which were not, and why.
Every semantic check result must include location information(the auditor needs to return to the original text for verification): find the <!-- SOURCE page=N --> marker of the checked content from report.md, and fill it into page; put an excerpt of the original text of the checked line (about 50 characters) into context. This way the auditor can click the page number in Excel to open the original report, and view the original text excerpt to verify directly, without flipping through the PDF page by page.
Append the supplementary results in the unified format to results.json(fields the same as Step 4).
Step 6 — Output report
python3 scripts/export_report.py results.json -o <输出目录>
Generate two files:
- 7 sheet Excel: classified in an auditor-friendly way; passed items do not go into the detailed sheet (only in summary statistics):
- Summary: check scope / passed / issues / doubtful statistics + statistics by category + severity explanation
- Intra-report cross-footing: Balance sheet balancing, income statement derivation, cash flow statement, statement of changes in equity (L1 identities + vertical addition within a statement)
- Statement-to-note cross-footing: Statement figures vs. note figures (accounts receivable = gross carrying amount - allowance for bad debts, etc., 41 accounts)
- Intra-note cross-footing: Note movement schedule reconcile (opening + increase - decrease = closing), cross-note table relationships
- Horizontal and vertical addition: Horizontal and vertical totals of note detail tables (gross carrying amount - bad debts = carrying value, sum of details = total)
- Text formatting: Typos, headers, page numbers, company name, unit, numbers
- Check items: List of all check items (including passed items)
- Markdown review report: Divided into chapters by issue/questionable item, for human reading
Severity binarization:
- Issue: Confirmed error requiring correction (Agent review confirmed)
- Questionable: Suspicious, requiring manual verification (Agent review questionable)
- Pass: Passed items are not displayed in the detailed sheet, only in summary statistics
Output is read-only and does not modify the source report (audit-only).
Step 7 — Interpret results
Explain the discovered issues to the user, describing them by severity,keep it concise:
- Error (red): The calculator computes a definite discrepancy, e.g., assets ≠ liabilities + equity. Provide the expected value, actual value, difference, page location, and repair suggestion.
- Abnormal (yellow): AI + code dual verification inconsistency, e.g., statement figures vs. note figures do not match. Prompt for manual review.
- Notice (blue): AI finds something suspicious but it cannot be quantified, e.g., non-operating income is negative. Avoid under-reporting but do not create noise.
For items marked "abnormal", make a second judgment to rule out false rule triggers (such as normal differences caused by different note scopes) before presenting to the user.
Output habits: After completing Step 5 final review and Step 6 export, directly tell the user the report save path (Excel + Markdown), and provide core statistics (number of issues/questionable items/main issues). There is no need to expand item by item unless the user requests it.
Key guidance
Details are in the respective reference files; here only the skeleton and timing for reading are given.
Locate the four statements: Look at the preparing entity, accounting period, and item structure; semantically identify 8 consolidated + parent-company statements or 4 standalone statements. Identify variants (shareholders' equity = owners' equity). Read references/rules.md to understand the scope of account coverage.
Note semantic index: Understand that "Note III, Monetary funds" discusses monetary funds, and "(III) Accounts receivable" discusses accounts receivable; do not exhaustively match titles with regular expressions. Index by account and retrieve data on demand; do not load everything.
Column meaning determination: Determine closing/opening/note number/item name columns by looking at headers and content; do not hard-code the number of columns. 3 columns, 4 columns, and about 8 columns in left-right dual-panel layouts can all be handled.
AI structure annotation: Read references/structure_annotation.md, use the prompt templates inside to annotate row type/op, column type, and horizontal addition relationships; batch annotation improves efficiency. If annotation fails, downgrade to total-row-only verification.
Note-only: No four statements → skip inter-statement/statement-note cross-checks, perform intra-note cross-checks + horizontal and vertical addition + text formatting. Clearly mark in the report "This document is notes-only; inter-statement/statement-note cross-checks were not performed."
Arithmetic discipline + batching for large reports
Combined large reports of 50-150 pages (e.g., Shengtun 128 pages) easily cause context explosion. Batching strategy:
- First read the four statements (usually in the front part of the report, with fewer pages) → perform inter-statement/intra-statement checks
- Go account by account to find details in the notes (use semantic indexing to locate, do not load all notes)
- Process in batches to avoid context explosion
Arithmetic discipline remains unchanged throughout: call calculator for any numerical addition or subtraction; the AI only decides which numbers to pass in.
Cross-platform dependency installation + mineru privacy
Python dependencies(common to mac/Windows):
pip install pdfplumber python-docx openpyxl openai
mineru(only needed for scanned/image statements):
npm install -g mineru-open-api
mineru-open-api auth # 配置免费 token
The script is pure Python and cross-platform (pathlib, no platform-specific commands), supporting both mac/Windows, Chinese paths, and paths containing spaces.
mineru data privacy: Audit reports contain sensitive financial data; before uploading to the cloud, the user must be informed and give informed consent. Public reports of listed companies have no privacy issues; for unpublished/confidential reports, it is recommended to prioritize the text path (pdfplumber/python-docx, data does not leave the local machine). See references/mineru_usage.md。
references index
| File | When to read |
|---|---|
references/rules.md |
Consult the 41-account comparison table when doing statement-note cross-checks; consult reconcile rules when doing change statements; when the user asks which accounts are covered |
references/structure_annotation.md |
Use prompt templates to annotate note tables when doing horizontal and vertical addition; consult fallback strategies when annotation fails; consult efficiency suggestions for batch annotation |
references/extraction.md |
Judge numerical confidence when seeing mineru source markers in report.md; when the user asks why four paths/why text is prioritized |
references/mineru_usage.md |
Guide installation and configuration of mineru when encountering scanned/image statements for the first time; troubleshoot mineru call failures; when the user asks about data security |
references/user_rules.md |
Load and execute user-defined rules on every check; write them in when the user describes new requirements |
User-defined rules
Read references/user_rules.md. Users can describe special check requirements in the conversation (e.g., "our firm requires verifying the completeness of related-party disclosures" "for financial industry clients, focus on checking the five-tier loan classification"), and after understanding them, organize them into the format "natural language description + applicable scenario + expected result" and write them into the "## My Rules" section of that file. Rules follow the skill (not bound to a specific report directory) and are shared by all reports. Execute these rules together during checks. When a rule becomes invalid or needs modification, the user can just say so in the conversation, and you update the file.