Skip to content

fix: fail-closed on ambiguous classification + claim comparison (#17, #18)#42

Merged
Rahul Dass (rahuldass19) merged 8 commits into
mainfrom
fix/issue-17-18-classification-and-claim-comparison
Jun 21, 2026
Merged

fix: fail-closed on ambiguous classification + claim comparison (#17, #18)#42
Rahul Dass (rahuldass19) merged 8 commits into
mainfrom
fix/issue-17-18-classification-and-claim-comparison

Conversation

@rahuldass19

@rahuldass19 Rahul Dass (rahuldass19) commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #17 (guessing classifications) and #18 (success without comparing claim) across 6 guards.

#17 — Guessing classifications instead of blocking

Guard File Before After
CapitalGainsGuard determine_term Bad dates → "ERROR_DATE_FORMAT" sentinel → flows to verified=True Raises ValueError — caller catches and blocks
CapitalGainsGuard determine_term Unknown asset type → fabricated 1095-day threshold Raises ValueError — known types only
CapitalGainsGuard verify_tax_rate SLAB verified=True ignoring claimed_rate entirely verified=False — slab rates can't be proven without income bracket
ClassificationGuard verify_worker_status Mixed signals (some employee indicators but not all) → default to CONTRACTOR Returns Noneverified=False with "ambiguous classification" error. Contractor only when NO employee indicators present
SpeculationGuard verify_setoff Substring "intraday" in source — unrecognized = non-speculative Known vocabulary required (intraday, f&o, futures, options, delivery, business, capital_gains) — unknown sources blocked
SetoffGuard verify_setoff "Default Allow" for any head not in prohibition matrix Allowlist for explicitly allowed heads + SALARY added to prohibition matrix + unknown heads blocked
GSTGuard RCM verify_rcm_applicability Unknown service coerced to OTHER, unknown entity to INDIVIDUAL — could suppress statutory RCM _try_coerce returns Noneverified=False with error naming the unrecognized value

#18 — Success without comparing claim to computed truth

Guard File Before After
GSTGuard RCM verify_rcm_applicability Always returns verified=True — calculator masquerading as verifier Optional claimed_is_rcm parameter — when provided, compares computed is_rcm against claim, returns verified=True only on exact match. When omitted, returns computed_only=True flag (backward compatible — separates calculation from verification)
CryptoTaxGuard verify_flat_tax_rate vda_income <= 0verified=True ignoring claimed_tax. Negative income (loss) treated as "no income" vda_income == 0 → verifies claimed_tax == 0. vda_income < 0verified=False (loss, not "no income" — route to set-off/lapse logic)

Caller updates

  • verifier.py _check_capital_gains — now catches ValueError from determine_term and produces a block with the error message

Test changes

  • test_rcm.py::test_unknown_string_service_is_forward_chargetest_unknown_string_service_fail_closed — asserts verified=False for unknown service (was asserting is_rcm=False, enforcing the fail-open behavior)

Cleanup

  • Removed unused imports in setoff_guard.py (List, Dict) and crypto_guard.py (List, Optional)
  • Removed pre-existing unused variables in crypto_guard.py verify_set_off (total_gains, non_vda_losses)

Test results

115 tests pass, 0 regressions. Ruff lint clean.

Summary by CodeRabbit

  • Bug Fixes

    • Improved fail-closed validation for capital gains date parsing/asset types, slab-rate verification, worker classification ambiguity, speculation exact-match sources, GST reverse-charge inputs, crypto zero/negative income and set-off, and inter-head set-off (including blocking salary loss set-off).
    • Capital-gains preflight now blocks when term determination fails instead of erroring.
  • Refactor

    • Tightened normalization and deterministic vs computation-only behavior for GST RCM and set-off eligibility checks.
  • Tests

    • Expanded coverage for ambiguous/unknown inputs, debt-fund/STCG handling, speculation exact-match, crypto edge cases, and updated assertions.

…18)

#17 — guessing classifications instead of blocking:
- CapitalGainsGuard.determine_term: raise ValueError on bad dates
  and unknown asset types (was returning ERROR_DATE_FORMAT sentinel
  that silently flowed to verified=True)
- CapitalGainsGuard SLAB rates: return verified=False — slab rates
  cannot be deterministically verified without taxpayer's income bracket
- SpeculationGuard: require known vocabulary (intraday, f&o, delivery,
  etc.) — reject unrecognized source strings instead of substring guess
- SetoffGuard: add allowlist for explicitly allowed loss heads, add
  SALARY to prohibition matrix (salary losses cannot be set off
  inter-head), block heads not in prohibition matrix or allowlist
- GSTGuard RCM: fail-closed on unknown service/entity — no silent
  coercion to OTHER/INDIVIDUAL (was suppressing statutory RCM)

#18 — success without comparing claim to computed truth:
- GSTGuard RCM: add optional claimed_is_rcm parameter — when provided,
  compare computed is_rcm against claim and return verified=True only
  on exact match. When omitted, return computed_only=True flag
  (backward compatible, separates calculation from verification)
- CryptoTaxGuard: distinguish vda_income==0 (verify claimed_tax==0)
  from vda_income<0 (loss — route to set-off, not 'no income')

Test updated: test_rcm.py test_unknown_string_service_is_forward_charge
→ test_unknown_string_service_fail_closed (asserts verified=False)

Also cleaned: unused imports in setoff_guard.py and crypto_guard.py,
pre-existing unused variables in crypto_guard.py verify_set_off

115 tests pass, 0 regressions. Ruff lint clean.
- verify_worker_status: return None for mixed signals (some but not
  all employee indicators) instead of defaulting to CONTRACTOR
- verify_classification_claim: handle None → verified=False with
  'ambiguous classification' error
- Contractor only returned when NO employee indicators are present

Fixes the original #17 finding: 'Worker classification defaults to
contractor in mixed-signal cases' — e.g. provides_tools=False,
reimburses_expenses=True, indefinite_relationship=True was collapsing
to CONTRACTOR despite two employee indicators being present.

115 tests pass.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5661f546-65fa-4775-82a4-32f40b42d00b

📥 Commits

Reviewing files that changed from the base of the PR and between 22319be and 521127f.

📒 Files selected for processing (2)
  • examples/demo_audit.py
  • qwed_tax/jurisdictions/india/guards/gst_guard.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • qwed_tax/jurisdictions/india/guards/gst_guard.py

📝 Walkthrough

Walkthrough

Multiple tax guard classes are hardened to fail closed: unknown or ambiguous inputs now raise ValueError or return verified: False with explicit errors instead of defaulting to success-like outcomes. Changes span capital gains, worker classification, speculation, inter-head setoff, crypto flat tax, and GST RCM guards. GSTGuard.verify_rcm_applicability gains a dual verification/calculation mode via a new optional claimed_is_rcm parameter. Test coverage comprehensively validates fail-closed behavior across all guards.

Changes

Fail-closed guard hardening

Layer / File(s) Summary
Capital gains: ValueError raises and slab-rate fix
qwed_tax/guards/capital_gains_guard.py, qwed_tax/verifier.py
determine_term raises ValueError for invalid dates and unknown asset types; verify_tax_rate returns verified: False for SLAB-rate claims; verifier.py wraps determine_term with try/except ValueError to block propagation.
Worker classification: ambiguity detection
qwed_tax/guards/classification_guard.py
verify_worker_status counts mixed employee indicators and returns None for inconclusive cases; verify_classification_claim short-circuits on None with a verified: False ambiguity error and adds fail-closed validation that llm_claim is a non-empty string before claim normalization.
Speculation guard: source vocabulary and classification helper
qwed_tax/guards/speculation_guard.py
Explicit speculative/non-speculative vocabulary sets added; _classify_source classmethod maps source strings to a category or "unknown"; verify_setoff rejects unrecognized sources with descriptive errors and fix instructions instead of substring matching.
Inter-head setoff: SALARY prohibition and explicit allowlist
qwed_tax/jurisdictions/india/guards/setoff_guard.py
PROHIBITED_SETOFFS gains TaxHead.SALARY → ["ALL"]; _EXPLICITLY_ALLOWED_LOSS_HEADS allowlist introduced; verify_setoff returns verified: False with a manual-review message when a loss head is absent from both structures.
Crypto flat tax: zero and negative income edge cases and optional gains
qwed_tax/jurisdictions/india/guards/crypto_guard.py, examples/demo_audit.py
verify_set_off gains optional gains parameter and fails closed when gains is provided; verify_flat_tax_rate now explicitly handles vda_income == 0 (verify only when claimed_tax == 0) and vda_income < 0 (reject as loss, route to verify_set_off); demo example updated to call verify_set_off with only losses.
GST RCM: dual verification/calculation mode and fail-closed coercion
qwed_tax/jurisdictions/india/guards/gst_guard.py
verify_rcm_applicability gains optional claimed_is_rcm parameter; _try_coerce helper added for fail-closed enum validation; verification mode compares computed vs claimed is_rcm and returns audit_trace; calculation mode returns computed_only: True with verified: False indicating deterministic verification requires the claim.
Test coverage: fail-closed behavior validation
tests/test_rcm.py, tests/test_guards_coverage.py
New test classes validate classification ambiguity blocking, capital gains debt-fund STCG/unknown asset/SLAB/invalid date handling, speculation exact-match classification and unknown source rejection, crypto zero vs negative income, setoff allowlist restrictions; RCM tests assert fail-closed enum coercion, verification-mode claim matching, and computation-mode separation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #17 — [Bug]: Legal and tax classifications are guessed instead of blocked when facts are ambiguous: This PR directly addresses all representative problems cited in the issue—mixed-signal worker classification now returns None instead of defaulting to contractor, unknown asset types raise ValueError instead of using a fallback threshold, and the slab-rate path now returns verified: False instead of a success-like result.

Possibly related PRs

  • QWED-AI/qwed-tax#26: Both PRs modify SpeculationGuard.verify_setoff in qwed_tax/guards/speculation_guard.py—this PR changes source classification/rule enforcement, while the retrieved PR adds Decimal-safe parsing for loss_amount—so they overlap at the same function/code path.
  • QWED-AI/qwed-tax#41: Both PRs change CapitalGainsGuard.verify_tax_rate to fail-closed for unhandled/missing rate configurations (this PR updates SLAB verification to return verified: False, while the retrieved PR makes "no statutory rate configured" return verified: False).

Suggested labels

bug

Poem

🐇 No more guessing games at tax time,
The guards say "unknown" — that's not a crime!
Mixed signals? Blocked. Bad dates? Raised high.
SLAB rates unproven? We wave them goodbye.
Fail closed, hop safe — the rabbit knows why. 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main fix: implementing fail-closed behavior for ambiguous classification and unverified claims across the guards.
Linked Issues check ✅ Passed The PR successfully addresses Issue #17 objectives by blocking ambiguous classifications, removing defaults for unknown asset types, preventing slab verification, and adding validation for worker status, capital gains, and speculation sources.
Out of Scope Changes check ✅ Passed All changes are directly aligned with Issue #17 requirements: error handling for ambiguous cases, vocabulary validation, loss-head allowlisting, and fail-closed behavior on unknown inputs.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-17-18-classification-and-claim-comparison

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown

Greptile Summary

This PR hardens six tax guards against fail-open behavior: bad dates and unknown asset types now raise ValueError, slab rates return verified=False, mixed worker-classification signals return None, unrecognized speculation sources are blocked by an exact-match vocabulary, GST RCM gains a dual verification/computation mode with fail-closed coercion, and the inter-head setoff guard replaces its "default allow" path with an explicit allowlist.

  • crypto_guard.py: verify_set_off now accepts gains as optional and immediately returns verified=False when it is non-empty — but the qwed-open-responses TaxGuard always passes gains through, so any tool call that includes gain data is now permanently blocked rather than computed.
  • speculation_guard.py: Substring matching replaced by set membership after normalization; _KNOWN_NON_SPECULATIVE contains \"capital gains\" (with a space) which is unreachable after normalization converts spaces to underscores — the entry is dead code.
  • gst_guard.py: _build_calculation_response returns verified=False (previously True) for callers that omit claimed_is_rcm; callers relying on verified being truthy for the computation-only path are now broken.

Confidence Score: 3/5

Not safe to merge without updating the qwed-open-responses TaxGuard to drop gains from its verify_set_off call, or the behavior change will silently break all crypto gain+loss tool calls in production.

The fail-closed improvements to classification, speculation, and capital gains are well-reasoned and thoroughly tested. The critical gap is that crypto_guard.verify_set_off now returns verified=False for any non-empty gains dict, but the downstream qwed-open-responses TaxGuard unconditionally passes gains through — turning a working code path into a permanent block with no visible error to the caller beyond "not implemented." The demo_audit.py example was updated to drop gains, but that downstream consumer was not.

qwed_tax/jurisdictions/india/guards/crypto_guard.py and the unupdated qwed-open-responses/src/qwed_open_responses/guards/tax_guard.py (related repo) need the most attention.

Important Files Changed

Filename Overview
qwed_tax/jurisdictions/india/guards/crypto_guard.py Zero/negative income distinction is correct; fail-closed on non-empty gains breaks the qwed-open-responses downstream consumer that always passes gains through
qwed_tax/guards/capital_gains_guard.py Correctly raises ValueError for bad dates/unknown types and hard-codes STCG for debt_fund; rates dict still missing debt_fund_STCG (previously flagged P1 not fixed by this PR)
qwed_tax/jurisdictions/india/guards/gst_guard.py _try_coerce correctly blocks unknown enums; dual-mode verification/calculation split is well-structured; claimed_is_rcm identity comparison (is vs ==) is safe since both values are guaranteed bools
qwed_tax/guards/speculation_guard.py Exact-match vocabulary approach correctly blocks unknown sources; _classify_source normalizes consistently; "capital gains" (with space) in the set is unreachable dead code after normalization
qwed_tax/jurisdictions/india/guards/setoff_guard.py SALARY prohibition matrix entry and explicit allowlist both correct; early-return refactor in verify_setoff is logically equivalent and eliminates the former default-allow path
qwed_tax/guards/classification_guard.py Mixed-signal detection and None return for ambiguous classification is clean; return type annotation correctly updated to Optional[WorkerType]; non-string claim guard added
qwed_tax/verifier.py ValueError catch in _check_capital_gains correctly blocks on bad date/asset-type; verify_india_crypto correctly forwards gains so the guard's fail-closed check fires

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[verify_set_off called] --> B{gains provided\nand non-empty?}
    B -- Yes --> C[verified=False\n'not implemented']
    B -- No --> D{VDA loss\npresent?}
    D -- Yes --> E[verified=False\nSection 115BBH]
    D -- No --> F[verified=True\nAllowed]

    G[qwed-open-responses\nTaxGuard._verify_crypto_tax] --> H[verify_set_off\nlosses=...\ngains=args.get gains or empty]
    H --> A

    style C fill:#ff6b6b,color:#fff
    style E fill:#ff6b6b,color:#fff
    style F fill:#51cf66,color:#fff
    style C stroke:#c92a2a
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[verify_set_off called] --> B{gains provided\nand non-empty?}
    B -- Yes --> C[verified=False\n'not implemented']
    B -- No --> D{VDA loss\npresent?}
    D -- Yes --> E[verified=False\nSection 115BBH]
    D -- No --> F[verified=True\nAllowed]

    G[qwed-open-responses\nTaxGuard._verify_crypto_tax] --> H[verify_set_off\nlosses=...\ngains=args.get gains or empty]
    H --> A

    style C fill:#ff6b6b,color:#fff
    style E fill:#ff6b6b,color:#fff
    style F fill:#51cf66,color:#fff
    style C stroke:#c92a2a
Loading

Comments Outside Diff (1)

  1. qwed_tax/jurisdictions/india/guards/crypto_guard.py, line 279-284 (link)

    P1 Downstream consumer breaks when gains is non-empty

    The qwed-open-responses TaxGuard always passes gains through to verify_set_off:

    result = guard.verify_set_off(
        losses=arguments.get("losses") or {},
        gains=arguments.get("gains") or {},
    )

    With this PR, any tool call that includes a non-empty gains payload now returns verified=False with "not implemented," which _check_result surfaces as a fail_result. Before this PR the guard attempted to compute a result; now it silently rejects. The demo_audit.py was updated to drop gains, but the downstream consumer was not — leaving callers that supply both losses and gains permanently blocked on what was a working code path.

Reviews (7): Last reviewed commit: "fix: pass gains through to verify_set_of..." | Re-trigger Greptile

Comment thread qwed_tax/jurisdictions/india/guards/gst_guard.py Outdated
Comment thread qwed_tax/guards/capital_gains_guard.py Outdated
Comment thread qwed_tax/guards/speculation_guard.py
Comment thread qwed_tax/guards/speculation_guard.py Outdated
Comment thread qwed_tax/guards/capital_gains_guard.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
qwed_tax/guards/classification_guard.py (1)

14-24: ⚠️ Potential issue | 🟠 Major

Update return type annotation to Optional[WorkerType] to reflect that the function returns None for ambiguous cases.

Line 44 returns None when mixed signals are detected, but the signature declares -> WorkerType. This breaks the type contract and prevents static type checkers from validating the None check at line 60. Add Optional to the imports and update the return type annotation.

Proposed fix
-from typing import Dict, Any
+from typing import Dict, Any, Optional
@@
-    def verify_worker_status(self, behavioral_control: bool, financial_control: bool, relationship_permanence: bool) -> WorkerType:
+    def verify_worker_status(
+        self,
+        behavioral_control: bool,
+        financial_control: bool,
+        relationship_permanence: bool,
+    ) -> Optional[WorkerType]:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qwed_tax/guards/classification_guard.py` around lines 14 - 24, The
verify_worker_status method has a return type annotation of WorkerType but the
docstring and implementation indicate it can return None when mixed signals are
detected. Update the return type annotation from WorkerType to
Optional[WorkerType] by importing Optional from the typing module (if not
already imported) and modifying the function signature of verify_worker_status
to declare -> Optional[WorkerType]. This will align the type signature with the
actual behavior where None is returned for ambiguous cases.

Source: Linters/SAST tools

🧹 Nitpick comments (1)
tests/test_rcm.py (1)

145-150: ⚡ Quick win

Add tests for dual-mode contract (claimed_is_rcm vs computed_only).

Good fail-closed test for unknown service. Please also add explicit assertions for verification mode (match/mismatch) and computation mode (computed_only behavior), since that contract is security-critical in this PR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_rcm.py` around lines 145 - 150, The current
test_unknown_string_service_fail_closed method only covers the fail-closed
behavior for unknown service types, but does not test the security-critical
dual-mode contract behavior. Add additional test methods that explicitly verify
the verification mode behavior (testing both matching and mismatching scenarios
between claimed_is_rcm and computed RCM status) and the computed_only mode
behavior where verification is bypassed. Include explicit assertions for these
modes in the guard.verify_rcm_applicability call to ensure the contract between
claimed_is_rcm and computed_only parameters is properly enforced and tested.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@qwed_tax/guards/capital_gains_guard.py`:
- Around line 15-20: The try-except block in _check_capital_gains only catches
ValueError, but other exceptions like TypeError (from strptime receiving
non-string dates) and AttributeError (from calling .lower() on non-string
asset_type) can escape unhandled. Expand the except clause to catch all relevant
exception types (ValueError, TypeError, AttributeError) or use a broader
exception handler to ensure all unexpected errors from strptime, asset_type
operations, and date calculations are properly caught and converted to a
consistent error response that maintains the fail-closed flow.
- Around line 24-36: The debt_fund threshold is set to 0 in the thresholds
dictionary, but the comparison logic `days > limit` means any holding period
greater than 0 days will be classified as LTCG, which contradicts the intended
behavior stated in the comment. To fix this, change the debt_fund threshold
value to a number large enough that the condition `days > limit` will always
evaluate to false for any realistic holding period (such as a very large integer
like 10000 or float('inf')), ensuring debt_fund assets are always classified as
STCG regardless of holding period.

In `@qwed_tax/guards/classification_guard.py`:
- Around line 69-74: Add a type guard check before calling `.upper()` on
`llm_claim` in the normalization block. If `llm_claim` is not a string, return
`verified=False` with an explicit error message instead of allowing the
AttributeError to be raised. The type check should occur before the comment "#
Normalize claim" and the subsequent call to `llm_claim.upper()`, ensuring the
fail-closed path returns a controlled error response rather than crashing.

In `@qwed_tax/guards/speculation_guard.py`:
- Around line 67-76: The _classify_source method uses simple substring matching
with the `keyword in source` check in both the cls._KNOWN_SPECULATIVE and
cls._KNOWN_NON_SPECULATIVE loops, which allows keywords to match within larger
words or tokens rather than as complete words. Replace the substring matching
with word boundary matching (e.g., using regex word boundaries \b or by
splitting the source string into tokens and comparing against individual words)
to ensure keywords only match as complete, standalone words and prevent false
classifications from ambiguous inputs.

In `@qwed_tax/jurisdictions/india/guards/gst_guard.py`:
- Around line 203-206: The `_try_coerce` method currently only catches
`ValueError` when attempting enum coercion, but malformed JSON values such as
lists or dicts can raise `TypeError` instead, which will escape the exception
handler and break the fail-closed behavior. Update the except clause to catch
both `ValueError` and `TypeError` so that any enum coercion failure (whether
ValueError or TypeError) returns `None` consistently.
- Around line 180-184: The computation mode block in the RCM/FCM decision logic
incorrectly returns verified=True without performing any claim comparison,
creating a false success state that could lead callers to accept unproven
decisions. Change the verified field to False in the return statement where the
liability is set to "RECIPIENT (RCM)" or "PROVIDER (FCM)" to accurately reflect
that this is a computed-only result without claim verification. Reserve
verified=True only for cases where the computation has been explicitly validated
against a claim comparison.

---

Outside diff comments:
In `@qwed_tax/guards/classification_guard.py`:
- Around line 14-24: The verify_worker_status method has a return type
annotation of WorkerType but the docstring and implementation indicate it can
return None when mixed signals are detected. Update the return type annotation
from WorkerType to Optional[WorkerType] by importing Optional from the typing
module (if not already imported) and modifying the function signature of
verify_worker_status to declare -> Optional[WorkerType]. This will align the
type signature with the actual behavior where None is returned for ambiguous
cases.

---

Nitpick comments:
In `@tests/test_rcm.py`:
- Around line 145-150: The current test_unknown_string_service_fail_closed
method only covers the fail-closed behavior for unknown service types, but does
not test the security-critical dual-mode contract behavior. Add additional test
methods that explicitly verify the verification mode behavior (testing both
matching and mismatching scenarios between claimed_is_rcm and computed RCM
status) and the computed_only mode behavior where verification is bypassed.
Include explicit assertions for these modes in the
guard.verify_rcm_applicability call to ensure the contract between
claimed_is_rcm and computed_only parameters is properly enforced and tested.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 93a20313-e5a1-4b3d-aa16-50f079219796

📥 Commits

Reviewing files that changed from the base of the PR and between ff22659 and 4163b10.

📒 Files selected for processing (8)
  • qwed_tax/guards/capital_gains_guard.py
  • qwed_tax/guards/classification_guard.py
  • qwed_tax/guards/speculation_guard.py
  • qwed_tax/jurisdictions/india/guards/crypto_guard.py
  • qwed_tax/jurisdictions/india/guards/gst_guard.py
  • qwed_tax/jurisdictions/india/guards/setoff_guard.py
  • qwed_tax/verifier.py
  • tests/test_rcm.py

Comment thread qwed_tax/guards/capital_gains_guard.py Outdated
Comment thread qwed_tax/guards/capital_gains_guard.py
Comment thread qwed_tax/guards/classification_guard.py
Comment thread qwed_tax/guards/speculation_guard.py
Comment thread qwed_tax/jurisdictions/india/guards/gst_guard.py Outdated
Comment thread qwed_tax/jurisdictions/india/guards/gst_guard.py
- CapitalGainsGuard: debt_fund special-case returns STCG always (was
  threshold=0 which classified as LTCG for any holding > 0 days)
- CapitalGainsGuard: catch TypeError in date parsing (non-string dates)
- CapitalGainsGuard: raise from exc (Ruff B904)
- SpeculationGuard: exact match instead of substring (was classifying
  'intraday_equity_position' as speculative, 'side_business_income' as
  non-speculative)
- ClassificationGuard: Optional[WorkerType] return type annotation
- ClassificationGuard: type guard for non-string llm_claim
- GSTGuard: computation mode verified=False (was True — calculator
  masquerading as verifier, #18 core fix)
- GSTGuard: _try_coerce catches TypeError (malformed JSON values)
- GSTGuard: remove no-op ternary on reason field
- Tests: 22 new tests for dual-mode RCM, debt_fund STCG, SLAB block,
  speculation exact match, classification ambiguity, crypto zero/negative,
  setoff allowlist, entity fail-closed
- Test: update test_trade_tax_setoff to use exact known source names

137 tests pass, 0 regressions. Ruff lint clean.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
qwed_tax/jurisdictions/india/guards/crypto_guard.py (1)

22-42: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail closed when gains is provided but not validated.

At Line 22, gains is accepted but ignored, and the method can still return verified=True at Line 38. That creates a verification false-positive surface for inputs the guard does not actually check. Until gain-side validation is implemented, reject non-empty gains explicitly.

Proposed fail-closed patch
 def verify_set_off(self, losses: Dict[str, Decimal], gains: Optional[Dict[str, Decimal]] = None) -> TaxResult:
@@
-    # Rule 1: Check for VDA Losses being used
+    # Fail closed: gain-side verification is not implemented yet.
+    if gains:
+        return TaxResult(
+            verified=False,
+            message="Gain-side set-off verification is not implemented in CryptoTaxGuard. Provide losses-only payload or route to inter-head set-off guard.",
+            allowed_set_off=Decimal(0),
+        )
+
+    # Rule 1: Check for VDA Losses being used
     if "VDA" in losses and losses["VDA"] < 0:
         return TaxResult(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qwed_tax/jurisdictions/india/guards/crypto_guard.py` around lines 22 - 42,
The verify_set_off method accepts an optional gains parameter but does not
validate it, causing the method to return verified=True even when unhandled
gains data is provided. To fail closed, add an explicit check at the beginning
of the verify_set_off method (before the VDA loss check) that rejects any
non-empty gains dictionary. If gains is provided and contains entries, return a
TaxResult with verified=False and a message indicating that inter-head
adjustment verification is not yet implemented. This prevents false positives
for validation scenarios the guard does not currently handle.

Source: Linters/SAST tools

qwed_tax/jurisdictions/india/guards/gst_guard.py (1)

157-159: ⚠️ Potential issue | 🟠 Major

Add strict boolean type validation for claimed_is_rcm to prevent false verification with non-bool truthy values.

The comparison verified = (claimed_is_rcm == is_rcm) is unsafe: Python's equality operator treats integers like 1 and 0 as equivalent to True and False respectively (1 == True evaluates to True). If claimed_is_rcm receives an integer from JSON deserialization or dynamic input despite the Optional[bool] type hint, verification can incorrectly pass.

Proposed fix
         # Verification mode: compare computed RCM against claimed RCM
         if claimed_is_rcm is not None:
+            if not isinstance(claimed_is_rcm, bool):
+                return {
+                    "verified": False,
+                    "error": (
+                        "Invalid claimed_is_rcm. Expected a boolean true/false "
+                        "for deterministic verification."
+                    ),
+                    "is_rcm": is_rcm,
+                }
-            verified = (claimed_is_rcm == is_rcm)
+            verified = (claimed_is_rcm is is_rcm)
             return {
                 "verified": verified,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qwed_tax/jurisdictions/india/guards/gst_guard.py` around lines 157 - 159, The
equality comparison in the verification statement does not perform strict type
checking, allowing Python's truthy behavior to treat integers like 1 and 0 as
equivalent to boolean True and False values. Add explicit type validation using
isinstance(claimed_is_rcm, bool) to ensure claimed_is_rcm is actually a boolean
before performing the equality comparison with is_rcm. This prevents false
verification when claimed_is_rcm receives integer values from JSON
deserialization or other dynamic input sources despite the Optional[bool] type
hint.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@qwed_tax/jurisdictions/india/guards/crypto_guard.py`:
- Around line 22-42: The verify_set_off method accepts an optional gains
parameter but does not validate it, causing the method to return verified=True
even when unhandled gains data is provided. To fail closed, add an explicit
check at the beginning of the verify_set_off method (before the VDA loss check)
that rejects any non-empty gains dictionary. If gains is provided and contains
entries, return a TaxResult with verified=False and a message indicating that
inter-head adjustment verification is not yet implemented. This prevents false
positives for validation scenarios the guard does not currently handle.

In `@qwed_tax/jurisdictions/india/guards/gst_guard.py`:
- Around line 157-159: The equality comparison in the verification statement
does not perform strict type checking, allowing Python's truthy behavior to
treat integers like 1 and 0 as equivalent to boolean True and False values. Add
explicit type validation using isinstance(claimed_is_rcm, bool) to ensure
claimed_is_rcm is actually a boolean before performing the equality comparison
with is_rcm. This prevents false verification when claimed_is_rcm receives
integer values from JSON deserialization or other dynamic input sources despite
the Optional[bool] type hint.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a25b5b1e-ab65-4afa-bc46-befee178ddfa

📥 Commits

Reviewing files that changed from the base of the PR and between 4163b10 and 426861c.

⛔ Files ignored due to path filters (2)
  • qwed_tax/__pycache__/__init__.cpython-311.pyc is excluded by !**/*.pyc
  • qwed_tax/__pycache__/models.cpython-311.pyc is excluded by !**/*.pyc
📒 Files selected for processing (7)
  • qwed_tax/guards/capital_gains_guard.py
  • qwed_tax/guards/classification_guard.py
  • qwed_tax/guards/speculation_guard.py
  • qwed_tax/jurisdictions/india/guards/crypto_guard.py
  • qwed_tax/jurisdictions/india/guards/gst_guard.py
  • tests/test_guards_coverage.py
  • tests/test_rcm.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • qwed_tax/guards/classification_guard.py
  • qwed_tax/guards/capital_gains_guard.py
  • qwed_tax/guards/speculation_guard.py

Comment thread qwed_tax/jurisdictions/india/guards/crypto_guard.py
Comment thread qwed_tax/verifier.py
Comment thread qwed_tax/verifier.py Outdated
@sonarqubecloud

Copy link
Copy Markdown

@rahuldass19 Rahul Dass (rahuldass19) merged commit b68ea79 into main Jun 21, 2026
23 checks passed
@mintlify

mintlify Bot commented Jun 21, 2026

Copy link
Copy Markdown

Docs PR opened: QWED-AI/docs#220

Added a changelog entry for QWED-Tax PR #42 covering fail-closed hardening across six guards for ambiguous classifications and unverified claims.

@mintlify

mintlify Bot commented Jun 21, 2026

Copy link
Copy Markdown

Docs PR opened: QWED-AI/docs#221

Documented fail-closed behavior across six tax guards and the new claim-comparison mode for GST RCM verification.

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.

[Bug]: Legal and tax classifications are guessed instead of blocked when facts are ambiguous

1 participant