Context
PipelineState<Validated>::to_transaction_facts() at crates/ledger-core/src/pipeline.rs currently returns TransactionFacts::new() — an empty default. Every field (vendor_jurisdiction, supply_type, tax_code, amount, is_business_activity, is_ordinary, is_necessary) is None.
Consequence: verify_legal() (introduced in PRD-7 Phase 0) dispatches to LegalSolver::verify_all() with a fact-free struct, which returns Z3Result::Unknown for every rule, including the five new rules in Jurisdiction::legal_ruleset(). The legal gate exists in the type system but produces no signal.
What Needs to Happen
Add a parsed document field contract. PipelineState<Validated> must carry enough structured data for to_transaction_facts() to emit non-None fields. Two options, pick the simpler one:
Option A — add a doc_fields: HashMap<String, String> field to PipelineState<S>.
to_transaction_facts() maps known keys ("vendor_jurisdiction", "supply_type", "tax_code", "amount") from the map. Callers (ingest path) populate the map from parsed document rows.
Option B — add a typed DocumentFields struct as a public field on PipelineState<S>.
pub struct DocumentFields {
pub vendor_jurisdiction: Option<String>,
pub supply_type: Option<String>,
pub tax_code: Option<String>,
pub amount: Option<rust_decimal::Decimal>,
pub is_business_activity: Option<bool>,
pub is_ordinary: Option<bool>,
pub is_necessary: Option<bool>,
}
to_transaction_facts() copies fields directly. Preserves type safety; avoids stringly-typed map.
Option B is preferred — it mirrors TransactionFacts shape and makes the mapping trivial.
Acceptance Criteria
PipelineState<Validated> carries a DocumentFields (or equivalent) field populated at construction.
to_transaction_facts() returns a TransactionFacts with non-None fields when the document fields are set.
test_verify_legal_integration in pipeline.rs exercises a complete fact → Z3 verification path: set DocumentFields { vendor_jurisdiction: Some("US"), supply_type: Some("SaaS"), tax_code: Some("BASEXCLUDED"), .. } and assert verify_legal() returns Ok(Classified) against au_gst::rule_38_190().
- Setting
tax_code: Some("INPUT") for a US SaaS vendor asserts verify_legal() returns Err(NeedsReview).
- No new dependencies. No
unwrap in non-test code.
Files
crates/ledger-core/src/pipeline.rs — primary change site
crates/ledger-core/src/legal.rs — TransactionFacts shape reference
crates/ledger-core/src/ingest.rs — must populate DocumentFields at row parse time
Dependency
Unblocks PRD-7 Phase 2 (symbolic Z3 upgrade) and PRD-7 Phase 3 (audit sheet) — both are meaningless until facts are real.
Context
PipelineState<Validated>::to_transaction_facts()atcrates/ledger-core/src/pipeline.rscurrently returnsTransactionFacts::new()— an empty default. Every field (vendor_jurisdiction,supply_type,tax_code,amount,is_business_activity,is_ordinary,is_necessary) isNone.Consequence:
verify_legal()(introduced in PRD-7 Phase 0) dispatches toLegalSolver::verify_all()with a fact-free struct, which returnsZ3Result::Unknownfor every rule, including the five new rules inJurisdiction::legal_ruleset(). The legal gate exists in the type system but produces no signal.What Needs to Happen
Add a parsed document field contract.
PipelineState<Validated>must carry enough structured data forto_transaction_facts()to emit non-None fields. Two options, pick the simpler one:Option A — add a
doc_fields: HashMap<String, String>field toPipelineState<S>.to_transaction_facts()maps known keys ("vendor_jurisdiction","supply_type","tax_code","amount") from the map. Callers (ingest path) populate the map from parsed document rows.Option B — add a typed
DocumentFieldsstruct as a public field onPipelineState<S>.to_transaction_facts()copies fields directly. Preserves type safety; avoids stringly-typed map.Option B is preferred — it mirrors
TransactionFactsshape and makes the mapping trivial.Acceptance Criteria
PipelineState<Validated>carries aDocumentFields(or equivalent) field populated at construction.to_transaction_facts()returns aTransactionFactswith non-None fields when the document fields are set.test_verify_legal_integrationinpipeline.rsexercises a complete fact → Z3 verification path: setDocumentFields { vendor_jurisdiction: Some("US"), supply_type: Some("SaaS"), tax_code: Some("BASEXCLUDED"), .. }and assertverify_legal()returnsOk(Classified)againstau_gst::rule_38_190().tax_code: Some("INPUT")for a US SaaS vendor assertsverify_legal()returnsErr(NeedsReview).unwrapin non-test code.Files
crates/ledger-core/src/pipeline.rs— primary change sitecrates/ledger-core/src/legal.rs—TransactionFactsshape referencecrates/ledger-core/src/ingest.rs— must populateDocumentFieldsat row parse timeDependency
Unblocks PRD-7 Phase 2 (symbolic Z3 upgrade) and PRD-7 Phase 3 (audit sheet) — both are meaningless until facts are real.