Skip to content

feat: acquisition hardening — opencli fetch() migration, catalog-driven demand matching, dedup proof - #42

Merged
2233admin merged 4 commits into
mainfrom
feat/acquisition-hardening
Jul 25, 2026
Merged

feat: acquisition hardening — opencli fetch() migration, catalog-driven demand matching, dedup proof#42
2233admin merged 4 commits into
mainfrom
feat/acquisition-hardening

Conversation

@2233admin

Copy link
Copy Markdown
Owner

What changed

  • migrate opencli channel (846L, largest acquisition surface / 205-site external CLI catalog) onto the thick fetch() contract: runner-level error taxonomy + rate-limited client wiring; collect() behavior untouched (0a190bc)
  • demand assembler now matches needs against the real opencli adapter catalog (list_opencli_adapter_nodes(include_write=False)): exact site/command > Chinese alias > description keyword, deterministic order, capped at 3 slots; catalog failure fails soft to the legacy Xiaohongshu/Bilibili keyword floor; no-match still fail-closed to request_missing_capability (4cb6c94)
  • dedup investigation concluded storage-level per-source dedup was ALREADY fully enforced (uq_source_content + unconditional content_hash + in-batch dedup + SAVEPOINT retry + duplicates measurement chain) — added proof tests + schema docs instead of a redundant second mechanism (c0d9915)

Why

Acquisition-layer hardening pass: retry/limit machinery coverage for the biggest source surface, needs-to-source strategy upgraded from a 2-entry keyword table to catalog lookup, and the dedup guarantee made explicit and regression-locked.

Validation

  • tests/unit: 1431 passed, 1 skipped (2 pre-existing Windows-only GBK env failures excluded, tracked separately)
  • new tests verified load-bearing against reverted implementations (git stash cross-check)
  • live fail-soft confirmation: catalog GBK crash fired inside integration tests and was absorbed by the fallback path
  • ruff clean on touched files

Notes

Stacked on #41 (codex/dataflow-native-cleaning, base of this PR). Retarget to main after #41 merges.

What: OpenCLIChannel now overrides fetch() (narrow delegation to the
existing collect(), mirroring BrowserActChannel's pattern) instead of
relying on AbstractChannel's default adapter. capabilities.default_rate
is now spelled out explicitly (60/min, unchanged value); incremental/
paginated stay False — opencli's site/command catalog is an external
binary probed via --help at runtime, with no cursor or page-token
contract anywhere in this codebase to drive the runner's pagination
loop against.

Why: flips channel_runner.run_channel's `type(chan).fetch is not
AbstractChannel.fetch` check so opencli is treated as migrated: the
runner now builds it a RateLimitedClient from the declared rate (same
accepted no-op cost BrowserActChannel documents, since opencli's
transport is local subprocess / LAN-agent HTTP-WS, not ctx.http's
public-API shape) and a raised failure's error_type reaches
error_taxonomy.effective_error_type directly rather than through the
identical inherited default. collect() is untouched — same
site-routing/auth logic, same observable behavior; fetch() is pure
addition.

Tests: 6 new unit tests covering fetch() success via mocked
_run_opencli, retryable (TimeoutError) vs permanent (FileNotFoundError)
error-taxonomy classification through fetch()'s ChannelFetchError,
collect() not routing through fetch(), the migration identity check,
and run_channel actually building a RateLimitedClient for the real
channel. Full tests/unit: 1418 passed (was 1412), same pre-existing 2
GBK-encoding failures in test_nodes_install_script.py (unrelated,
backend/api/v1/nodes.py:435 read_text() with no explicit encoding).
…r catalog

_source_slots_for_need() now tries the ~205-adapter opencli catalog
(via list_opencli_adapter_nodes) before falling back to the hardcoded
Xiaohongshu/Bilibili keyword floor. Matching is deterministic: exact
site id/name token > Chinese alias > description/domain/strategy
keyword, capped at 3 slots, read-access adapters only.

Catalog access is wrapped in try/except so a load failure (missing
binary, subprocess/decode error -- including the known GBK crash in
_load_opencli_catalog on this box) is treated as "catalog
unavailable" and falls back to the legacy keyword matcher, which is
unchanged. New tests mock the catalog boundary and never invoke the
real opencli CLI.
Investigated per task: content_hash (normalizer, unconditional) + DB
unique constraint uq_source_content(source_id, content_hash) +
pre-insert/in-batch checks + IntegrityError retry already fully
enforce per-source dedup at ingest for every write_strategy that owns
a DB row (legacy, odp_shadow/dual/primary), independent of the
workflow-graph text.deduplicate operator. Verified every call site
(LocalExecutor, Celery tasks, plan_ir executor) funnels through the
same run_pipeline -> storer path. alembic check: no model/migration
drift, no schema change needed.

No code gap found, so no new machinery added. Closed the one real
test gap (in-batch content_hash duplicate with no identity() —
previously only the cross-run and identity-based cases were covered)
plus a distinct-items sanity test, and documented the guarantee
(including the previously undocumented identity_key column) in
docs/schema.md.
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

OpenCLIChannel now implements the fetch contract with rate configuration and typed errors. Demand assembly uses OpenCLI adapter catalog matching before legacy fallback. Schema documentation and storer tests expand identity and batch deduplication coverage.

Changes

OpenCLI fetch migration

Layer / File(s) Summary
OpenCLI fetch contract and rate configuration
backend/channels/opencli_channel.py
Adds the fetch-related types, sets default_rate="60/min", and returns FetchResult or ChannelFetchError through collect().
Fetch and runner validation
tests/unit/channels/test_opencli_channel.py
Tests fetch override behavior, subprocess results, retry classification, collect routing, and rate-limited runner construction.

Catalog-aware demand assembly

Layer / File(s) Summary
Catalog matching and fallback resolution
backend/workflow/demand_assembler.py
Matches readable catalog adapters by deterministic token, alias, and description tiers before falling back to legacy keyword slots.
Catalog matching behavior tests
tests/unit/test_demand_assembler.py
Covers matching, filtering, fallback, result limits, ordering, and legacy behavior.

Ingestion deduplication documentation and tests

Layer / File(s) Summary
Collected record identity and deduplication documentation
docs/schema.md
Documents identity_key, its index, write-path deduplication, and write-strategy behavior.
Batch deduplication test coverage
tests/unit/pipeline/test_storer.py
Verifies duplicate hashes are skipped within a batch and distinct hashes are stored.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: lunnynight

Poem

I’m a bunny, and fetch now runs,
Through typed errors, slots, and suns.
Catalog paths hop into view,
Duplicate records shrink to two—
One true hash, one tidy row,
Carrots applaud the cleaner flow!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
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.
Title check ✅ Passed The title clearly summarizes the main changes: OpenCLI fetch migration, catalog-driven demand matching, and dedup validation.
Description check ✅ Passed The description is directly related to the PR and accurately describes the OpenCLI, demand matching, and dedup changes.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

@repowise-bot

repowise-bot Bot commented Jul 25, 2026

Copy link
Copy Markdown

✅ Health: 7.1

📋 At a glance
2 hotspots touched · 6 new findings introduced · 1 co-change pair left out.

Files & modules (2)
  • backend (1 file)
    • backend/channels/opencli_channel.py
  • tests (1 file)
    • .../channels/test_opencli_channel.py

📌 Before you merge

  • Run tests/integration/test_opencli_channel_api.py, tests/conftest.py: they depend on the changed files
  • backend/agent_server.py changed together with backend/channels/opencli_channel.py in 10 past commits and isn't in this PR

🗺️ Change map

flowchart LR
  subgraph PR ["Changed in this PR (2 with dependents)"]
    f_backend_channels_opencli_channel_py["backend/channels/opencli_channel.py 🔥"]:::changed
    f_tests_unit_pipeline_test_storer_py[".../pipeline/test_storer.py"]:::changed
  end
  f_backend_agent_server_py["backend/agent_server.py"]
  f_backend_channels_opencli_channel_py --> f_backend_agent_server_py
  f_backend_api_v1___init___py[".../v1/__init__.py"]
  f_backend_channels_opencli_channel_py --> f_backend_api_v1___init___py
  f_backend_api_v1_browsers_py[".../v1/browsers.py"]
  f_backend_channels_opencli_channel_py --> f_backend_api_v1_browsers_py
  f_backend_api_v1_nodes_py[".../v1/nodes.py"]
  f_backend_channels_opencli_channel_py --> f_backend_api_v1_nodes_py
  more(["+2 more dependents"])
  PR --> more
  w_backend_agent_server_py(["⚠️ backend/agent_server.py changed together 10×, not in PR"]):::warn
  f_backend_channels_opencli_channel_py -.- w_backend_agent_server_py
  t_tests_integration_test_opencli_channel_api_py(["✅ tests/integration/test_opencli_channel_api.py"]):::guard
  t_tests_integration_test_opencli_channel_api_py -.-> f_backend_channels_opencli_channel_py
  t_tests_conftest_py(["✅ tests/conftest.py"]):::guard
  t_tests_conftest_py -.-> f_tests_unit_pipeline_test_storer_py
  classDef changed fill:#dbeafe,stroke:#1d4ed8,color:#1e3a5f
  classDef warn fill:#fef3c7,stroke:#b45309,color:#78350f
  classDef guard fill:#dcfce7,stroke:#15803d,color:#14532d
Loading

Solid arrows: code that imports the changed files (6 direct dependents, from the last indexed snapshot). Dashed: history/tests.

🚨 Change risk: 9.4/10 (high)
This change's risk is driven by:

  • more lines added than baseline
  • more scattered than baseline
🔎 More signals (2)

🔥 Hotspots touched (2)

  • backend/channels/opencli_channel.py — 31 commits/90d, 8 dependents · primary owner: xujinghua (100%)
  • .../channels/test_opencli_channel.py — 3 commits/90d, 0 dependents · primary owner: xujinghua (100%)

🔗 Hidden coupling (1 file)

  • backend/channels/opencli_channel.py co-changes with backend/agent_server.py (10× — 🟡 notable) — not in this PR.

👀 Suggested reviewers @xujinghua


📊 Full report · ⭐ Star Repowise · 📥 Install bot · Last updated 2026-07-25 09:56 UTC
Silence on a single PR with [skip repowise] in the title · Per-repo toggle on repowise.dev/settings?tab=bot

@2233admin

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

🧹 Nitpick comments (3)
backend/channels/opencli_channel.py (1)

812-849: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Delegate instead of duplicating AbstractChannel.fetch()'s body.

This override exists purely to flip type(chan).fetch is not AbstractChannel.fetch for channel_runner's migration check — the docstring says so explicitly. But the body (Lines 842-848) is a verbatim copy of AbstractChannel.fetch()'s default implementation (backend/channels/base.py). If that default ever changes (e.g. different metadata merging, different error wrapping), this copy silently drifts out of sync with no test or type system enforcement to catch it.

Delegating to the base implementation achieves the same identity-check goal with zero duplication risk:

♻️ Proposed refactor
         result = await self.collect(ctx.config, ctx.params)
-        if not result.success:
-            raise ChannelFetchError(
-                result.error or f"{self.channel_type} collect failed",
-                error_type=result.error_type,
-            )
-        return FetchResult(items=result.items, metadata=result.metadata)
+        return await AbstractChannel.fetch(self, ctx)

(type(chan).fetch still resolves to OpenCLIChannel.fetch, a distinct function object from AbstractChannel.fetch, so the migration check still passes — this just removes the duplicated logic.)

🤖 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 `@backend/channels/opencli_channel.py` around lines 812 - 849, Update
OpenCLIChannel.fetch to delegate directly to AbstractChannel.fetch instead of
duplicating its collect, error-wrapping, and FetchResult construction logic.
Preserve the override itself so type(chan).fetch remains distinct from
AbstractChannel.fetch for the migration check, and pass through the existing
context unchanged.
tests/unit/channels/test_opencli_channel.py (2)

1242-1281: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assertion doesn't verify the claimed rate value.

The docstring says this proves the client is "built from its declared default_rate", but mock_rlc.assert_called_once() only checks that RateLimitedClient was constructed once — it doesn't check the constructor was actually given a token bucket configured for "60/min". A regression that hardcoded a different rate elsewhere wouldn't be caught here.

🤖 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/unit/channels/test_opencli_channel.py` around lines 1242 - 1281, Update
test_run_channel_builds_rate_limited_client_for_opencli to inspect the arguments
passed to RateLimitedClient and assert its token-bucket configuration uses the
OpenCLI channel’s declared default_rate of “60/min”, while preserving the
existing construction-count assertion and result checks.

1135-1157: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider also asserting metadata passthrough.

The test only checks result.items; FetchResult.metadata forwarding (called out in backend/channels/base.py's FetchResult docstring as important for opencli's node_url/chrome_mode) isn't exercised here. Not a functional gap, just an easy strengthening of this specific test's coverage.

🤖 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/unit/channels/test_opencli_channel.py` around lines 1135 - 1157,
Strengthen test_fetch_returns_items_via_mocked_subprocess by asserting that
result.metadata preserves the metadata returned through the mocked opencli fetch
flow, including opencli-relevant fields such as node_url and chrome_mode. Keep
the existing items assertion and use the established FetchResult metadata shape.
🤖 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 `@backend/workflow/demand_assembler.py`:
- Around line 542-554: Update _catalog_match_tier so tier 0 is awarded only when
the normalized text contains a valid site token; remove the command-based
condition from that tier. Leave alias matching and description-token matching
unchanged.

---

Nitpick comments:
In `@backend/channels/opencli_channel.py`:
- Around line 812-849: Update OpenCLIChannel.fetch to delegate directly to
AbstractChannel.fetch instead of duplicating its collect, error-wrapping, and
FetchResult construction logic. Preserve the override itself so type(chan).fetch
remains distinct from AbstractChannel.fetch for the migration check, and pass
through the existing context unchanged.

In `@tests/unit/channels/test_opencli_channel.py`:
- Around line 1242-1281: Update
test_run_channel_builds_rate_limited_client_for_opencli to inspect the arguments
passed to RateLimitedClient and assert its token-bucket configuration uses the
OpenCLI channel’s declared default_rate of “60/min”, while preserving the
existing construction-count assertion and result checks.
- Around line 1135-1157: Strengthen
test_fetch_returns_items_via_mocked_subprocess by asserting that result.metadata
preserves the metadata returned through the mocked opencli fetch flow, including
opencli-relevant fields such as node_url and chrome_mode. Keep the existing
items assertion and use the established FetchResult metadata shape.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 41c7267b-eddf-4f4b-9ddb-e9cb3e9c74bb

📥 Commits

Reviewing files that changed from the base of the PR and between a2614bd and c0d9915.

📒 Files selected for processing (6)
  • backend/channels/opencli_channel.py
  • backend/workflow/demand_assembler.py
  • docs/schema.md
  • tests/unit/channels/test_opencli_channel.py
  • tests/unit/pipeline/test_storer.py
  • tests/unit/test_demand_assembler.py

Comment thread backend/workflow/demand_assembler.py
- demand_assembler: drop command-only matches from tier 0 — command names
  repeat across unrelated sites and must not outrank exact site matches
- opencli_channel.fetch(): delegate to AbstractChannel.fetch instead of
  duplicating its body (override kept for the migration identity check)
- tests: assert RateLimitedClient token bucket is built from the declared
  60/min default_rate; assert fetch() metadata passthrough (chrome_mode)
@2233admin
2233admin changed the base branch from codex/dataflow-native-cleaning to main July 25, 2026 10:08
@2233admin
2233admin merged commit e897d68 into main Jul 25, 2026
6 checks passed
@2233admin
2233admin deleted the feat/acquisition-hardening branch July 25, 2026 10:08
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