Skip to content

feat(phase9 #100 D10.h): legacy search hard-cut + web_search canonicalization + retrieval handle exposure + D10 hurl coverage#1721

Merged
earayu merged 7 commits into
mainfrom
bryce/phase9-task100-d10h-cutover
Apr 26, 2026
Merged

feat(phase9 #100 D10.h): legacy search hard-cut + web_search canonicalization + retrieval handle exposure + D10 hurl coverage#1721
earayu merged 7 commits into
mainfrom
bryce/phase9-task100-d10h-cutover

Conversation

@earayu

@earayu earayu commented Apr 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

D10.h cutover, single PR per design pack §G + amendment-#2 + PM final scope lock (msg=dc63c7e6 / msg=5760999e). The system is pre-launch with no users / no data per @earayu2 msg=9730bb6b, so the soak / data-migration gate that previously guarded task #100 is dropped — only the code-level gates (caller sweep, write-set boundary, CI green) apply.

Write set

Block Files Author
A — search_collection / search_chat_files hard-cut + caller sweep aperag/mcp/server.py (delete two omnibus tools + rewrite the aperag_usage_guide resource), aperag/domains/agent_runtime/services.py (_KNOWLEDGE_SEARCH_TOOLS / _READING_TOOLS) Bryce
B — web_search §B.4 canonicalization aperag/mcp/tools/search_web.py (query required, kw-only, top_k, source: str | None), tests/unit_test/mcp/test_search_split.py (signature pin) Bryce
C — retrieval chunk_id / section_path / heading_anchor exposure aperag/domains/retrieval/schemas.py, new tests/unit_test/domains/retrieval/test_search_result_metadata.py architect (bec7bde5)
D — stale narrative cleanup aperag/mcp/cursor/invariants.py (pending architect canonical lock per msg=441c5e56 removed) Bryce
E — D10 hurl coverage tests/e2e_http/hurl/full/22_d10_read_primitives.hurl / 23_d10_pagination.hurl / 24_d10_search_split.hurl / 25_d10_cutover.hurl Bryce
F — caller migration assertion semantics tests/unit_test/mcp/test_search_split.py, tests/unit_test/test_mcp_server.py, tests/unit_test/test_mcp_contract.py, tests/unit_test/agent_runtime/test_agent_runtime_v3.py Bryce

§G hard gates

  • feat/frontend #1 contract shape change → 3-root grep: confirmed search_collection / search_chat_files runtime references are gone everywhere except the agent-runtime translator string fixtures (which intentionally exercise routing on arbitrary tool-name strings) and a source_type: "search_collection" test fixture in chat history (D8.5-BE artifact, harmless string token). Both are non-functional and out of D10.h scope.
  • feat: api test #3 bridge / adapter deletion → caller validation: every place that used mcp_server.search_collection / search_chat_files as a runtime attribute is migrated in this same PR (services.py routing set, mcp_server / mcp_contract / agent_runtime_v3 tests).
  • fix: upload token #4 caller migration assertion semantics: pre-existing tests that pinned the omnibus tools' presence are not deleted — they are rewritten in the same PR to pin their absence (test_search_collection_legacy_omnibus_removed_from_module, etc).
  • fix: create collection bug #5 cross-stack design boundary: task #80 territory deliberately untouched (snapshot_assembler, RedisChatMessageHistory, StoredChatMessagePart, chat/history, utils/history); architect-confirmed in inventory msg=b9e7f91e.

Cross-block gate

24_d10_search_split.hurl asserts chunk_id / section_path / heading_anchor appear on the wire. Those assertions were placed before block C landed so the gate would fail loudly if Drift #1 propagation didn't ship in the same PR — block C's SearchResultMetadata allowlist update closes that gate.

Out of scope (deferred per PM constraint)

  • aperag/service/search_service.py "legacy compatibility layer" — never created in the repo, so nothing to delete (PM msg=dc63c7e6 feat: api test #3).
  • D8 fallback / artifact projection / RedisChatMessageHistory cleanup — task #80 territory, owned by @明书 on PR feat(phase8 #80 D8.6 chunk-1): hard-cut RedisChatMessageHistory + StoredChatMessage* legacy #1720+ (PM msg=309b3ed3 feat/frontend #1).
  • Indexing-layer section_path / heading_anchor attach across the 5 indexer modes (vector_index / graph_index / summary_index / vision_index / fulltext_index) — explicit out-of-scope per PM msg=5760999e fix: upload token #4 (multi-indexer expansion would balloon the write-set). The schema field is exposed; populated values for those handles arrive once each indexer is taught to attach section context, which is a future lane.
  • Backend aperag/domains/web_access/schemas.py max_resultstop_k rename — the wrapper translates at the MCP boundary (PM constraint feat: auth bearer token support #2 msg=309b3ed3, "目标是 cutover contract 收口").

Test plan

  • uv run pytest tests/unit_test/ — 1001 passed, 29 skipped
  • uv run ruff check + uv run ruff format --check — clean
  • CI lint-and-unit / e2e-http-smoke / provider-preflight / e2e-http-provider — pending PR open

Reviewers per RR2

  • @符炫炜 architect spec line-by-line quick-check (per memory feedback_dataflow_review.md step 0+ enforcement + step 0 dataflow trace; this lane is co-owned so the architect's own block C is intentionally excluded from the architect's quick-check perimeter)
  • @weston optional二线 sanity, focused on the destructive cutover caller sweep + the hard-cut narrative consistency

🤖 Generated with Claude Code

Bryce and others added 7 commits April 26, 2026 09:16
…cy delete + web_search canonicalize + cursor narrative

Per design pack §G D10.h + amendment-#2 + PM scope lock (msg=dc63c7e6):

Block A — search_collection / search_chat_files hard-cut + caller sweep:
- aperag/mcp/server.py: delete `search_collection` (line 295-465 ~170 LOC) + `search_chat_files` (468-583 ~116 LOC) bodies; rewrite the `aperag_usage_guide` resource docstring to document the canonical D10 split tools (vector_search / graph_search / fulltext_search / web_search) instead of the deprecated omnibus surface; replace the "deferred to D10.h cutover" NOTE comment with a closure-state note pointing at the split tools.
- aperag/domains/agent_runtime/services.py: update `_KNOWLEDGE_SEARCH_TOOLS` from `{"list_collections", "search_collection"}` to the split-tool set `{list_collections, list_documents, vector_search, graph_search, fulltext_search}`; expand `_READING_TOOLS` to cover the 6 D10.c read primitives so user-activity routing tracks the post-cutover surface end-to-end.

Block B — web_search §B.4 canonicalization:
- aperag/mcp/tools/search_web.py: `query` is now positional + required (raises ValueError on empty); every other parameter is keyword-only; the result-count limit is named `top_k` (not `max_results`); `source` is `str | None` so a missing domain filter is null. The wrapper still passes `max_results` to the internal `/api/v2/web/search` payload — backend rename is intentionally out of this lane (PM constraint #2 msg=309b3ed3 "目标是 cutover contract 收口,不是重写 hurl 框架").
- tests/unit_test/mcp/test_search_split.py: replace the deferred-shape signature pin with `test_web_search_signature_matches_b4_canonical` — asserts query is required, the 4 other params are kw-only with the canonical names + defaults, and the legacy `max_results` parameter is gone.

Block D — stale narrative cleanup:
- aperag/mcp/cursor/invariants.py: drop the "(pending architect canonical lock per msg=441c5e56)" parenthetical now that #1710 has merged the canonical lock to main.

Block C (retrieval `chunk_id` / `section_path` / `heading_anchor` propagation per Drift #1) is left for the architect to author on this same branch per msg=b9e7f91e cross-lane offer; block E (D10 hurl coverage suite) follows after C lands.

#80 territory deliberately untouched (snapshot_assembler / RedisChatMessageHistory / StoredChatMessagePart / chat/history / utils/history) per PM msg=309b3ed3 #100/#80 disjoint lock.
4 new hurl files exercise the post-cutover MCP wire surface end-to-
end; mirror 21_d10_capabilities's tools/list + substring-contains
pattern (the wire is JSON-RPC framed inside SSE, so jsonpath would
skip the envelope):

- 22_d10_read_primitives.hurl — pin the §A.1-§A.8 input schema names
  for every paginated and metadata read primitive plus the
  ``read_document`` byte-range parameters and the ``read_document_chunk``
  stable handle.
- 23_d10_pagination.hurl — pin §C.5 PaginationParams (cursor + limit)
  inputs and PaginationResult (items + next_cursor + total_count)
  outputs on every paginated read primitive.
- 24_d10_search_split.hurl — pin the §B.1-§B.4 split-search surface
  and the §B.4 canonical parameter set (top_k present, max_results
  intentionally absent). Includes assertions on
  chunk_id/section_path/heading_anchor that exercise the retrieval-
  propagation block authored by the architect on this same branch —
  these will fail until that commit lands, which is the intended
  cross-block gate.
- 25_d10_cutover.hurl — cutover gate: every canonical D10 tool name
  is still on tools/list, the legacy search_collection / search_chat_files
  omnibus pair is gone. A future regression that re-registers either
  legacy tool fails CI loudly before merge.

Per PM constraint #2 (msg=309b3ed3) the hurl coverage matches this
hard-cut one-for-one — no infra refactor, no new transport, just
substring-contains over the existing ``/mcp/`` mount.
…ding_anchor on SearchResultMetadata

Per amendment-#2 Drift #1 (msg=ebfcdabe) + PM final scope lock
(msg=dc63c7e6 / msg=5760999e) — D10.h cutover Block C: surface the 3
LOCKED §A.9 stable handle fields on the retrieval domain public
allowlist so external Agents (Claude Code / Codex / Cursor) can
navigate from search hits back to the canonical D10.c read primitives
(``read_document_chunk(chunk_id)`` / ``read_document_section(section_path)``).

## Changes

- ``aperag/domains/retrieval/schemas.py``:
  - ``SearchResultMetadata`` allowlist adds 3 fields (``chunk_id``,
    ``section_path``, ``heading_anchor``). The model keeps
    ``extra="forbid"`` — adding the fields does not relax the
    allowlist.
  - ``SearchResultMetadata.from_raw()`` extends extraction so the
    3 fields propagate from upstream backends that already include
    them (e.g. ``aperag/domains/indexing/fulltext_index.py:541-553``
    already surfaces ``chunk_id``).

- ``tests/unit_test/domains/retrieval/test_search_result_metadata.py``
  (NEW) — 7 tests pin the contract:
  1. Each of the 3 fields is constructible.
  2. Unknown keys still rejected (``extra="forbid"`` regression).
  3. ``from_raw()`` extracts all 3 when present.
  4. Missing fields surface as ``None`` (upstream propagation gap is
     not a schema break).
  5. Non-string / empty values filtered so the public surface never
     carries a numeric chunk_id or empty string.

## Out of scope (per PM msg=5760999e #4 constraint)

Indexing-layer attachment of ``section_path`` / ``heading_anchor`` to
chunk metadata at index time is NOT included here. Per the constraint,
multi-indexer expansion (vector / fulltext / graph / summary / vision
each writing section context) would balloon the write-set; we keep
D10.h Block C as a 1-2-touch retrieval-domain surface change. The
3 fields surface as ``None`` until the indexing-layer enhancement
lands in a follow-up. ``chunk_id`` populates immediately via the
existing fulltext index ``_source.chunk_id`` propagation
(``fulltext_index.py:541-553``).

## §G hard gate compliance

- #1 (3-root grep): no caller assertion drift on
  ``SearchResultMetadata`` shape outside the allowlist additions
  (allowlist additions are additive).
- #5 (cross-stack): only ``aperag/domains/retrieval/schemas.py`` +
  ``tests/unit_test/domains/retrieval/`` touched on the architect
  side; Bryce's Block A/B/D commits cover the rest of D10.h scope.

Block C complement to architect+Bryce co-own #100 (msg=a17a4017
execution split: architect commits Block C on shared branch).
… cutover

Per §G hard gate #4 the cutover lane is the right place to update the
test surface that previously pinned the legacy ``search_collection``
behaviour, so the assertions match the post-cutover reality:

- tests/unit_test/mcp/test_search_split.py:
  drop the two ``[DEPRECATED]`` banner tests and the two
  body-still-targets-v2 tests; replace them with two cutover-removal
  tests (``test_search_collection_legacy_omnibus_removed_from_module``
  / ``test_search_chat_files_legacy_omnibus_removed_from_module``)
  that pin both the runtime attribute absence and the absence of the
  ``async def`` in source. Inline the ``_async_def_source`` ast walk
  into the one remaining caller (``test_web_search_module_targets_v2_web_path``).
- tests/unit_test/test_mcp_server.py:
  rename ``test_search_collection_docstring_explains_step_and_failure_meaning``
  to ``test_search_collection_legacy_omnibus_no_longer_registered`` —
  the user-visible step language for the new split tools is covered
  in ``test_search_split.py`` so this file just pins the absence.
- tests/unit_test/test_mcp_contract.py:
  rewrite the ``search_collection`` URL-and-import invariants as
  ``test_legacy_search_collection_omnibus_stays_removed`` +
  ``test_search_result_legacy_import_stays_gone``. The original tests
  guarded the Phase 2 retrieval hard-cut (URL must be v2, SearchResult
  must come from the retrieval domain); the post-cutover invariant is
  that neither symbol re-enters the server module at all.
- tests/unit_test/agent_runtime/test_agent_runtime_v3.py:
  ``test_event_service_to_event_envelope_adds_user_activity_contract``
  switches its sample tool name from the removed ``search_collection``
  to ``vector_search`` so the user-activity inference contract is
  exercised against the canonical ``_KNOWLEDGE_SEARCH_TOOLS`` set
  updated in ``aperag/domains/agent_runtime/services.py``.

1001/1001 unit tests pass; ruff check + format clean.
…itive hurl gate + stale split-tool docstrings

Per Weston msg=8a691444:

1. tests/fixtures/mcp_agent.py
   The ``searcher`` agent's instructions still pointed at the omnibus
   ``search_collection()`` call — a real instruction surface, not just
   prose. After the cutover that tool is gone from MCP ``tools/list``
   so the fixture would teach the agent to call a non-existent tool.
   Migrate the instruction to the canonical D10 split-search flow:
   ``list_collections`` first, then compose ``vector_search`` /
   ``fulltext_search`` / ``graph_search`` per the question, and chain
   into ``read_document_chunk`` / ``read_document_section`` via the
   ``chunk_id`` / ``section_path`` handles on each ``SearchResultItem``.

2. tests/e2e_http/hurl/full/24_d10_search_split.hurl
   The three ``contains "\"chunk_id\""`` / ``"\"section_path\""`` /
   ``"\"heading_anchor\""`` assertions claimed to gate the
   ``SearchResultItem.metadata`` outputSchema, but the split-search
   tools return ``Dict[str, Any]`` whose FastMCP outputSchema is
   ``additionalProperties: true`` — those substrings would actually
   be matched by unrelated read-primitive input schemas (e.g.,
   ``read_document_chunk(chunk_id=...)``), making the hurl a
   false-positive gate. Drop the three assertions and add a header
   comment pointing at the proper Pydantic-level pin in
   ``tests/unit_test/domains/retrieval/test_search_result_metadata.py``
   (which the architect's block C already authored).

3. aperag/mcp/tools/search_{vector,graph,fulltext}.py module docstrings
   Each one still narrated the omnibus deprecation timeline ("the
   alias remains until D10.h cutover") even though D10.h is the lane
   that just deleted it. Rewrite the three module docstrings to
   describe the post-cutover state: each split tool is the sole
   public entry point for its recall mode, and the retrieval
   ``SearchResultMetadata`` allowlist now exposes the three §A.9
   stable handle fields. Non-blocking per Weston, but cleaner to
   land while we are touching this lane.

1001 unit tests pass; ruff check + format clean.
CI e2e-http-provider failed at ``22_d10_read_primitives.hurl:52`` with
``the HTTP method <body> is not valid``. The four new hurl files I
added in block E left the body assertions hanging directly under
``HTTP 200`` without an ``[Asserts]`` block, so the hurl parser tried
to read each ``body contains "..."`` line as the start of a new
request (looking for an HTTP method like GET/POST/etc).

The existing ``21_d10_capabilities.hurl`` (which my files were
modeled on) does have ``[Asserts]`` after its final ``HTTP 200`` —
I missed that line when copying the pattern. Add it to all four:

- 22_d10_read_primitives.hurl
- 23_d10_pagination.hurl
- 24_d10_search_split.hurl
- 25_d10_cutover.hurl

Same hurl-only fix pattern as huangheng's #1719 follow-up
(`379d2535`): no production code touched, only the hurl assertion
framing.
…in 23_d10_pagination.hurl

CI run on head ``7d64991`` failed at
``23_d10_pagination.hurl:67`` and ``:68`` — same false-positive
pattern Weston already flagged for 24_d10_search_split.hurl
(msg=8a691444). The MCP tool functions in ``aperag/mcp/server.py``
are typed ``-> Dict[str, Any]``, so FastMCP exposes only
``"outputSchema": {"additionalProperties": true}`` on the
``tools/list`` wire — the ``items / next_cursor / total_count``
PaginationResult envelope field names never reach the body, and
``body contains "\\"next_cursor\\""`` / ``"\\"total_count\\""``
genuinely fail.

The PaginationParams input names (``cursor / limit``) DO surface
on the wire because they are inputSchema parameters; those four
substrings stay. Replaced the misleading "FastMCP emits the
Pydantic model JSON schema" comment with a header note pointing at
the cursor unit suite, which is where the envelope-shape pin
already lives (``tests/unit_test/mcp/test_cursor_contract.py`` +
``tests/unit_test/service/test_pagination_helper.py``).

Same hurl-only fix pattern as the previous ``[Asserts]`` push and
huangheng's #1719 ``379d2535`` follow-up — no production code
touched.
@earayu
earayu merged commit 9109f81 into main Apr 26, 2026
4 checks passed
@earayu
earayu deleted the bryce/phase9-task100-d10h-cutover branch April 26, 2026 02:38
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