Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions tax/guards.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,117 @@

Guards currently emitting `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

| Layer | Field | Purpose |
| --- | --- | --- |
| 1. Agent-safe | `agent_message: str` | Short, model-facing summary. No statute IDs, no rule IDs, no detection logic — safe to feed back to an LLM for correction. |
| 2. Developer | `developer_fields: dict` | Structured evidence: `constraint_id`, `statute`, `jurisdiction`, `audit_trace`, plus guard-specific fields like `deduction`, `net_payable`, `allowable_credit`. |
| 3. Proof | `proof_ref: Optional[str]` | `sha256:…` hash of the retained proof artifact. Present **only** when `status == VERIFIED`. This is the authority bit. |

### Status states

`TaxDiagnosticStatus` is a strict tri-state:

- **`VERIFIED`** — the tax decision was deterministically proven. `proof_ref` MUST be present. Downstream gates MAY admit for control flow.
- **`UNVERIFIABLE`** — the decision could not be proven (insufficient evidence, computation-only mode, unknown rule). `proof_ref` MUST be `None`. Gates MUST NOT admit.
- **`BLOCKED`** — verification could not even be attempted (missing fields, parse error, unsupported service). `proof_ref` MUST be `None`. Gates MUST NOT admit.

Richer distinctions (e.g. "below threshold" vs. "unknown service") live in `developer_fields.constraint_id`, not in the status.

<Warning>
**Authority contract.** `proof_ref is not None` is the **only** signal that a verdict is admissible for control flow. A `VERIFIED` status without a `proof_ref` is structurally impossible — the dataclass raises in `__post_init__`. Do not infer authority from any other field.
</Warning>

### Calling `to_diagnostic()`

`TDSGuard`, `InputCreditGuard`, and `GSTGuard` expose a `to_diagnostic()` static method that converts their existing dict result into a `TaxDiagnosticResult`.

```python
from qwed_tax.guards.tds_guard import TDSGuard

Check warning on line 63 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L63

Did you really mean 'TDSGuard'?
from qwed_tax.diagnostics import TaxDiagnosticStatus

guard = TDSGuard()
raw = guard.calculate_deduction(service="professional_fees", amount=50_000)

diag = TDSGuard.to_diagnostic(raw)

if diag.status is TaxDiagnosticStatus.VERIFIED:
# diag.proof_ref is guaranteed non-None here
submit_payment(net_payable=diag.developer_fields["net_payable"])
else:
# diag.proof_ref is None — never admit for control flow
escalate(diag.agent_message, constraint_id=diag.developer_fields["constraint_id"])
```

The same shape applies to `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.

```python
from qwed_tax.audit import build_trace, trace_proof_ref, TDS_194J

trace = build_trace(TDS_194J, outcome="DEDUCTION_REQUIRED", inputs={"amount": "50000"})
proof = trace_proof_ref(trace)
# "sha256:9f4c…"
```

The proof reference binds a verdict to the exact evidence that justified it. If any input, rule, or outcome changes, the hash changes — making verdict/evidence drift structurally detectable in downstream audit logs.

### Constructing results directly

For custom guards or wrapper code, use the factory methods rather than the raw constructor:

```python
from qwed_tax.diagnostics import TaxDiagnosticResult

# VERIFIED — proof_ref is computed from evidence
from qwed_tax.audit import build_trace, TDS_194J
trace = build_trace(TDS_194J, outcome="DEDUCTION_REQUIRED", inputs={"amount": "50000"})
diag = TaxDiagnosticResult.verified(
agent_message="Tax deduction verified.",
developer_fields={"constraint_id": "TDS_194J", "deduction": "5000"},
evidence=trace,
)
Comment thread
greptile-apps[bot] marked this conversation as resolved.

# UNVERIFIABLE — no proof was established
diag = TaxDiagnosticResult.unverifiable(
agent_message="Amount is below the deduction threshold; no TDS required.",
developer_fields={"constraint_id": "TDS_194J_BELOW_THRESHOLD"},
)

# BLOCKED — verification could not be attempted
diag = TaxDiagnosticResult.blocked(
agent_message="Unknown service type. Cannot determine deduction.",
developer_fields={"constraint_id": "TDS_UNKNOWN"},
)
```

### 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.

Check warning on line 134 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L134

Did you really mean 'TDSGuard'?

Check warning on line 134 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L134

Did you really mean 'GSTGuard'?

## 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.

Check warning on line 140 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L140

Did you really mean 'Misclassification'?

- **Behavioral Control:** Does the employer provide tools/instructions?
- **Financial Control:** Does the employer reimburse expenses?
Expand Down Expand Up @@ -113,7 +219,7 @@
| GA | \$100,000 | 200 |

<Note>
**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.

Check warning on line 222 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L222

Did you really mean 'unmodeled'?
</Note>

### PayrollGuard (FICA limits)
Expand Down Expand Up @@ -193,7 +299,7 @@
| `same_state` claim conflicts with the actual states | `False` | `message` flagging the conflict |

<Note>
**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.

Check warning on line 302 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L302

Did you really mean 'lookups'?
</Note>

```python
Expand Down Expand Up @@ -243,7 +349,7 @@
```

<Note>
**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.

Check warning on line 352 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L352

Did you really mean 'unmodeled'?
</Note>

### Form1099Guard
Expand Down Expand Up @@ -279,7 +385,7 @@
```

<Note>
**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.

Check warning on line 388 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L388

Did you really mean 'unmodeled'?
</Note>

## India (CBDT)
Expand All @@ -289,7 +395,7 @@
**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.

Check warning on line 398 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L398

Did you really mean 'crypto'?
- `verify_flat_tax_rate()` — Verifies the strict 30% flat tax on positive VDA income.

```python
Expand Down Expand Up @@ -324,11 +430,11 @@
</Note>

<Note>
**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.

Check warning on line 433 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L433

Did you really mean 'paise'?
</Note>


### GSTGuard (RCM)

Check warning on line 437 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L437

Did you really mean 'GSTGuard'?

**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.

Expand Down Expand Up @@ -397,14 +503,14 @@
**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.
</Note>

### GSTGuard (CGST/SGST/IGST split)

Check warning on line 506 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L506

Did you really mean 'GSTGuard'?

**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`, and `IGST` must be `0`.
- **Inter-state** (supplier state != place of supply): `IGST = value × rate / 100`, and `CGST`/`SGST` must be `0`.

A small rounding tolerance (2 paise) applies only to tax-carrying legs (CGST/SGST for intra-state supplies, IGST for inter-state supplies). Legs that must be exactly zero (the wrong tax type for the supply) get **no** tolerance, so even a tiny wrong-type amount is rejected. Negative claimed amounts fail closed.

Check warning on line 513 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L513

Did you really mean 'paise'?

```python
from qwed_tax.jurisdictions.india.guards.gst_guard import GSTGuard
Expand Down Expand Up @@ -443,7 +549,7 @@

**Goal:** Classify stock market income into the correct tax head using the Z3 theorem prover.

- **Intraday** = Speculative Business Income (Slab Rate, Sec 43(5))

Check warning on line 552 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L552

Did you really mean 'Intraday'?
- **Delivery** = Capital Gains (STCG/LTCG based on holding period)
- **F&O** = Non-Speculative Business Income

Expand All @@ -468,10 +574,10 @@
| Speculative Business | Only Speculative Business profit |
| Long-term Capital Gains | Only Long-term Capital Gains |
| Short-term Capital Gains | Short-term or Long-term Capital Gains |
| VDA (Crypto) | Nothing (lapses entirely) |

Check warning on line 577 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L577

Did you really mean 'Crypto'?
| Salary | Nothing (cannot generate a loss for inter-head set-off) |

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`.

Check warning on line 580 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L580

Did you really mean 'allowlist'?

```python
from qwed_tax.jurisdictions.india.guards.setoff_guard import InterHeadAdjustmentGuard, TaxHead
Expand All @@ -486,7 +592,7 @@
```

<Note>
**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.

Check warning on line 595 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L595

Did you really mean 'allowlist'?
</Note>

### DepositRateGuard
Expand Down Expand Up @@ -518,7 +624,7 @@

### 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.

Check warning on line 627 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L627

Did you really mean 'Intraday'?

Check warning on line 627 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L627

Did you really mean 'Intraday'?

`verify_setoff` classifies the loss and profit sources against a fixed vocabulary:

Expand All @@ -542,7 +648,7 @@
```

<Note>
**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.

Check warning on line 651 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L651

Did you really mean 'substring'?
</Note>

### CapitalGainsGuard
Expand All @@ -561,7 +667,7 @@
</Note>

<Note>
**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.

Check warning on line 670 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L670

Did you really mean 'unmodeled'?
</Note>

<Note>
Expand All @@ -569,7 +675,7 @@
</Note>

<Note>
**`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.

Check warning on line 678 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L678

Did you really mean 'unparseable'?
</Note>

### Accounts payable guards (indirect tax)
Expand All @@ -580,7 +686,7 @@
- _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).

Check warning on line 689 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L689

Did you really mean 'GSTIN's'?
- **TDSGuard:** Calculates withholding based on service type.
- _Professional Fees:_ 10% (Sec 194J).
- _Contractors:_ 1% or 2% (Sec 194C).
Expand All @@ -606,7 +712,7 @@
| Rent (Land) | 2,40,000 | 10% | 194I |

<Note>
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`).

Check warning on line 715 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L715

Did you really mean 'substring'?
</Note>

<Warning>
Expand Down Expand Up @@ -662,7 +768,7 @@

## International

### DTAAGuard (foreign tax credit)

Check warning on line 771 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L771

Did you really mean 'DTAAGuard'?

**Goal:** Verify Foreign Tax Credit (FTC) eligibility under Double Taxation Avoidance Agreements. **Logic:**

Expand Down Expand Up @@ -806,7 +912,7 @@

### RemittanceGuard (FEMA/LRS)

**Goal:** Prevent forex violations. **Logic:**

Check warning on line 915 in tax/guards.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwed-ai) - vale-spellcheck

tax/guards.mdx#L915

Did you really mean 'forex'?

- **LRS Limit:** Enforces \$250,000 annual limit per PAN.
- **Prohibited List:** Blocks Gambling, Lottery, Racing, and **Margin Trading**.
Expand Down