Skip to content

v3 importer: five-layer template pipeline, field sources, libdeg, template checks - #252

Open
fatsheep2 wants to merge 34 commits into
deb-sig:dev-v3from
fatsheep2:dev-v3
Open

fatsheep2 wants to merge 34 commits into
deb-sig:dev-v3from
fatsheep2:dev-v3

Conversation

@fatsheep2

Copy link
Copy Markdown
Contributor

Description

把 fork 上 dev-v3 的工作合回上游 dev-v3。设计见 doc/rfc-v3-importer-architecture.md:五层流水线 reader → shape → slots → roles → reconcile。

  • 读取层 pkg/reader:csv / xls / xlsx / json / xml / html / eml / api / 纯文本统一成表格。xls 改用打过补丁的 Apache-2.0 版 extrame/xls,去掉 GPL-3.0 的 xlsReader(另有单独 PR 提交给 master)。xlsx 按显示格式读出的数字不再补零:excelize 2.11 起会把 0.85 读成 0.850。
  • v3 模板引擎 pkg/importer:
    • shape:locateHeader 按表头名定位、dropIf 等,在规则之前整理表格;
    • slots:把账单映射到 Beancount 的日期、付款方、说明、金额、元数据;
    • direction:按列值、两列或金额正负判断收支,规则也能改方向(例如支付宝「不计收支」的退款);
    • 角色(self / other / from / to)和账户绑定;
    • 每个字段记录来源(sources:取自哪一列、哪条规则);
    • capture、failIf、timezone、output 块、reconcile(去重、余额断言)。
  • ExactMoney:金额按十进制原文一路传到输出;jd、huobi、hxsec、oklink 已迁移;新 provider 必须登记是否迁移。
  • 编译器稳定排序:同一时间的交易保持 provider 的顺序(另有单独 PR 提交给 master)。
  • libdeg / embed / wasm:C 库和浏览器版,供 App 和模板站使用。
  • CI:每次改动都用 provider 模板仓库的全部模板做回归,逐个对比期望输出。

Motivation and Context

  • 账单格式和导入规则从 DEG 本体里拆出来,由模板仓库维护;新增或更新账单格式时,不必再为每家机构写一个 Go provider。
  • 字段来源(sources)让上层应用能显示「这个值是从哪一列、哪条规则来的」,方便用户核对和改规则。
  • libdeg / wasm 让 App 和网页能直接嵌入同一个引擎,结果和命令行一致。
  • 去掉 GPL 依赖后,DEG 可以被闭源程序嵌入。

Dependencies

  • 去掉:github.com/shakinm/xlsReader(GPL-3.0)
  • 新增:github.com/antchfx/htmlquery、jsonquery、xmlquery、xpath(html / json / xml 读取,MIT)
  • 已合并上游 master 的依赖升级(excelize 2.11、Go 1.25 等)

Type of change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

现有 provider 和 translate 用法不变。与 master 相比,现有 provider 的输出只有一处差异:mt、oklink 中同一时间的交易顺序不同(稳定排序导致,排序后内容一致)。

How has this been tested?

  • go test ./... 通过
  • make test-providers 41/41 通过
  • make test-libdeg 通过(C API 跑一次导入)
  • GOOS=js GOARCH=wasm go build ./cmd/wasm-runtime 通过
  • provider 模板仓库全部 78 个模板版本与期望输出一致(CI 里的 Verify provider templates)
  • 现有 provider 输出与 master 逐个比较:只有 mt、oklink 是同一时间的交易换了顺序
  • 支付宝:用 master 的新格式示例账单跑 v3 模板,7/7 笔交易的日期、说明、金额与 Go provider 一致

复现:make test && make test-libdeg;模板回归见 .github/workflows/templates.yml。

Is this change properly documented?

  • doc/rfc-v3-importer-architecture.md:v3 架构 RFC
  • docs/docs/providers/template.md:模板语法,包括 capture、failIf、timezone、other / direction、^= $= =~ 运算符,以及新增语法的准入标准
  • docs/docs/providers/payment/alipay.md(中英文):支付宝退款在模板下的行为。Go provider 默认成对移除「原交易 + 全额退款」,v3 模板逐行处理、两笔都保留(合计为零)

claude and others added 30 commits October 8, 2026 21:08
Reader / Shaper / Mapper / Classifier / Reconciler, each with a fixed
data shape between them. Covers payment platforms, banks, securities,
exchanges, on-chain sources, and PDF/EML/XML/JSON readers. Maps upstream
issues to the layer that absorbs them and gives a migration order from
the current dev-v3 templateRules path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
…ate reader block

Readers produce one shape, a Table of string cells, and know nothing
about amounts or transactions. csv/xlsx/xls move over unchanged (sheet
selection and utf-16 added). json and xml share one XPath contract:
records selects a node per row, columns map names to relative paths.
text matches layout text with a named-group regexp plus a continuation
regexp, and converts pdf through pdftotext -layout when asked.

Templates may declare a reader: block; without one the legacy
template.fileFormat/encoding/delimiter fields are translated, so all 50
template-repo pins produce byte-identical output before and after.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
A shape: list settles everything about rows that is not one bill line
per transaction, before any rule runs: locateHeader finds the header by
anchor column names instead of counting leading rows, dropMatching and
dropIf remove preamble, totals and failed orders, merge folds split
fills into one record, split fans a line carrying a fee into two.

Without a locateHeader step the legacy skipLeadingRows/sourceHeaders/
headerLocate fields still decide the header, so a template can adopt a
single dropIf without touching anything else. All 50 template-repo pins
produce byte-identical output.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
Templates that declare slots fill only Beancount fields and metadata.
Personal rules assign accounts, and missing sides fall back to FIXME.

Co-authored-by: fatsheep <fatsheep2@users.noreply.github.com>
(cherry picked from commit b69a3ab)
Slot mappings, personal rules, and engine FIXME sides each leave a
FieldSource on the order: which rule, which columns, and which slot.
Beancount output is unchanged, and templates without slots still use
templateRules with empty Sources.

Co-authored-by: fatsheep <fatsheep2@users.noreply.github.com>
(cherry picked from commit 4411cdc)
…count bindings

Builds on the slot contract: direction replaces amountSign with three
explicit forms (one column's values, two amount columns, or the sign);
template vars may carry conditions and may never be accounts; legs
declare multi-leg shapes by role from a closed core set (from, to, cash,
custody, position, fee, gas, pnl, plus x- custom roles) and a template
leg naming an account is rejected. The rules file binds roles once under
accounts: and overrides per rule with an accounts action; unbound core
roles fall back to their FIXME account and are recorded as engine-filled.

Also fixes two drifts on dev-v3 that securities templates depend on:
format() now honours width and zero-pad flags (%06.0f), and arithmetic
results take their scale from the operands instead of the expression
text (0.85 + 0 prints 0.85, not 0.85000000). config init prefers the
registry's starter rules over the generated skeleton.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
Readers: html shares the XPath records/columns contract over the DOM;
eml is a container that picks a MIME part (media type or
attachment:<glob>, nested multipart, base64 and quoted-printable) and
hands it to an inner reader; api fetches the bill from an endpoint where
{source} is the import argument and {env.NAME} an environment variable,
failing early when a variable is unset and never echoing query values
in errors.

Reconcile: template.balance.column asserts the running balance between
consecutive rows in either row order, so a misread layout-text row is an
error rather than a wrong entry. reconcile.dedupe drops transactions
repeated in the bill or already present in an existing ledger by exact
key, flags near matches within a window, and flagEngineFilled marks
transactions whose accounts the engine had to fill. import logs what the
stage did. All 50 template-repo pins are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
… rules in slot mode

self is the bill's own account and lands on from for an outflow and on
to for an inflow, so a bank template's starter rules bind one role.
direction gains invert (credit-card statements list charges positive)
and default (a column value in neither list, such as 不计收支). With
slots declared, templateRules still run but may only shape fields;
accounts, postings and account-valued vars are rejected. Rule-added
metadata keys keep the position the template declares. Two-column
direction treats a zero beside a value as a placeholder, and the
rule-warning scanner no longer reads comparison operators as columns.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
cmd/wasm-runtime exposes degRuntimeImport(template, rules, name, bytes)
and returns the Beancount text plus transaction, FIXME and warning
counts, so the template hub can run a statement locally. The rules-file
merge moves into pkg/importer (ParseRulesFile/ApplyRulesFile) for the
CLI and the browser to share, and the analyser interface and runtime
analyser move to pkg/analyser/api so compilers no longer depend on every
provider package.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
…ration and metadata filtering

output.payee / output.narration rewrite every transaction after the
template mapping and before personal rules; output.metadata.drop or
keep filters metadata last, so rule-added keys are covered. The CLI's
private rules-file struct is replaced by importer.RulesFile so the CLI,
the browser build and the template hub read one schema. config init
documents the block in the generated file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
…; stable ordering

Four generic primitives from the provider audit, each in one layer:
- shape.capture lifts statement-level values (account number, card
  alias, statement month) from rows outside the table into <file.x>,
  visible to shape conditions, slots, vars and rules.
- shape.failIf stops the import on a matching record, with a message and
  the row, so a changed export fails loudly instead of writing entries.
- template.timezone pins wall-clock dates (and .timestamp) to an IANA
  zone instead of the machine's.
- role other is the counterparty side, to on an outflow and from on an
  inflow, so one rule covers a purchase and its refund. Rules may also
  set direction: outflow|inflow when only the user knows, such as which
  wallet address is theirs.

Ordering is now deterministic: compilers sort with a stable sort and a
newest-first statement is reversed first, so same-day entries keep the
order they happened in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
…=~ operators, and the admission criteria for new syntax

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
…les file; keep empty captures

Template rules that arrive through a rules file are now checked against
the slot contract at import, so postings or from/to there fail loudly
instead of being silently ignored. A capture group that matches an empty
value (a blank card alias) counts as found.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
…te writes an operator; shape conditions see cleaned cells

A var copied from a bill column (a 12/29 date, a 2024-01 code) stays
text; only expressions whose template text has an operator are
evaluated. Shape conditions now see cells after the same cleanup rows
get (BOM, ="…" wrappers), so failIf/dropIf match what rules match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
v3 importer: five-layer pipeline (reader, shape, slots, roles, reconcile)
The compilers now sort with a stable sort, so entries sharing a
timestamp keep the order the provider emitted them. The two expected
files differ only in that order (sorted contents are identical); make
test is 37/37 again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
Drop importer helpers left unused after the slot contract, check Close/
Remove/WriteFile errors, lowercase error strings, and take the tagged
switches and ReplaceAll calls staticcheck asks for. No behaviour change;
make test stays 37/37.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
…n't deadlock

degRuntimeImport now returns a Promise and does the work on a goroutine.
Templates with timezone: call time.LoadLocation, which probes the zoneinfo
directory through syscall/js fs; that callback can't fire while the
synchronous export holds the event loop, so every pinned template hung
the page. time/tzdata is embedded since the browser has no zoneinfo.

Checked with a 1,481-row Alipay export: the wasm output is byte-identical
to the CLI's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
fix: make dev-v3 green and stop the wasm runtime deadlocking on timezone templates
…L-3.0 xlsReader

github.com/shakinm/xlsReader is GPL-3.0, so any binary that links the
importer (the CLI, the wasm runtime, an app embedding the engine) is
bound by it. The .xls path now uses a copy of extrame/xls (Apache-2.0)
under pkg/reader/internal/xls with two fixes: numeric cells with a
built-in number format no longer panic, and non-date custom formats
("#,##0.00") no longer render amounts as dates. Rows keep the cells the
file defines, so the ccb and spdb_debit providers read through the same
function.

All 15 sample .xls bills (template repo + examples) produce the same
rows as before, blank rows aside; make test 37/37, template verify
78/78, golangci-lint clean.

Refs fatsheep/mirato#431

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
pkg/embed runs one import in memory and returns a versioned result
(contract 1): compiled Beancount text plus structured transactions -
date, payee, narration, ordered metadata, parsed postings (account,
amount, currency, cost, price), a from/to summary for plain transfers,
and per-slot sources (template / rule id / engine).

cmd/libdeg exports it over C for apps that embed DEG (Mirato via Rust
FFI): deg_import returns that JSON, deg_free releases it,
deg_contract_version reports the contract. Calls are thread-safe:
memory output is keyed per call and compilation is serialised because
the beancount compiler keeps its templates in package variables. The
browser runtime now goes through the same function.

test/libdeg/smoke.sh builds the c-archive, links a C program against it
and runs an import; CI runs it via make test-libdeg.

Refs fatsheep/mirato#431

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
Embed DEG in apps: Apache-licensed .xls reader and a C library (libdeg)
Templates on deg-provider-template are read by this engine. Each pull
request and push to dev-v3 builds DEG from the change and runs the template
repo's verify script, so a DEG change cannot quietly break a template. The
template repo already verifies against DEG dev-v3, so both sides check each
other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
ci: verify every provider template on each DEG change
Brings in upstream's CIB debit and IBKR providers, the new Alipay format,
the keep-refunds switch, Go 1.25 and the dependency bumps.

- go.mod/go.sum: newer versions from both sides; blackfriday/v2 stays for cobra 1.10.
- validator: upstream's multi-file check, lower-case error strings as lint wants.
- cib_debit: reads .xls through pkg/reader like the other providers, so the
  GPL xlsReader dependency stays out.
- ExactMoney gate: cib_debit and ibkr registered as not yet migrated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
Formatting only; no logic changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
excelize 2.11 (from upstream's bump) applies custom number formats, so a
stored 0.85 in a "0.000" cell read as "0.850" and the htsec template's
commissions gained a decimal place. When the shown text is a plain decimal
of the same value as the stored one, its trailing fractional zeros are
dropped; dates, percentages, thousands separators and text cells keep
their shown text.

Found by the provider-template check on the upstream merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
Merge upstream master into dev-v3; keep the GPL xls reader out
The built-in provider removes a matched full refund and its original order;
the template provider works row by row, so both stay, as if
keepRefundRecords were true. Balances are the same; to drop them, ignore
both sides in the rules file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
fatsheep2 and others added 4 commits October 11, 2026 03:28
docs(alipay): the template provider always keeps refund records
Upstream dev-v3 already lists them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpABFTvN18pARUNZFqCHis
@fatsheep2
fatsheep2 requested a review from a team as a code owner October 10, 2026 22:11

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants