Statutory audit trace
Several guards attach a structured, machine-readableaudit_trace to their verdict so an auditor can see exactly which statute drove the decision — instead of parsing free-text reasons. The field is additive: existing result keys are unchanged, so callers that ignore it keep working.
audit_trace include InputCreditGuard (ITC), TDSGuard (Sec 194J/194C/194H/194I), and GSTGuard (RCM). Rule identifiers and statute strings are centralized in qwed_tax/audit.py.
Structured diagnostics (TaxDiagnosticResult)
TaxDiagnosticResult is an opt-in, three-layer model that converts a guard’s legacy dict return into a typed, tri-state verdict with a cryptographic proof reference. The legacy {"verified": ..., "audit_trace": ...} dict is unchanged — to_diagnostic() is additive, so existing callers keep working.
Use it when you need a single, uniform shape across guards (for API responses, gating logic, or audit pipelines) instead of branching on guard-specific keys.
The three layers
Status states
TaxDiagnosticStatus is a strict tri-state:
VERIFIED— the tax decision was deterministically proven.proof_refMUST be present. Downstream gates MAY admit for control flow.UNVERIFIABLE— the decision could not be proven (insufficient evidence, computation-only mode, unknown rule).proof_refMUST beNone. Gates MUST NOT admit.BLOCKED— verification could not even be attempted (missing fields, parse error, unsupported service).proof_refMUST beNone. Gates MUST NOT admit.
developer_fields.constraint_id, not in the status.
Calling to_diagnostic()
TDSGuard, InputCreditGuard, and GSTGuard expose a to_diagnostic() static method that converts their existing dict result into a TaxDiagnosticResult.
InputCreditGuard.to_diagnostic() (ITC) and GSTGuard.to_diagnostic() (RCM).
Proof references
compute_proof_ref(evidence) returns a deterministic sha256:… hash over a JSON-serialized evidence dict. trace_proof_ref(trace) is a convenience wrapper for the output of build_trace(). Both fail closed if the evidence is not JSON-serializable.
Constructing results directly
For custom guards or wrapper code, use the factory methods rather than the raw constructor:Advisory checks
TaxAdvisoryCheck attaches non-proof-bearing analysis as metadata. The advisory_only=True invariant is enforced in __post_init__ — advisory checks populate developer_fields["advisory_checks"] and never influence status or proof_ref. Use them to surface useful context (e.g. “supplier GSTIN appears inactive”) without making it part of the verdict.
Serialization
TaxDiagnosticResult is frozen and provides to_dict() / from_dict() for API responses. to_dict() includes a flat is_authoritative boolean for clients that don’t want to inspect proof_ref directly.
Migration status
to_diagnostic() is currently available on TDSGuard, InputCreditGuard, and GSTGuard — the three guards that already emit audit_trace. The remaining guards still return their legacy dict shapes; subsequent releases will extend to_diagnostic() coverage.
United States (IRS)
ClassificationGuard (IRS common law)
Goal: Prevent “Employee Misclassification” lawsuits. Logic: Uses the IRS Common Law test to determine if a worker is a W-2 Employee or 1099 Contractor.- Behavioral Control: Does the employer provide tools/instructions?
- Financial Control: Does the employer reimburse expenses?
- Relationship: Is it indefinite?
Fails closed on ambiguous facts.
verify_worker_status returns WorkerType.CONTRACTOR only when no employee indicators are present. When some — but not all — of behavioral_control, financial_control, and relationship_permanence are true, it returns None, and verify_classification_claim then returns {"verified": False, "error": "Ambiguous classification: facts contain mixed employee/contractor indicators. Cannot deterministically classify — manual review required."}. The previous default-to-contractor path on mixed signals has been removed.ClassificationGuard (ABC Test / Z3)
Goal: Formal verification of worker classification under state-specific ABC Test laws (CA AB5, NJ, MA). Uses the Z3 theorem prover to prove classification correctness. Rule: A worker is a contractor only if all three criteria are met:- A: Free from control and direction
- B: Work is outside the usual course of business
- C: Customarily engaged in an independent trade
NexusGuard (economic nexus)
Goal: Prevent Sales Tax Evasion. Logic: Checks local state thresholds for 2025.- NY/TX/CA: > $500,000 Sales
- FL/IL/PA: > $100,000 Sales
Fails closed on unmodeled states. A call with a
state code that is not in the threshold table returns {"verified": False, "error": "State <CODE> not in configured nexus threshold table. Cannot verify nexus liability — block pending rule configuration."}. The guard never falls back to “not high-risk → no tax” for jurisdictions it has not been configured for.PayrollGuard (FICA limits)
Goal: Verify paycheck math. Logic:- Gross-to-Net:
Gross - Taxes - Deductions == Net(Exact Decimal match). - Social Security Cap: Enforces 2025 Wage Base Limit ($176,100). Tax stops after this amount.
WithholdingGuard (W-4 / Z3)
Goal: Verify W-4 exempt status claims using the Z3 theorem prover. Logic: An employee can only claim “Exempt” if they had zero tax liability last year AND expect no liability this year (IRS Pub 505).ReciprocityGuard (State tax)
Goal: Determine which state receives income tax withholding when an employee lives in one state and works in another. The guard is a deterministic lookup against a fixed table of reciprocity agreements — it fails closed whenever the agreement, or either state, is not recognized. Covered reciprocity pairs: NJ-PA, PA-NJ, MD-PA, PA-MD, VA-MD, MD-VA.The pair table is limited to the states modeled in the
State enum (NY, NJ, CA, TX, PA, FL, MD, VA). Pennsylvania has additional reciprocity agreements (with IN, MI, OH, VA, WV, WI) that are not modeled here because those states are not in the enum. VA-PA is a known gap — callers will receive verified=False for that pair until the enum and table are extended.
Both methods return the same result shape:
Evaluation order: The
same_state conflict check runs before the same-state and reciprocity lookups. If a caller passes same_state=True with different states (or same_state=False with identical states), the guard returns verified=False immediately — the same-state and reciprocity branches are never reached. Pass same_state=None (the default) to skip the conflict check and let the guard evaluate states on their own.ReciprocityGuard no longer uses a Z3 solver. A prior implementation built a Z3 expression that was tautologically satisfiable regardless of the input states, causing the guard to return
verified=True for arrangements that had no reciprocity agreement. The current implementation is a deterministic lookup against the table above — same input, same output, no solver state.AddressGuard
Goal: Verify that zip codes match their claimed state. Uses a simplified heuristic lookup of zip code prefixes.Fails closed on unmodeled states. States that are not in the simplified zip-prefix table (currently CA, FL, NJ, NY, PA, TX) return
{"verified": False, "message": "State <CODE> not in validation database. Address cannot be auto-verified — manual review required."}. The guard never assumes an unknown state is valid.Form1099Guard
Goal: Verify IRS 1099 filing requirements for contractor payments. Logic:- Checks payment amount against filing thresholds by payment type.
- Determines which form is required (1099-NEC or 1099-MISC).
Fails closed on unmodeled payment types. Payment types without a defined filing rule return
{"filing_required": "UNVERIFIABLE", "form": None, "reason": "No filing rule configured for payment type '<TYPE>'. Cannot verify filing requirement — manual determination required."}. Treat any value other than True or False as a hold-and-escalate signal — the guard will not silently report filing_required=False for a payment category it has not been configured to evaluate.India (CBDT)
CryptoTaxGuard (Sec 115BBH)
Rule: Losses from Virtual Digital Assets (VDA) cannot be set off against any other income (including other VDA gains). Two verification methods:verify_set_off()— Blocks any AI attempt to reduce tax liability using crypto losses.verify_flat_tax_rate()— Verifies the strict 30% flat tax on positive VDA income.
Negative VDA income is a loss, not “no income”.
verify_flat_tax_rate distinguishes three cases:vda_income == 0— verifiesclaimed_tax == 0. Any non-zero claim returnsverified=False.vda_income < 0— returnsverified=Falsewith the message"VDA income is negative (<X>) — this is a loss, not income. Use verify_set_off for loss treatment."Losses must be routed toverify_set_off(and under Section 115BBH, they lapse rather than offsetting other heads).vda_income > 0— verifies the 30% flat tax.
verified=True for any vda_income <= 0 and ignoring claimed_tax — has been removed.Comparison is exact at the paise (1/100) level. Both
expected_tax (vda_income * 0.30) and claimed_tax are quantized to two decimal places using ROUND_HALF_UP before an exact == comparison. A 1-paise deviation now returns verified=False. There is no Decimal("0.1") rounding tolerance — callers must round their claim to two decimal places with ROUND_HALF_UP to match.GSTGuard (RCM)
Rule: Certain notified services require the Reverse Charge Mechanism (the recipient pays the tax instead of the provider). RCM applicability is expressed as a declarative rule table — one entry per notified service, each carrying its predicate and statutory reference.
Any service/recipient combination outside these rules resolves to forward charge (the provider pays).
verify_rcm_applicability runs in two modes:
- Verification mode (recommended) — pass
claimed_is_rcmand the guard compares the computed RCM liability against the claim.verified=Trueonly on exact match. - Calculation mode (backward compatible) — omit
claimed_is_rcmand the guard returns the computed result withcomputed_only=Trueto signal that no claim was checked.
verify_rcm_applicability accepts either enum members or their raw string values (e.g. "LEGAL", "BODY_CORPORATE"), so JSON-sourced payloads work without pre-conversion.Fails closed on unknown service/entity values. Service names outside
ServiceType and entity values outside EntityType now return {"verified": False, "error": "Unknown service type '<X>'. Cannot determine RCM applicability.", "is_rcm": None} (and equivalents for provider/recipient). The previous behavior of silently coercing unknown services to OTHER and unknown entities to INDIVIDUAL — which could suppress a statutory RCM liability — has been removed. Normalize inputs to a known enum value before calling, or treat the error as a hold-and-escalate signal.GSTGuard (CGST/SGST/IGST split)
Goal: Verify that a claimed tax breakup matches the place of supply. This verifies the split — the GST rate is an input, not something the guard derives.- Intra-state (supplier state == place of supply):
CGST = SGST = value × rate / 200, andIGSTmust be0. - Inter-state (supplier state != place of supply):
IGST = value × rate / 100, andCGST/SGSTmust be0.
The guard fails closed: missing states, non-finite values, or negative amounts return
{"verified": False, "error": "..."} rather than raising.InvestmentGuard (Trading / Z3)
Goal: Classify stock market income into the correct tax head using the Z3 theorem prover.- Intraday = Speculative Business Income (Slab Rate, Sec 43(5))
- Delivery = Capital Gains (STCG/LTCG based on holding period)
- F&O = Non-Speculative Business Income
InterHeadAdjustmentGuard (Set-off matrix)
Goal: Enforce the full inter-head set-off rules for Indian income tax. Uses a prohibition matrix to block illegal loss adjustments.
Heads with no inter-head restrictions per the Income Tax Act —
HOUSE_PROPERTY, BUSINESS_NON_SPECULATIVE, and OTHER_SOURCES — are on an explicit allowlist and always return verified=True.
Fails closed on unknown heads and blocks SALARY losses. Loss heads that are neither in the prohibition matrix nor on the explicit allowlist return
{"verified": False, "message": "Loss head <X> is not in the configured prohibition matrix or allowlist. Cannot verify set-off legality — manual review required."}. TaxHead.SALARY is now in the prohibition matrix with ["ALL"] — agents that try to set off a salary loss against any profit head are blocked. The previous “default allow” path for any head not in the matrix has been removed.DepositRateGuard
Goal: Verify bank deposit interest rates, specifically senior citizen premiums (60+).SpeculationGuard
Rule: Intraday (Speculative) losses can only be set off against Intraday (Speculative) profits. They cannot reduce F&O or Delivery income. Losses must be carried forward for up to 4 years.verify_setoff classifies the loss and profit sources against a fixed vocabulary:
- Speculative:
intraday - Non-speculative:
f&o,f_o,futures,options,delivery,business,capital_gains
Fails closed on unknown source strings. The guard no longer relies on substring matching (the previous
"intraday" in source check treated everything else as non-speculative). Source names outside the known vocabulary return {"verified": False, "error": "Unrecognized loss source '<X>'. Known sources: ..."} with a fix hint. Normalize agent output to one of the recognized names before calling.CapitalGainsGuard
Goal: Classify assets as STCG or LTCG based on holding period and verify the statutory tax rate.debt_fund always classifies as STCG. determine_term returns "STCG" for asset_type="debt_fund" regardless of holding period, reflecting the Budget 2023 amendment. However, verify_tax_rate does not have a configured rate for debt_fund_STCG — it will return {"verified": False, "error": "No statutory rate configured for debt_fund_STCG. Cannot verify claimed rate."}. Callers must handle this fail-closed response and verify the slab rate against the taxpayer’s bracket through a separate path.Fails closed on unmodeled asset/term pairs.
verify_tax_rate returns {"verified": False, "error": "No statutory rate configured for <asset>_<term>. Cannot verify claimed rate."} when the (asset_type, term) combination is not in the rate table. The previous “no hard constraint, assume verified” path has been removed — the guard no longer signs off on rates it cannot independently check.Slab-rated assets cannot be verified without the taxpayer’s bracket. When the rate table resolves to
SLAB (e.g. debt_LTCG, debt_STCG), verify_tax_rate returns {"verified": False, "error": "Rate for <key> is subject to slab rates — cannot deterministically verify claimed rate of <X>. Taxpayer's slab band is required for verification."}. Slab rates depend on the filer’s total income bracket and are out of scope for the rate-table check.determine_term raises ValueError on unparseable dates and unknown asset types. Pass purchase_date and sale_date as YYYY-MM-DD strings and an asset_type of equity, real_estate, debt, or debt_fund. Anything else raises ValueError — the previous behavior of returning an "ERROR_DATE_FORMAT" sentinel (which then flowed to verified=True upstream) or fabricating a 1095-day threshold for unknown assets has been removed. Callers should catch ValueError and block. TaxPreFlight._check_capital_gains now does this and produces a structured block with the error message.Accounts payable guards (indirect tax)
Goal: Automate GST/VAT and TDS compliance. Logic:- InputCreditGuard: Checks Section 17(5) “Blocked List”.
- Food/Beverages: Blocked.
- Motor Vehicles: Blocked (unless transport biz).
- Gift to Employee: Blocked only above INR 50,000. Gifts below the threshold are ITC-eligible.
- InputCreditGuard.verify_gstin_format: Validates a GSTIN’s structure and its 15th-digit checksum (base-36 GSTN algorithm). A string that matches the format but carries an incorrect check digit is rejected with a generic
"Invalid GSTIN checksum."error (the correct digit is never echoed back). - TDSGuard: Calculates withholding based on service type.
- Professional Fees: 10% (Sec 194J).
- Contractors: 1% or 2% (Sec 194C).
- Commission: 5% (Sec 194H).
- Rent (Land): 10% (Sec 194I).
Category matching for
InputCreditGuard uses exact match (not substring). Ensure you pass the canonical category name (e.g., FOOD_AND_BEVERAGE, MOTOR_VEHICLE, GIFT_TO_EMPLOYEE).InputCreditGuard flags default-allow categories. ITC is “allowed unless specifically blocked” under GST law, so unknown categories still return
verified=True — but the response now also carries "unverified_category": True and audit_trace.inputs.category_match: "default_allow". Consumers that need to distinguish “explicitly eligible” from “allowed by default” should branch on unverified_category before posting the credit.CorporateGuard (Loans and valuation)
Goal: Corporate Governance (Sec 185/Valuation). Logic:- RelatedPartyGuard: Prohibits loans to Directors/Relatives/Holding-Co-Directors unless specific exemptions apply. Also enforces interest rate benchmarking under Section 186.
- ValuationGuard: Deterministically calculates Convertible Note conversion prices (
min(Cap, Discount)).
DIRECTOR, DIRECTOR_RELATIVE, PARTNER, PARTNER_OF_DIRECTOR, HOLDING_COMPANY_DIRECTOR.
ValuationGuard.verify_conversion
Fails closed on edge-case inputs. The guard returns
{"verified": False, "error": "..."} when:- Any input fails to parse as a
Decimal("Invalid numerical input for valuation."). discountis outside[0, 1)("Discount must be between 0 and 1."). A discount of exactly1is rejected because it would force the discounted price to zero; values above1would otherwise produce negative shares.cap,next_round_price, orinvestmentis zero or negative ("Cap, next round price, and investment must be positive.").- The final share price resolves to zero through any other path (
"Final price resolved to zero — cannot compute shares.").
verified=False branch explicitly — there is no exception to catch.International
DTAAGuard (foreign tax credit)
Goal: Verify Foreign Tax Credit (FTC) eligibility under Double Taxation Avoidance Agreements. Logic:- Basic Credit: Allowable credit = min(Foreign Tax Paid, Home Tax Payable on foreign income).
- Treaty Rate Limit: When a DTAA treaty rate is provided, the credit is further capped at the treaty-limited amount.
- Excess Lapsed: Any foreign tax paid above the allowable credit is reported as lapsed.
allowable_credit, excess_tax_lapsed) are returned as stable plain-string Decimals rather than floats to preserve exact precision across serialization boundaries.
Response fields:
The guard fails closed: any non-numeric, non-finite (NaN / Infinity), boolean, or negative input returns
verified: False with a descriptive message and zeroed credit fields rather than raising an exception.TransferPricingGuard (arm’s length price)
Goal: Verify that related-party transactions are priced within an acceptable range of the Arm’s Length Price (ALP). Ref: OECD Guidelines, US Sec 482, India Sec 92C. Monetary inputs acceptDecimal, str, int, or float. Outputs (safe_harbour_range, potential_adjustment, adjustment_required) are returned as stable plain-string Decimals.
On any non-numeric, non-finite, or boolean input the guard fails closed and returns
{"verified": False, "risk": "INVALID_NUMERIC_INPUT", "message": "...", "safe_harbour_range": [], "potential_adjustment": "0"}.PoEMGuard (place of effective management)
Goal: Determine tax residency of foreign companies under CBDT Circular 6 of 2017 and OECD models. Logic: A foreign company is treated as Indian Resident if it fails the Active Business Outside India (ABOI) test AND its key management is in India. ABOI test criteria (all must be true to pass):- Assets outside India >= 50%
- Employees outside India >= 50%
- Payroll outside India >= 50%
Response fields:
The
metrics object returns each ratio as a stable plain-string Decimal rounded to four decimal places (ROUND_HALF_UP). The underlying unrounded ratios are used for the ABOI 50% threshold comparison.
The guard fails closed with
{"verified": False, "residency": "UNVERIFIABLE", "reason": "..."} when any numeric input is non-numeric, non-finite, boolean, or negative, or when an “outside India” component exceeds its total (assets, employees, or payroll).RemittanceGuard (FEMA/LRS)
Goal: Prevent forex violations. Logic:- LRS Limit: Enforces $250,000 annual limit per PAN.
- Prohibited List: Blocks Gambling, Lottery, Racing, and Margin Trading.
- TCS Logic: Applies 20% tax for generic remittance, 5% for Education/Medical. Education with loan funding is 0.5%. Threshold exemption of INR 7,00,000.
verify_lrs_limit accepts Decimal, str, int, or float for amount_usd and financial_year_usage. Non-numeric, non-finite, or boolean inputs fail closed with {"verified": False, "error": "BLOCKED: ..."}. Negative values for either parameter are also rejected ("BLOCKED: Remittance amount must be non-negative." / "BLOCKED: Financial year usage must be non-negative.") so a negative usage can’t push a real transaction back under the $250,000 LRS cap. calculate_tcs returns a Decimal and raises ValueError on invalid numeric input.