Skip to content

feat(claims): Provable Memory layer, default-off (v2 spine) - #163

Merged
jaylfc merged 9 commits into
masterfrom
feat/claims-layer
Jun 13, 2026
Merged

feat(claims): Provable Memory layer, default-off (v2 spine)#163
jaylfc merged 9 commits into
masterfrom
feat/claims-layer

Conversation

@jaylfc

@jaylfc jaylfc commented Jun 13, 2026

Copy link
Copy Markdown
Owner

What

The v2 spine after surprisal died (N-009/N-010/N-011): an additive, default-off Provable Memory layer that makes F-009 (the measured 18.8 percent extraction-hallucination rate) a live, always-on gate. Built subagent-driven from the approved private spec; 9 tasks, TDD throughout.

How it works

  • Extract with provenance: each fact becomes a claim tagged with the archive span(s) it came from (taosmd/claims/extract.py).
  • Async verify: a fail-closed, batched, idempotent verify-pass (verify_pass.py) shows a cross-family local judge ONLY the cited spans and marks each claim supported / partial / unsupported / contradicted. Any failure leaves it unverified, never promoted.
  • Demote, never delete: a pure recall gate (gate.py) behind search(prefer_verified=...) (default off); verified preferred, unsupported excluded from default recall, everything still in the zero-loss archive and queryable (reversible).
  • ClaimStore (store.py): zero-loss sqlite, rebuildable from the archive, exposes the live hallucination rate. Store-mode gate is a pure function.
  • CLI: taosmd verify, taosmd claims status.

Safety / scope

  • Default-off: prefer_verified defaults off, so existing installs are byte-for-byte unchanged in behaviour. 886 tests pass (+24 claims), the claims package only imports memory_extractor + _db (clean isolation).
  • The default flip is gated on the pre-registered E-009 experiment (does the gate cut served-hallucination without dropping judged accuracy/R@K by >0.02), in this PR's report change.
  • Out of scope (next): contradiction/temporal-state tracking, compilation versioning, the E-009 bench harness (benchmarks/claims_gate_probe.py), the actual default flip.

Review

Not self-merged: it touches the live recall path (though default-off). Spec is private (~/tinyagentos-private/specs/2026-06-13-v2-claims-layer-design.md); the public spec ships when E-009 validates.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Claims layer (Provable Memory) for extracting and verifying claims from ingested content (default off)
    • Claims verified asynchronously with local model judge and fail-closed policy
    • New prefer_verified parameter in search to optionally demote unverified claims
    • New CLI commands: taosmd claims status (view verification statistics) and taosmd verify (verify unverified claims)
  • Documentation

    • Added pre-registered experiment E-009 documenting claims verification gates and evaluation metrics

@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a complete claims layer (Provable Memory) feature for extracting, storing, verifying, and gating search results based on claim verification status. The implementation spans extraction, storage, verification, search integration, and CLI tooling, with a pre-registered E-009 experiment design.

Changes

Provable Memory Claims Layer

Layer / File(s) Summary
Research Documentation & Experiment Design
CHANGELOG.md, docs/research-report.md
Pre-registration of E-009 experiment documenting gate modes, metrics (accuracy, R@K, hallucination_rate), and kill criterion; changelog entry summarizing the claims layer feature.
Claim Extraction & Package Foundation
taosmd/claims/__init__.py, taosmd/claims/extract.py, tests/claims/test_extract.py
Package structure with exports; claims_from_text() converts regex-extracted facts into claim dicts with archive span provenance; tests validate span association and empty-input handling.
SQLite-Backed Claim Storage
taosmd/claims/store.py, tests/claims/test_store.py
ClaimStore with async lifecycle, CRUD, status validation, unverified-claim pulls, per-span status resolution, and hallucination-rate aggregation; comprehensive test coverage for all operations.
Verification Pipeline
taosmd/claims/verifier.py, taosmd/claims/verify_pass.py, tests/claims/test_verifier.py, tests/claims/test_verify_pass.py
Verifier protocol, FakeVerifier for testing, LocalEntailmentVerifier for Ollama HTTP calls with fail-closed verdict parsing; verify_pass() orchestrates batched, idempotent verification with span fetching and status persistence.
Claims Gate for Search Recall
taosmd/claims/gate.py, tests/claims/test_gate.py
Pure gate function with three modes (off, prefer_verified, strict); filters/reorders hits by claim_status, boosts supported hits; comprehensive mode coverage in tests.
API Store Initialization, Ingest, and Search Gating
taosmd/api.py, tests/claims/test_retrieve_gate.py
Stores initialization adds ClaimStore; ingest captures span_id and tags metadata; search adds prefer_verified parameter and uses _attach_and_gate_claims() to apply claim-based gating; integration tests validate demoting, preference, and passthrough modes.
CLI Commands and Parser Refactoring
taosmd/cli.py, tests/claims/test_cli_claims.py
Parser extracted to _build_parser(); claims status computes and prints live rates; verify command orchestrates batched verification with Ollama; comprehensive CLI argument parsing tests.

Sequence Diagram(s)

sequenceDiagram
  participant Ingest
  participant Archive
  participant ClaimStore
  participant MemoryIndex
  Ingest->>Archive: record(text)
  Archive-->>Ingest: span_id
  Ingest->>Ingest: claims_from_text(text, span_id)
  Ingest->>MemoryIndex: add vector record<br/>(archive_span_id=span_id)
  Ingest->>ClaimStore: add_claim(text, spans)<br/>(status=unverified)
Loading
sequenceDiagram
  participant Search
  participant MemoryIndex
  participant ClaimStore
  participant Gate
  Search->>MemoryIndex: retrieve(query)
  MemoryIndex-->>Search: hits with confidence
  loop For each hit
    Search->>ClaimStore: status_for_spans(archive_span_id)
    ClaimStore-->>Search: claim_status
  end
  Search->>Gate: apply_claims_gate(hits, prefer_verified)
  Gate->>Gate: filter: drop unsupported
  Gate->>Gate: boost: +score for supported
  Gate->>Gate: sort by score DESC
  Gate-->>Search: reordered hits
  Search-->>Search: strip transient fields
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

In archives deep where claims take root,
A verifier stands, fail-closed and mute,
Local judges weigh each fact with care,
Gating searches with provenance rare,
Truth extracted, stored, and made fair! 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 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 change: a new Provable Memory claims layer that is default-off (v2 spine). It clearly conveys the primary feature addition and its key characteristic (default-off).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 feat/claims-layer

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.

Comment on lines +28 to +41
while True:
pending = await store.pull_unverified(limit=batch)
fresh = [c for c in pending if c["id"] not in seen]
if not fresh:
break
for claim in fresh:
seen.add(claim["id"])
spans = await fetch_spans(claim["archive_span_ids"])
status, model = verifier.verify(claim["text"], spans)
if status == "unverified":
# fail-closed: stays unverified, retried in a future pass.
continue
await store.set_status(claim["id"], status, verifier_model=model, now=now)
done += 1

@gitar-bot gitar-bot Bot Jun 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Bug: verify_pass skips claims beyond a fully-failing first batch

verify_pass (taosmd/claims/verify_pass.py:28-41) paginates via store.pull_unverified(limit=batch), which always returns the first batch claims still in status 'unverified' ordered by id (store.py:77-81). When a claim's verifier returns 'unverified' it is left unverified (fail-closed) but added to the seen set. On the next loop iteration pull_unverified returns the SAME first batch rows (they are still 'unverified' and lowest id), fresh becomes empty after filtering by seen, and the loop breaks.

Consequence: if a whole batch (e.g. 100) of claims fails or is genuinely unsupported-but-returned-unverified, every unverified claim with a higher id is never pulled in that pass — and since seen is reset on each new verify invocation, those claims remain permanently unreachable as long as the leading batch keeps returning 'unverified'. The store can quietly accumulate unverifiable claims that block all claims behind them.

Fix: advance pagination by id cursor instead of relying on status changing to move the window.

Page by an id cursor so failed/unverified claims no longer block the rest of the pass.:

# store.py
async def pull_unverified(self, limit: int = 100, after: int = 0) -> list[dict]:
    rows = self._conn.execute(
        "SELECT * FROM claims WHERE status = 'unverified' AND id > ?"
        " ORDER BY id LIMIT ?", (after, limit)
    ).fetchall()
    return [self._row(r) for r in rows]

# verify_pass.py
async def verify_pass(store, verifier, fetch_spans, batch=100, now=None) -> int:
    done = 0
    after = 0
    while True:
        pending = await store.pull_unverified(limit=batch, after=after)
        if not pending:
            break
        for claim in pending:
            after = max(after, claim["id"])
            spans = await fetch_spans(claim["archive_span_ids"])
            status, model = verifier.verify(claim["text"], spans)
            if status == "unverified":
                continue
            await store.set_status(claim["id"], status, verifier_model=model, now=now)
            done += 1
    return done

Was this helpful? React with 👍 / 👎

Comment thread taosmd/claims/store.py
Comment on lines +83 to +97
async def status_for_spans(self, span_ids: list[int]) -> str | None:
"""Worst status among claims backed by any of these archive spans.

Used by the recall gate to judge a hit by the claims its source spans
back. Worst-wins so one unsupported claim demotes the hit. None when no
claim references these spans (a raw, non-claim memory)."""
if not span_ids:
return None
rows = self._conn.execute("SELECT archive_span_ids, status FROM claims").fetchall()
want = set(span_ids)
order = {s: i for i, s in enumerate(
("supported", "unverified", "partial", "contradicted", "unsupported"))}
worst = None
for r in rows:
if want & set(json.loads(r["archive_span_ids"])):

@gitar-bot gitar-bot Bot Jun 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Performance: status_for_spans does a full claims table scan per hit

status_for_spans (taosmd/claims/store.py:83-100) runs SELECT archive_span_ids, status FROM claims — a full table scan — and JSON-decodes every row on every call. _attach_and_gate_claims (taosmd/api.py:336-357) invokes it once per hit when the gate is on, so a single search() with the gate enabled costs O(hits × total_claims) row reads plus JSON parsing. As the claim store grows this becomes a hot-path cost on every gated recall (exactly the path E-009 exercises before the default is flipped).

Suggested fix: scan the claims table once per search and build a span→worst-status map (or filter rows in SQL by the union of needed span ids), rather than re-scanning per hit. The archive_span_ids JSON column also prevents index use; consider a normalized claim_spans(span_id, claim_id) table for indexed lookups if the store is expected to be large.

Build the span→status map with a single table scan instead of one scan per hit.:

# api.py _attach_and_gate_claims: scan once, resolve all hits against one map
from taosmd.claims.gate import apply_claims_gate  # noqa: PLC0415
span_map = await claim_store.worst_status_map()  # {span_id: worst_status}
for h in hits:
    span = (h.get("metadata") or {}).get("archive_span_id")
    h["claim_status"] = span_map.get(span) if isinstance(span, int) else None
    h["score"] = h.get("confidence", 0.0)
# (add ClaimStore.worst_status_map() that does the single scan + worst-wins fold)

Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Jun 13, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 0 resolved / 2 findings

Implements the Provable Memory layer with provenance-based verification, but verify_pass fails to process batches beyond the first error and status_for_spans triggers inefficient full table scans per hit.

⚠️ Bug: verify_pass skips claims beyond a fully-failing first batch

📄 taosmd/claims/verify_pass.py:28-41 📄 taosmd/claims/store.py:77-81

verify_pass (taosmd/claims/verify_pass.py:28-41) paginates via store.pull_unverified(limit=batch), which always returns the first batch claims still in status 'unverified' ordered by id (store.py:77-81). When a claim's verifier returns 'unverified' it is left unverified (fail-closed) but added to the seen set. On the next loop iteration pull_unverified returns the SAME first batch rows (they are still 'unverified' and lowest id), fresh becomes empty after filtering by seen, and the loop breaks.

Consequence: if a whole batch (e.g. 100) of claims fails or is genuinely unsupported-but-returned-unverified, every unverified claim with a higher id is never pulled in that pass — and since seen is reset on each new verify invocation, those claims remain permanently unreachable as long as the leading batch keeps returning 'unverified'. The store can quietly accumulate unverifiable claims that block all claims behind them.

Fix: advance pagination by id cursor instead of relying on status changing to move the window.

Page by an id cursor so failed/unverified claims no longer block the rest of the pass.
# store.py
async def pull_unverified(self, limit: int = 100, after: int = 0) -> list[dict]:
    rows = self._conn.execute(
        "SELECT * FROM claims WHERE status = 'unverified' AND id > ?"
        " ORDER BY id LIMIT ?", (after, limit)
    ).fetchall()
    return [self._row(r) for r in rows]

# verify_pass.py
async def verify_pass(store, verifier, fetch_spans, batch=100, now=None) -> int:
    done = 0
    after = 0
    while True:
        pending = await store.pull_unverified(limit=batch, after=after)
        if not pending:
            break
        for claim in pending:
            after = max(after, claim["id"])
            spans = await fetch_spans(claim["archive_span_ids"])
            status, model = verifier.verify(claim["text"], spans)
            if status == "unverified":
                continue
            await store.set_status(claim["id"], status, verifier_model=model, now=now)
            done += 1
    return done
⚠️ Performance: status_for_spans does a full claims table scan per hit

📄 taosmd/claims/store.py:83-97 📄 taosmd/api.py:336-350

status_for_spans (taosmd/claims/store.py:83-100) runs SELECT archive_span_ids, status FROM claims — a full table scan — and JSON-decodes every row on every call. _attach_and_gate_claims (taosmd/api.py:336-357) invokes it once per hit when the gate is on, so a single search() with the gate enabled costs O(hits × total_claims) row reads plus JSON parsing. As the claim store grows this becomes a hot-path cost on every gated recall (exactly the path E-009 exercises before the default is flipped).

Suggested fix: scan the claims table once per search and build a span→worst-status map (or filter rows in SQL by the union of needed span ids), rather than re-scanning per hit. The archive_span_ids JSON column also prevents index use; consider a normalized claim_spans(span_id, claim_id) table for indexed lookups if the store is expected to be large.

Build the span→status map with a single table scan instead of one scan per hit.
# api.py _attach_and_gate_claims: scan once, resolve all hits against one map
from taosmd.claims.gate import apply_claims_gate  # noqa: PLC0415
span_map = await claim_store.worst_status_map()  # {span_id: worst_status}
for h in hits:
    span = (h.get("metadata") or {}).get("archive_span_id")
    h["claim_status"] = span_map.get(span) if isinstance(span, int) else None
    h["score"] = h.get("confidence", 0.0)
# (add ClaimStore.worst_status_map() that does the single scan + worst-wins fold)
🤖 Prompt for agents
Code Review: Implements the Provable Memory layer with provenance-based verification, but verify_pass fails to process batches beyond the first error and status_for_spans triggers inefficient full table scans per hit.

1. ⚠️ Bug: verify_pass skips claims beyond a fully-failing first batch
   Files: taosmd/claims/verify_pass.py:28-41, taosmd/claims/store.py:77-81

   `verify_pass` (taosmd/claims/verify_pass.py:28-41) paginates via `store.pull_unverified(limit=batch)`, which always returns the first `batch` claims still in status 'unverified' ordered by id (store.py:77-81). When a claim's verifier returns 'unverified' it is left unverified (fail-closed) but added to the `seen` set. On the next loop iteration `pull_unverified` returns the SAME first `batch` rows (they are still 'unverified' and lowest id), `fresh` becomes empty after filtering by `seen`, and the loop breaks.
   
   Consequence: if a whole batch (e.g. 100) of claims fails or is genuinely unsupported-but-returned-unverified, every unverified claim with a higher id is never pulled in that pass — and since `seen` is reset on each new `verify` invocation, those claims remain permanently unreachable as long as the leading batch keeps returning 'unverified'. The store can quietly accumulate unverifiable claims that block all claims behind them.
   
   Fix: advance pagination by id cursor instead of relying on status changing to move the window.

   Fix (Page by an id cursor so failed/unverified claims no longer block the rest of the pass.):
   # store.py
   async def pull_unverified(self, limit: int = 100, after: int = 0) -> list[dict]:
       rows = self._conn.execute(
           "SELECT * FROM claims WHERE status = 'unverified' AND id > ?"
           " ORDER BY id LIMIT ?", (after, limit)
       ).fetchall()
       return [self._row(r) for r in rows]
   
   # verify_pass.py
   async def verify_pass(store, verifier, fetch_spans, batch=100, now=None) -> int:
       done = 0
       after = 0
       while True:
           pending = await store.pull_unverified(limit=batch, after=after)
           if not pending:
               break
           for claim in pending:
               after = max(after, claim["id"])
               spans = await fetch_spans(claim["archive_span_ids"])
               status, model = verifier.verify(claim["text"], spans)
               if status == "unverified":
                   continue
               await store.set_status(claim["id"], status, verifier_model=model, now=now)
               done += 1
       return done

2. ⚠️ Performance: status_for_spans does a full claims table scan per hit
   Files: taosmd/claims/store.py:83-97, taosmd/api.py:336-350

   `status_for_spans` (taosmd/claims/store.py:83-100) runs `SELECT archive_span_ids, status FROM claims` — a full table scan — and JSON-decodes every row on every call. `_attach_and_gate_claims` (taosmd/api.py:336-357) invokes it once per hit when the gate is on, so a single `search()` with the gate enabled costs O(hits × total_claims) row reads plus JSON parsing. As the claim store grows this becomes a hot-path cost on every gated recall (exactly the path E-009 exercises before the default is flipped).
   
   Suggested fix: scan the claims table once per search and build a span→worst-status map (or filter rows in SQL by the union of needed span ids), rather than re-scanning per hit. The `archive_span_ids` JSON column also prevents index use; consider a normalized claim_spans(span_id, claim_id) table for indexed lookups if the store is expected to be large.

   Fix (Build the span→status map with a single table scan instead of one scan per hit.):
   # api.py _attach_and_gate_claims: scan once, resolve all hits against one map
   from taosmd.claims.gate import apply_claims_gate  # noqa: PLC0415
   span_map = await claim_store.worst_status_map()  # {span_id: worst_status}
   for h in hits:
       span = (h.get("metadata") or {}).get("archive_span_id")
       h["claim_status"] = span_map.get(span) if isinstance(span, int) else None
       h["score"] = h.get("confidence", 0.0)
   # (add ClaimStore.worst_status_map() that does the single scan + worst-wins fold)

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@kilo-code-bot

kilo-code-bot Bot commented Jun 13, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 9 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 3
WARNING 4
SUGGESTION 2
Issue Details (click to expand)

CRITICAL

File Line Issue
taosmd/api.py 104 Claims are extracted from pre-redaction text, so secrets can be persisted in claims.db even though archive/vector storage redacts them.
taosmd/claims/verifier.py 12 Verifier prompt concatenates archived source/claim text without delimiters or injection-resistance instructions.
taosmd/claims/verifier.py 23 Verdict parser can mark phrases like not supported as supported, violating fail-closed behavior.

WARNING

File Line Issue
taosmd/claims/gate.py 21 Invalid prefer_verified modes are silently treated as lenient instead of raising.
taosmd/claims/store.py 94 Claim status lookup scans and JSON-decodes every claim row per hit, creating recall latency risk.
taosmd/claims/store.py 97 Malformed archive_span_ids JSON can crash gated search.
taosmd/claims/verify_pass.py 35 Span fetch failures abort the whole verification pass instead of leaving one claim unverified and continuing.

SUGGESTION

File Line Issue
taosmd/claims/gate.py 25 Pure gate mutates caller-owned hit objects by adding boosted score.
taosmd/api.py 405 New public prefer_verified parameter is not documented in the search() docstring.
Other Observations (not in diff)

No additional issues found outside diff lines. Inline comment posting was attempted but rejected by the non-interactive environment permission layer.

Files Reviewed (18 files)
  • CHANGELOG.md
  • docs/research-report.md
  • taosmd/api.py
  • taosmd/claims/__init__.py
  • taosmd/claims/extract.py
  • taosmd/claims/gate.py
  • taosmd/claims/store.py
  • taosmd/claims/verifier.py
  • taosmd/claims/verify_pass.py
  • taosmd/cli.py
  • tests/claims/__init__.py
  • tests/claims/test_cli_claims.py
  • tests/claims/test_extract.py
  • tests/claims/test_gate.py
  • tests/claims/test_retrieve_gate.py
  • tests/claims/test_store.py
  • tests/claims/test_verifier.py
  • tests/claims/test_verify_pass.py

Fix these issues in Kilo Cloud


Reviewed by nex-n2-pro:free · 955,110 tokens

jaylfc added a commit that referenced this pull request Jun 13, 2026

@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: 10

🧹 Nitpick comments (2)
tests/claims/test_verify_pass.py (1)

17-45: ⚡ Quick win

Add a regression case where pending unverified claims exceed one batch.

Current tests use datasets smaller than the batch size (or single-claim fail-closed). Add a case with N > batch where verifier always returns "unverified" and assert all claims are attempted once. This will lock in the intended pass-progress semantics.

🤖 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/claims/test_verify_pass.py` around lines 17 - 45, Add a new test in
tests/claims/test_verify_pass.py that creates N claims where N > batch (use
_store and asyncio.run with s.add_claim in a loop), use a verifier class whose
verify method always returns ("unverified", "msg"), call verify_pass(s,
verifier, _spans_text, batch=batch_size, now=...) and assert the returned n
equals N (all claims attempted once) and each claim's status (via
asyncio.run(s.get(cid))) remains "unverified"; reference the existing
verify_pass function and _store helper and pick a concrete batch_size (e.g., 5)
with N > batch to reproduce the regression.
tests/claims/test_gate.py (1)

8-35: ⚡ Quick win

Add an invalid-mode regression test.

Once mode validation is enforced, add a test asserting invalid modes raise ValueError so silent fallback cannot regress.

🤖 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/claims/test_gate.py` around lines 8 - 35, Add a regression test that
ensures invalid modes raise ValueError: create a new test function (e.g.
test_invalid_mode_raises_value_error) that calls apply_claims_gate(hits,
mode="invalid_mode") and asserts a ValueError is raised (use pytest.raises) to
prevent silent fallback; place it alongside the existing tests for
apply_claims_gate.
🤖 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 `@taosmd/api.py`:
- Around line 134-136: In _ensure_stores, make ClaimStore initialization
fail-open: wrap the creation and await of ClaimStore (the ClaimStore(...) call
and await claims.init()) in a try/except that catches Exception, logs a
warning/error with the exception, and continues without re-raising (optionally
setting claims = None or marking the store as unavailable) so core ingest/search
won't fail when the gating is off; ensure callers handle a missing/None claims
store gracefully.
- Around line 231-238: The claims extraction/writes inside the ingest path (the
block using claims_from_text, stores["claims"].add_claim, span_id checks and the
archived variable) must be isolated so failures there do not propagate and
prevent incrementing archived or surface as a partial-success error; wrap the
entire claims extraction + per-claim add loop in a try/except that catches
Exception, logs the failure (including the exception) and continues without
re-raising, leaving archived += 1 executed as before; keep the existing guard
that the block only runs when "claims" in stores and span_id is a non-negative
int and do not change the surrounding control flow.

In `@taosmd/claims/gate.py`:
- Around line 21-25: The function apply_claims_gate silently treats any
non-"strict" mode as lenient, so validate the mode explicitly: accept only
"off", "strict", or "lenient" (or whatever allowed set you decide), raise a
ValueError for unknown modes, and only compute drop when mode is "strict" or
"lenient"; keep the existing logic that returns hits unchanged for "off" and
that sets drop = _DROP_STRICT for "strict" and drop = _DROP_LENIENT for
"lenient", then build kept = [h for h in hits if (h.get("claim_status") not in
drop)] and return kept.

In `@taosmd/claims/store.py`:
- Around line 91-99: status_for_spans() currently does a full-table scan and
JSON-decodes every row; refactor it to query an indexed join table instead:
add/use a normalized table (e.g., claim_spans with columns claim_id, span_id and
indexed span_id) and change the logic in status_for_spans to SELECT status FROM
claims JOIN claim_spans ON claims.id=claim_spans.claim_id WHERE
claim_spans.span_id IN (...) (or batch the IN with params), then compute the
worst status using the existing order mapping without per-row json.loads; this
removes the full-table scan and per-call JSON parsing and limits work to
matching spans only.

In `@taosmd/claims/verifier.py`:
- Around line 22-24: parse_verdict currently returns 'supported' because the
loop (v in ("CONTRADICTED","UNSUPPORTED","PARTIAL","SUPPORTED")) matches the
bare word SUPPORTED even when preceded by negation (e.g., "not supported");
update the regex used when v == "SUPPORTED" to exclude common negation patterns
before the word. Concretely, in parse_verdict where you iterate over v and use
re.search(rf"\b{v}\b", t), special-case SUPPORTED to use a negative-lookbehind
or explicit pattern like
r"(?<!\b(?:not|no|n't|never|cannot|can't)\b\s*)\bSUPPORTED\b" (with
re.IGNORECASE) so phrases containing negation do not return 'supported'; keep
the existing checks for "CONTRADICTED", "UNSUPPORTED", and "PARTIAL" unchanged.

In `@taosmd/claims/verify_pass.py`:
- Around line 29-32: The current fixed-window repull using
store.pull_unverified(limit=batch) plus filtering by seen causes starvation;
change the loop to use cursor-based pagination by tracking last_seen_id and
calling a paginated pull (e.g., store.pull_unverified_after(last_seen_id,
limit=batch) or add a parameter to store.pull_unverified to do WHERE id >
last_seen_id ORDER BY id LIMIT ?), update last_seen_id to the max id returned
each fetch, and remove the fresh filtering that stops the loop early (or only
skip already-seen ids while continuing the cursor). Ensure the new query returns
rows ordered by id so every unverified claim is attempted once per pass even if
earlier rows remain unverified.
- Around line 35-40: Per-claim exceptions in the verification loop currently
abort the whole pass; wrap the per-claim work (the calls to fetch_spans,
verifier.verify, and store.set_status for each claim) in a try/except so any
exception is caught, logged, and the loop continues (leaving that claim
unverified) rather than re-raising; specifically, enclose the sequence spanning
fetch_spans(claim["archive_span_ids"]), verifier.verify(claim["text"], spans),
and the subsequent await store.set_status(...) in a try block and in except log
the exception and continue to the next claim to preserve fail-closed behavior.

In `@taosmd/cli.py`:
- Around line 1488-1491: The --batch argument currently uses
verify_p.add_argument("--batch", type=int, ...) and accepts zero or negative
values; add a validator function (e.g., def positive_int(value): ...) that
converts to int, checks value > 0, and raises argparse.ArgumentTypeError on
failure, then change verify_p.add_argument to use type=positive_int and keep the
same default/help; this ensures --batch only accepts positive integers and
produces a clear argparse error message when invalid.
- Around line 998-999: The verify command in taosmd/cli.py imports httpx at
runtime (see the import added for the verify handler) but httpx is not declared
or installed; either add "httpx" to project.dependencies in pyproject.toml (or
create a dedicated extra) so it’s installed, or modify the verify handler to
catch ModuleNotFoundError around the httpx import and raise a clear error
message instructing the user to pip install httpx (and update
taosmd/auto_setup.py if you choose to auto-install it so the setup logic
installs the new dependency).

In `@tests/claims/test_store.py`:
- Around line 35-39: Replace the fragile "assert False, 'expected ValueError'"
in the test for s.set_status with an explicit test failure call (e.g.,
pytest.fail("expected ValueError")) so the test triggers a clear failure instead
of using assert False; update the test in tests/claims/test_store.py around the
s.set_status(cid, "bogus", verifier_model="m", now=1.0) try/except block to call
pytest.fail in the try branch and keep the except ValueError: pass as the
expected path.

---

Nitpick comments:
In `@tests/claims/test_gate.py`:
- Around line 8-35: Add a regression test that ensures invalid modes raise
ValueError: create a new test function (e.g.
test_invalid_mode_raises_value_error) that calls apply_claims_gate(hits,
mode="invalid_mode") and asserts a ValueError is raised (use pytest.raises) to
prevent silent fallback; place it alongside the existing tests for
apply_claims_gate.

In `@tests/claims/test_verify_pass.py`:
- Around line 17-45: Add a new test in tests/claims/test_verify_pass.py that
creates N claims where N > batch (use _store and asyncio.run with s.add_claim in
a loop), use a verifier class whose verify method always returns ("unverified",
"msg"), call verify_pass(s, verifier, _spans_text, batch=batch_size, now=...)
and assert the returned n equals N (all claims attempted once) and each claim's
status (via asyncio.run(s.get(cid))) remains "unverified"; reference the
existing verify_pass function and _store helper and pick a concrete batch_size
(e.g., 5) with N > batch to reproduce the regression.
🪄 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 Plus

Run ID: f3717adb-e53f-44ca-b8f2-9090a0ab5b13

📥 Commits

Reviewing files that changed from the base of the PR and between eec4571 and 7d7bca5.

📒 Files selected for processing (18)
  • CHANGELOG.md
  • docs/research-report.md
  • taosmd/api.py
  • taosmd/claims/__init__.py
  • taosmd/claims/extract.py
  • taosmd/claims/gate.py
  • taosmd/claims/store.py
  • taosmd/claims/verifier.py
  • taosmd/claims/verify_pass.py
  • taosmd/cli.py
  • tests/claims/__init__.py
  • tests/claims/test_cli_claims.py
  • tests/claims/test_extract.py
  • tests/claims/test_gate.py
  • tests/claims/test_retrieve_gate.py
  • tests/claims/test_store.py
  • tests/claims/test_verifier.py
  • tests/claims/test_verify_pass.py

Comment thread taosmd/api.py
Comment on lines +134 to +136
from taosmd.claims.store import ClaimStore # noqa: PLC0415
claims = ClaimStore(db_path=str(path / "claims.db"))
await claims.init()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep claims store initialization fail-open to preserve default-off behavior.

ClaimStore init is now a hard dependency of _ensure_stores; if it fails, core ingest/search fail even when gating is off.

Proposed fix
-        from taosmd.claims.store import ClaimStore  # noqa: PLC0415
-        claims = ClaimStore(db_path=str(path / "claims.db"))
-        await claims.init()
+        claims = None
+        try:
+            from taosmd.claims.store import ClaimStore  # noqa: PLC0415
+            claims = ClaimStore(db_path=str(path / "claims.db"))
+            await claims.init()
+        except Exception as exc:  # pragma: no cover - defensive
+            logger.warning("taosmd: claims store disabled (init failed): %s", exc)
🤖 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 `@taosmd/api.py` around lines 134 - 136, In _ensure_stores, make ClaimStore
initialization fail-open: wrap the creation and await of ClaimStore (the
ClaimStore(...) call and await claims.init()) in a try/except that catches
Exception, logs a warning/error with the exception, and continues without
re-raising (optionally setting claims = None or marking the store as
unavailable) so core ingest/search won't fail when the gating is off; ensure
callers handle a missing/None claims store gracefully.

Comment thread taosmd/api.py
Comment on lines +231 to 238
# Claims layer (additive): extract facts as claims tagged with the same
# archive span. Stored unverified; the verify-pass checks them async.
if "claims" in stores and isinstance(span_id, int) and span_id >= 0:
from taosmd.claims.extract import claims_from_text # noqa: PLC0415
for c in claims_from_text(text, span_id):
await stores["claims"].add_claim(
c["text"], c["archive_span_ids"], c["source_extractor"])
archived += 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Isolate claims extraction/write failures from core ingest writes.

If claims extraction/add fails here, ingest raises after archive/vector were already written; that creates a partial-success write path and under-reports archived.

Proposed fix
-        if "claims" in stores and isinstance(span_id, int) and span_id >= 0:
-            from taosmd.claims.extract import claims_from_text  # noqa: PLC0415
-            for c in claims_from_text(text, span_id):
-                await stores["claims"].add_claim(
-                    c["text"], c["archive_span_ids"], c["source_extractor"])
+        if stores.get("claims") is not None and isinstance(span_id, int) and span_id >= 0:
+            try:
+                from taosmd.claims.extract import claims_from_text  # noqa: PLC0415
+                for c in claims_from_text(text, span_id):
+                    await stores["claims"].add_claim(
+                        c["text"], c["archive_span_ids"], c["source_extractor"]
+                    )
+            except Exception as exc:  # pragma: no cover - defensive
+                logger.warning("taosmd: claims extraction skipped for span %s: %s", span_id, exc)
         archived += 1
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Claims layer (additive): extract facts as claims tagged with the same
# archive span. Stored unverified; the verify-pass checks them async.
if "claims" in stores and isinstance(span_id, int) and span_id >= 0:
from taosmd.claims.extract import claims_from_text # noqa: PLC0415
for c in claims_from_text(text, span_id):
await stores["claims"].add_claim(
c["text"], c["archive_span_ids"], c["source_extractor"])
archived += 1
# Claims layer (additive): extract facts as claims tagged with the same
# archive span. Stored unverified; the verify-pass checks them async.
if stores.get("claims") is not None and isinstance(span_id, int) and span_id >= 0:
try:
from taosmd.claims.extract import claims_from_text # noqa: PLC0415
for c in claims_from_text(text, span_id):
await stores["claims"].add_claim(
c["text"], c["archive_span_ids"], c["source_extractor"]
)
except Exception as exc: # pragma: no cover - defensive
logger.warning("taosmd: claims extraction skipped for span %s: %s", span_id, exc)
archived += 1
🤖 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 `@taosmd/api.py` around lines 231 - 238, The claims extraction/writes inside
the ingest path (the block using claims_from_text, stores["claims"].add_claim,
span_id checks and the archived variable) must be isolated so failures there do
not propagate and prevent incrementing archived or surface as a partial-success
error; wrap the entire claims extraction + per-claim add loop in a try/except
that catches Exception, logs the failure (including the exception) and continues
without re-raising, leaving archived += 1 executed as before; keep the existing
guard that the block only runs when "claims" in stores and span_id is a
non-negative int and do not change the surrounding control flow.

Comment thread taosmd/claims/gate.py
Comment on lines +21 to +25
def apply_claims_gate(hits: list[dict], mode: str = "off") -> list[dict]:
if mode == "off" or not hits:
return hits
drop = _DROP_STRICT if mode == "strict" else _DROP_LENIENT
kept = [h for h in hits if (h.get("claim_status") not in drop)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate gate mode explicitly instead of silently falling back.

Unknown mode values currently route to lenient behavior, which can silently change recall semantics on typos or bad caller input.

Proposed fix
 def apply_claims_gate(hits: list[dict], mode: str = "off") -> list[dict]:
+    if mode not in {"off", "prefer_verified", "strict"}:
+        raise ValueError(f"unsupported claims gate mode: {mode!r}")
     if mode == "off" or not hits:
         return hits
     drop = _DROP_STRICT if mode == "strict" else _DROP_LENIENT
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def apply_claims_gate(hits: list[dict], mode: str = "off") -> list[dict]:
if mode == "off" or not hits:
return hits
drop = _DROP_STRICT if mode == "strict" else _DROP_LENIENT
kept = [h for h in hits if (h.get("claim_status") not in drop)]
def apply_claims_gate(hits: list[dict], mode: str = "off") -> list[dict]:
if mode not in {"off", "prefer_verified", "strict"}:
raise ValueError(f"unsupported claims gate mode: {mode!r}")
if mode == "off" or not hits:
return hits
drop = _DROP_STRICT if mode == "strict" else _DROP_LENIENT
kept = [h for h in hits if (h.get("claim_status") not in drop)]
🤖 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 `@taosmd/claims/gate.py` around lines 21 - 25, The function apply_claims_gate
silently treats any non-"strict" mode as lenient, so validate the mode
explicitly: accept only "off", "strict", or "lenient" (or whatever allowed set
you decide), raise a ValueError for unknown modes, and only compute drop when
mode is "strict" or "lenient"; keep the existing logic that returns hits
unchanged for "off" and that sets drop = _DROP_STRICT for "strict" and drop =
_DROP_LENIENT for "lenient", then build kept = [h for h in hits if
(h.get("claim_status") not in drop)] and return kept.

Comment thread taosmd/claims/store.py
Comment on lines +91 to +99
rows = self._conn.execute("SELECT archive_span_ids, status FROM claims").fetchall()
want = set(span_ids)
order = {s: i for i, s in enumerate(
("supported", "unverified", "partial", "contradicted", "unsupported"))}
worst = None
for r in rows:
if want & set(json.loads(r["archive_span_ids"])):
if worst is None or order[r["status"]] > order[worst]:
worst = r["status"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

status_for_spans() does a full-table scan per retrieval hit.

Line 91 loads every claim row, and Line 97 JSON-decodes span arrays row-by-row on each call. Since retrieval attaches claim status per hit (taosmd/api.py Line 351), this grows toward O(hits × claims) and will become a latency bottleneck as claims grow. Consider normalizing span links into an indexed join table (e.g., claim_spans(claim_id, span_id)) so status lookup only touches matching spans.

🤖 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 `@taosmd/claims/store.py` around lines 91 - 99, status_for_spans() currently
does a full-table scan and JSON-decodes every row; refactor it to query an
indexed join table instead: add/use a normalized table (e.g., claim_spans with
columns claim_id, span_id and indexed span_id) and change the logic in
status_for_spans to SELECT status FROM claims JOIN claim_spans ON
claims.id=claim_spans.claim_id WHERE claim_spans.span_id IN (...) (or batch the
IN with params), then compute the worst status using the existing order mapping
without per-row json.loads; this removes the full-table scan and per-call JSON
parsing and limits work to matching spans only.

Comment thread taosmd/claims/verifier.py
Comment on lines +22 to +24
for v in ("CONTRADICTED", "UNSUPPORTED", "PARTIAL", "SUPPORTED"):
if re.search(rf"\b{v}\b", t):
return v.lower()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

parse_verdict() can misclassify negated replies as supported.

Line 23 matches bare SUPPORTED, so a reply like “not supported by source” can be interpreted as supported, which violates fail-closed behavior.

Proposed hardening
 def parse_verdict(text: str) -> str:
     """Map a judge reply to a status; unparseable -> 'unverified' (fail-closed)."""
     t = text.upper()
+    # Handle common negation before bare keyword matching.
+    if re.search(r"\bNOT\s+SUPPORTED\b", t):
+        return "unsupported"
     for v in ("CONTRADICTED", "UNSUPPORTED", "PARTIAL", "SUPPORTED"):
         if re.search(rf"\b{v}\b", t):
             return v.lower()
     return "unverified"
🤖 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 `@taosmd/claims/verifier.py` around lines 22 - 24, parse_verdict currently
returns 'supported' because the loop (v in
("CONTRADICTED","UNSUPPORTED","PARTIAL","SUPPORTED")) matches the bare word
SUPPORTED even when preceded by negation (e.g., "not supported"); update the
regex used when v == "SUPPORTED" to exclude common negation patterns before the
word. Concretely, in parse_verdict where you iterate over v and use
re.search(rf"\b{v}\b", t), special-case SUPPORTED to use a negative-lookbehind
or explicit pattern like
r"(?<!\b(?:not|no|n't|never|cannot|can't)\b\s*)\bSUPPORTED\b" (with
re.IGNORECASE) so phrases containing negation do not return 'supported'; keep
the existing checks for "CONTRADICTED", "UNSUPPORTED", and "PARTIAL" unchanged.

Comment on lines +29 to +32
pending = await store.pull_unverified(limit=batch)
fresh = [c for c in pending if c["id"] not in seen]
if not fresh:
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Fixed-window batching can starve claims beyond the first batch.

Lines 29-32 always repull the earliest unverified rows. If those stay unverified, fresh becomes empty and the loop exits before later IDs are ever attempted.

Use cursor-based pagination (WHERE id > last_seen_id ORDER BY id LIMIT ?) so each unverified claim is attempted once per pass, even when many remain fail-closed.

🤖 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 `@taosmd/claims/verify_pass.py` around lines 29 - 32, The current fixed-window
repull using store.pull_unverified(limit=batch) plus filtering by seen causes
starvation; change the loop to use cursor-based pagination by tracking
last_seen_id and calling a paginated pull (e.g.,
store.pull_unverified_after(last_seen_id, limit=batch) or add a parameter to
store.pull_unverified to do WHERE id > last_seen_id ORDER BY id LIMIT ?), update
last_seen_id to the max id returned each fetch, and remove the fresh filtering
that stops the loop early (or only skip already-seen ids while continuing the
cursor). Ensure the new query returns rows ordered by id so every unverified
claim is attempted once per pass even if earlier rows remain unverified.

Comment on lines +35 to +40
spans = await fetch_spans(claim["archive_span_ids"])
status, model = verifier.verify(claim["text"], spans)
if status == "unverified":
# fail-closed: stays unverified, retried in a future pass.
continue
await store.set_status(claim["id"], status, verifier_model=model, now=now)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Per-claim exceptions currently abort the whole verification pass.

On Lines 35-40, exceptions from fetch_spans, verifier.verify, or set_status bubble out and terminate the pass. For fail-closed behavior, these should leave the claim unverified and continue with the next claim.

Proposed fail-closed guard
         for claim in fresh:
             seen.add(claim["id"])
-            spans = await fetch_spans(claim["archive_span_ids"])
-            status, model = verifier.verify(claim["text"], spans)
+            try:
+                spans = await fetch_spans(claim["archive_span_ids"])
+                status, model = verifier.verify(claim["text"], spans)
+            except Exception:  # fail-closed at per-claim granularity
+                continue
             if status == "unverified":
                 # fail-closed: stays unverified, retried in a future pass.
                 continue
-            await store.set_status(claim["id"], status, verifier_model=model, now=now)
-            done += 1
+            try:
+                await store.set_status(claim["id"], status, verifier_model=model, now=now)
+                done += 1
+            except Exception:
+                continue
🤖 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 `@taosmd/claims/verify_pass.py` around lines 35 - 40, Per-claim exceptions in
the verification loop currently abort the whole pass; wrap the per-claim work
(the calls to fetch_spans, verifier.verify, and store.set_status for each claim)
in a try/except so any exception is caught, logged, and the loop continues
(leaving that claim unverified) rather than re-raising; specifically, enclose
the sequence spanning fetch_spans(claim["archive_span_ids"]),
verifier.verify(claim["text"], spans), and the subsequent await
store.set_status(...) in a try block and in except log the exception and
continue to the next claim to preserve fail-closed behavior.

Comment thread taosmd/cli.py
Comment on lines +998 to +999
import httpx # noqa: PLC0415

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether httpx is declared in packaging/dependency manifests.
fd -i 'pyproject.toml'
fd -i 'setup.py'
fd -i 'setup.cfg'
fd -i 'requirements.*\.txt'

rg -n --iglob 'pyproject.toml' --iglob 'setup.py' --iglob 'setup.cfg' --iglob 'requirements*.txt' '\bhttpx\b'

Repository: jaylfc/taosmd

Length of output: 93


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect the actual code around the reported lines in taosmd/cli.py
python3 - <<'PY'
import itertools
path = "taosmd/cli.py"
start = 980
end = 1030
with open(path, "r", encoding="utf-8") as f:
    for i, line in enumerate(f, 1):
        if start <= i <= end:
            print(f"{i}:{line.rstrip()}")
        if i > end:
            break
PY

# 2) Find all occurrences of "httpx" in packaging/manifest files
rg -n --hidden --no-ignore -S '\bhttpx\b' pyproject.toml setup.py setup.cfg taosmd/auto_setup.py requirements*.txt 2>/dev/null || true

# 3) If pyproject.toml exists, print the dependency sections (small, targeted extraction)
if [ -f pyproject.toml ]; then
  echo "---- pyproject.toml dependency sections (context around 'httpx') ----"
  rg -n -S 'httpx' pyproject.toml -n
  rg -n -S '(\[project\]|\[tool\.poetry\.dependencies\]|\[project\.dependencies\]|\[project\.optional-dependencies\])' pyproject.toml || true
fi

# 4) If requirements files exist, show their contents around httpx
if ls requirements*.txt >/dev/null 2>&1; then
  for f in requirements*.txt; do
    echo "---- $f (context around 'httpx') ----"
    rg -n -C 2 -S '\bhttpx\b' "$f" || true
  done
fi

Repository: jaylfc/taosmd

Length of output: 1985


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate dependency manifests
ls -la pyproject.toml setup.py setup.cfg requirements*.txt 2>/dev/null || true

# Search for declared dependency on httpx in pyproject.toml (and common sections)
if [ -f pyproject.toml ]; then
  echo "---- pyproject.toml lines mentioning httpx ----"
  rg -n -S '\bhttpx\b' pyproject.toml || true

  echo "---- pyproject.toml dependency sections headers ----"
  rg -n -S '(\[project\]|\[project\.dependencies\]|\[project\.optional-dependencies\]|\[tool\.poetry\.dependencies\]|\[tool\.poetry\.group\.[^]]*\.dependencies\])' pyproject.toml || true
fi

# Search for httpx across the repo to see if it’s declared anywhere else
rg -n -S '\bhttpx\b' . || true

Repository: jaylfc/taosmd

Length of output: 8554


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show pyproject dependency blocks with line numbers
python3 - <<'PY'
import itertools
p="pyproject.toml"
with open(p,encoding="utf-8") as f:
    for i,line in enumerate(f,1):
        if 1 <= i <= 80:
            pass
# print first 120 lines (pyproject is small: 1513 bytes)
with open(p,encoding="utf-8") as f:
    for i,line in enumerate(f,1):
        if i<=120:
            print(f"{i}:{line.rstrip()}")
        else:
            break
PY

# Check if requirements*.txt exist and contain httpx
ls -1 requirements*.txt 2>/dev/null || true
for f in requirements*.txt; do
  [ -f "$f" ] || continue
  echo "---- $f (httpx matches) ----"
  rg -n -S '\bhttpx\b' "$f" || true
done

# Inspect auto_setup.py for any dynamic install of httpx
python3 - <<'PY'
import re
path="taosmd/auto_setup.py"
with open(path,encoding="utf-8") as f:
    for i,line in enumerate(f,1):
        if i<=260:
            if 'httpx' in line or 'install' in line or 'requirements' in line:
                print(f"{i}:{line.rstrip()}")
PY

# Confirm whether any of the packaging manifests mention httpx at all
rg -n -S '\bhttpx\b' pyproject.toml setup.py setup.cfg requirements*.txt 2>/dev/null || true

Repository: jaylfc/taosmd

Length of output: 2201


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect taosmd/auto_setup.py around any httpx or dependency-install logic
python3 - <<'PY'
import re
path="taosmd/auto_setup.py"
with open(path,encoding="utf-8") as f:
    lines=f.readlines()

# Print only lines that reference installs/requirements/httpx, plus a small window
targets=[]
for i,line in enumerate(lines,1):
    if re.search(r'httpx', line, re.I) or re.search(r'pip\s+install|requirements|dependency|install_requires', line, re.I):
        targets.append(i)

for i in targets[:80]:
    start=max(1,i-10); end=min(len(lines), i+10)
    print(f"--- context {start}-{end} around line {i} ---")
    for j in range(start,end+1):
        print(f"{j}:{lines[j-1].rstrip()}")
PY

# 2) Find where auto_setup is invoked from the CLI path
rg -n -S 'auto_setup' taosmd/cli.py taosmd/*.py || true

# 3) Inspect taosmd/cli.py around verify command wiring (to see if any setup/auto-install runs)
python3 - <<'PY'
path="taosmd/cli.py"
with open(path,encoding="utf-8") as f: lines=f.readlines()
# Look for _verify_cmd usage and any call to auto_setup/main setup
for i,line in enumerate(lines,1):
    if 'def _verify_cmd' in line or '_verify_cmd(' in line or 'verify' in line and 'add_parser' in line:
        start=max(1,i-40); end=min(len(lines), i+40)
        print(f"--- context {start}-{end} around line {i} ---")
        for j in range(start,end+1):
            print(f"{j}:{lines[j-1].rstrip()}")
PY

Repository: jaylfc/taosmd

Length of output: 10748


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -S '\bhttpx\b' taosmd/auto_setup.py || true
rg -n -S 'pip\s+install|requirements|install_requires|dependency|ensurepip|subprocess\..*pip' taosmd/auto_setup.py || true

Repository: jaylfc/taosmd

Length of output: 39


taosmd verify imports httpx at runtime, but httpx isn’t declared in dependencies.

taosmd/cli.py imports httpx in the verify command handler, but pyproject.toml’s project.dependencies does not include httpx, and taosmd/auto_setup.py doesn’t install it—so taosmd verify can raise ModuleNotFoundError. Declare httpx in project.dependencies (or a dedicated extra) or handle ModuleNotFoundError with a clear install hint.

🤖 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 `@taosmd/cli.py` around lines 998 - 999, The verify command in taosmd/cli.py
imports httpx at runtime (see the import added for the verify handler) but httpx
is not declared or installed; either add "httpx" to project.dependencies in
pyproject.toml (or create a dedicated extra) so it’s installed, or modify the
verify handler to catch ModuleNotFoundError around the httpx import and raise a
clear error message instructing the user to pip install httpx (and update
taosmd/auto_setup.py if you choose to auto-install it so the setup logic
installs the new dependency).

Comment thread taosmd/cli.py
Comment on lines +1488 to +1491
verify_p.add_argument(
"--batch", type=int, default=100,
help="Claims to pull per batch (default: 100)",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate --batch as a positive integer.

Line 1489 accepts non-positive values. --batch 0 or negative values can lead to unintended query behavior and effectively disable batching semantics.

🤖 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 `@taosmd/cli.py` around lines 1488 - 1491, The --batch argument currently uses
verify_p.add_argument("--batch", type=int, ...) and accepts zero or negative
values; add a validator function (e.g., def positive_int(value): ...) that
converts to int, checks value > 0, and raises argparse.ArgumentTypeError on
failure, then change verify_p.add_argument to use type=positive_int and keep the
same default/help; this ensures --batch only accepts positive integers and
produces a clear argparse error message when invalid.

Comment on lines +35 to +39
try:
asyncio.run(s.set_status(cid, "bogus", verifier_model="m", now=1.0))
assert False, "expected ValueError"
except ValueError:
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use explicit failure instead of assert False in the exception path.

Line 37 uses assert False, which triggers Ruff B011 and is less robust than an explicit exception.

Proposed fix
     try:
         asyncio.run(s.set_status(cid, "bogus", verifier_model="m", now=1.0))
-        assert False, "expected ValueError"
+        raise AssertionError("expected ValueError")
     except ValueError:
         pass
🧰 Tools
🪛 Ruff (0.15.15)

[warning] 37-37: Do not assert False (python -O removes these calls), raise AssertionError()

Replace assert False

(B011)

🤖 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/claims/test_store.py` around lines 35 - 39, Replace the fragile "assert
False, 'expected ValueError'" in the test for s.set_status with an explicit test
failure call (e.g., pytest.fail("expected ValueError")) so the test triggers a
clear failure instead of using assert False; update the test in
tests/claims/test_store.py around the s.set_status(cid, "bogus",
verifier_model="m", now=1.0) try/except block to call pytest.fail in the try
branch and keep the except ValueError: pass as the expected path.

Source: Linters/SAST tools

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.

1 participant