Skip to content

promote: graph-path promotion as a server verb (MCP promote tool, ADR 0018) - #517

Merged
t0k0sh1 merged 7 commits into
mainfrom
466-promote-mcp-tool
Aug 9, 2026
Merged

promote: graph-path promotion as a server verb (MCP promote tool, ADR 0018)#517
t0k0sh1 merged 7 commits into
mainfrom
466-promote-mcp-tool

Conversation

@t0k0sh1

@t0k0sh1 t0k0sh1 commented Aug 9, 2026

Copy link
Copy Markdown
Owner

#466 S2(再評価コメントの分割 2/3): graph 経路の昇格を server verb として束ねる — ADR 0018

何をするか

POST /contexts/{name}/promote + MCP tool promote: scratch の source id 列を、確立済みの恒久 context へ export → filter → re-head → import → audit の 1 リクエストで昇格する。経路上に LLM なし・新 credential 面なし(ADR 0009 §13 の境界は不変)。

  • 転送は export/import の round trip そのもの(第3の write path を作らない): snapshot を指名 source に filter(各 edge は指名 source の寄与分のみ、count/weight 再計算 — unsourced 残差は構成上運ばれない)→ into 宛に render → /import と同じ retract-then-apply で適用。再昇格は冪等。alias は canonical が昇格 slice に live なものだけ運ばれ、残りは aliases_dropped で計数。
  • provenance は丸ごと移動: source id・stored_atdate・tags が原文どおり運ばれ、昇格後も citation はセッションを指す。
  • 作らない・消さない: into は既存必須(create block は strip — 競合削除時は NoContext 拒否)。scratch の退役は明示の retract_source のまま(step 5/6)。
  • 誤記防御: 存在しない source id は全体拒否(path-addressed issues、nothing_written)。予約 id(export:unsourced/export:empty)も拒否。
  • audit 同梱・判断は非同梱: 適用後に into の consolidation audit(3 check・既定上限)が audit で返る — candidates のみ。audit: false で opt-out、失敗時は audit_skipped(書き込みは既に durable のため degrade)。?dry_run=true は同形 preview・無書込・無 audit。
  • Write role(retract_source と同分類 — context を作れず group/schema を運ばない)。into/import の body-context 同様に handler で scope 判定。router 経由は scratch の shard へ丸ごと proxy(両 context 同一 shard 要件 — route.rs の divergence リストに追記)。

併せて直した既存バグ(dry-run 構築中に発覚)

POST /import?dry_run=true は cross-batch 状態を持たず、実 import が通るストリームを 2 パターンで誤拒否していた:

  1. alias が canonical より後のバッチに乗る場合(すべての export — alias は最終バッチ)→ 偽 UnknownCanonical
  2. 新規 context 名への restore の 2 バッチ目以降(create block は先頭バッチのみ)→ 偽 no_context

PreviewSeeds で「先行バッチが intern/create するはずのもの」を各バッチの検査に種付けして解消。cross-batch alias conflict は引き続き非予測(preview が実行より甘くなる方向 = 容量チェックと同じ advisory 側)。

テスト

  • unit: filter_to_sources(指名 share のみ・残差ゼロ・schema 非搬送)、MCP routing(dry_run=query/引数拒否)、auth role 表
  • 統合(tests/http_api/promote.rs): 移動と冪等性(share 分離・provenance・alias 会計・audit 同梱)、dry-run 無書込、拒否系(空/自己/宛先なし/未知 id/予約 id)、宛先 strict schema の判定+scratch schema 非感染、export ストリームの dry-run round trip(PreviewSeeds)、MCP tool e2e

Mutation gate

diff-scoped で予算(60)超過(初回 78 件)。over-budget の規定は module sweep だが、本 diff は新規モジュール(api/promote.rs)+既存 5 ファイルへの小追加で、既存ファイルの sweep は無関係な既存 mutant を数百件回すことになるため、S1(PR #513)の前例に従い全件をローカルで diff-scoped 実行し、missed ゼロまで解消した:

  • 初回 78 件 → 24 missed → kill テスト 12 本追加(scoped-key ゲート、retract 済み source の昇格拒否、refusal detail の各フィールド、warn-mode 会計、quota pre-check、weight 平均再計算、strip_create 契約、preview の phantom passage_dropped)+タイミング競合でのみ到達可能な 4 アームを helper 抽出 #[mutants::skip](理由コメント付き: budget/render の途中失効、quota の durable>0 形、audit 失敗理由マップ)+比較面の除去 2 箇所(warn-issue cap の無条件 extend+truncate、available_sources の純関数抽出 — count-0 attribution 残渣は HTTP 経由で構成不能のため unit で直接 pin)
  • 最終検証: 69 mutants tested in 52m: 58 caught, 11 unviable, 0 missed

分割の位置づけ

https://claude.ai/code/session_01UPjzCLVuXQmxhGkr6phPb9

Summary by CodeRabbit

  • 新機能

    • スクラッチコンテキストの指定ソースを既存コンテキストへ昇格できるようになりました。
    • HTTP APIとMCPの両方で、dry-run、冪等実行、監査結果、権限・スキーマ検証に対応しました。
    • 本文、属性、タグ、日付、関連情報、帰属データを保持して移行できます。
    • ソース欠落時は全体を拒否し、部分的な書き込みを防止します。
  • バグ修正

    • 複数バッチのインポートdry-runで先行バッチの状態を反映し、後続バッチを正しく検証できるようになりました。
  • ドキュメント

    • API仕様、昇格手順、利用ガイドを更新しました。

t0k0sh1 added 6 commits August 9, 2026 10:30
POST /contexts/{name}/promote and the MCP promote tool (#466 S2): the
named scratch sources move into an established destination context as
export -> filter -> re-head -> import -> audit, one request, no LLM
anywhere in the path. Each source moves whole (passage, date, tags,
only its own share of every edge; aliases exactly when their canonical
is live in the promoted slice, the rest counted), source ids survive,
and per-source retract-then-apply makes re-promotion idempotent. The
destination is never created, its own schema judges the incoming
batches, and the landing-zone consolidation audit rides back as
candidates. Write role, retract_source's classification; the body's
destination is scope-checked like /import's body contexts.

Building the dry run exposed two standing /import?dry_run defects the
same change fixes at the root (PreviewSeeds): a preview held no
cross-batch state, so an export-shaped stream whose aliases trail
their canonicals, and every post-first batch of a fresh-name restore,
refused spuriously where the real import applies cleanly.

Claude-Session: https://claude.ai/code/session_01UPjzCLVuXQmxhGkr6phPb9
Without the guard, a literal export:unsourced attribution (a prior
import round trip's residue) would slip through the source filter as
sourceless weight — sourceless weight deliberately cannot travel with
a promotion.

Claude-Session: https://claude.ai/code/session_01UPjzCLVuXQmxhGkr6phPb9
Scoped-key gate, retracted-source unavailability, refusal detail
fields, warn-mode envelope accounting, quota pre-check firing shape,
and the mean-not-product weight recomputation each get a pinning
test. The two timing-only refusal arms (budget spent mid-loop, render
dying mid-request) move into #[mutants::skip] helpers with the reason
in place; the quota refusal helper documents why its durable>0 shape
cannot be pinned (single destination, every batch carries growth).

Claude-Session: https://claude.ai/code/session_01UPjzCLVuXQmxhGkr6phPb9
The warn-issue cap loses its boundary comparison (unconditional
extend-then-truncate — each batch's issues are already capped, so the
overshoot is bounded); audit_skip_reason is skip-annotated (every arm
needs a post-apply race no test can time); strip_create's contract
gets a direct NoContext pin; and the preview's had_passage guard gets
a phantom-drop assertion on the dry-run shape.

Claude-Session: https://claude.ai/code/session_01UPjzCLVuXQmxhGkr6phPb9
The count-0 attribution rows the >-vs->= mutant needs cannot be
staged over HTTP (retraction removes the rows outright), so the
existence check moves next to filter_to_sources as a pure function
and the unit test constructs the residue directly.

Claude-Session: https://claude.ai/code/session_01UPjzCLVuXQmxhGkr6phPb9
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 82cf51b3-c718-40d0-a0ba-9f1a9a1c2614

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

構造化された scratch source を既存コンテキストへ昇格する HTTP API と MCP ツールを追加しました。ソースフィルタ、dry-run、冪等適用、監査、認可、スキーマ検証を実装しました。import dry-run はバッチ間状態を継承します。

Changes

グラフパス昇格

Layer / File(s) Summary
昇格契約とルーティング
adr/0018-graph-path-promotion.md, docs/promotion.html, examples/skills/memory-promotion/SKILL.md, src/api.rs, src/main.rs, src/mcp/*, src/auth.rs, src/route.rs, src/llm-protocol.md
HTTP の POST /contexts/{name}/promote と MCP の promote を追加しました。入力、認可、dry-run、監査、シャード制約を定義しました。
ソース単位のスナップショット生成
src/export.rs, src/ingest/model.rs
指定ソースのライブ帰属とパッセージだけを残し、関連の件数と平均重みを再計算します。バッチから create 指定を除去できます。
昇格 API の実行と結果集約
src/api/promote.rs, src/api/import.rs, src/api/consolidation.rs, tests/http_api/*
検証、再レンダー、インポート、容量・期限エラー、スキーマ警告、監査結果を処理します。HTTP と MCP の統合テストを追加しました。
dry-run のバッチ間状態継承
src/ingest/rejection.rs, src/ingest.rs, src/api/import.rs, src/ingest/tests.rs, CHANGELOG.md
PreviewSeeds で先行バッチの状態を後続バッチへ渡し、alias と context の誤った拒否を抑制します。

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

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant MCPRoute
  participant PromoteAPI
  participant ExportSnapshot
  participant ImportAPI
  Caller->>MCPRoute: promote(context, into, sources, dry_run)
  MCPRoute->>PromoteAPI: POST /contexts/{name}/promote
  PromoteAPI->>ExportSnapshot: filter_to_sources(sources)
  ExportSnapshot-->>PromoteAPI: filtered snapshot
  PromoteAPI->>ImportAPI: preview or apply batches
  ImportAPI-->>PromoteAPI: batch outcomes
  PromoteAPI-->>Caller: PromoteOutcome
Loading

Possibly related PRs

  • t0k0sh1/taguru#424: グラフパスの構造化された関連付けと走査機能を追加した PR です。今回のソース単位昇格がこのデータモデルを再利用します。
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed タイトルは、MCPのpromoteツールとサーバーAPIによるgraph-path promotionという主な変更を明確かつ簡潔に示しています。
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ 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 466-promote-mcp-tool

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 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 `@docs/promotion.html`:
- Around line 119-126: Update docs/promotion.html lines 119-126 to document the
HTTP contract as POST /contexts/{scratch}/promote?dry_run=true with body {into,
sources, audit?}, and state that dry runs preview transfer batches without
executing or returning audit results. Update
examples/skills/memory-promotion/SKILL.md lines 55-58 to state that dry_run:
true returns only the batches and that step 5 audit decisions require a real
apply.

In `@examples/skills/memory-promotion/SKILL.md`:
- Around line 51-53: Update the promote MCP tool example in the surrounding
memory-promotion instructions to include the required context argument alongside
into and sources, using the scratch context referenced by the text; preserve the
existing source session IDs and promotion target.

In `@src/api/promote.rs`:
- Around line 364-371: Update the promote route around landing_audit so its
heavy graph audit is covered by HeavyOpsLimiter, matching the existing
audit_drift pattern: obtain the limiter through the route’s Extension and
acquire a permit only in the branch that executes landing_audit. Preserve the
current dry-run and request.audit bypass behavior, and ensure the permit remains
held for the complete landing_audit call.
- Around line 372-382: promote のHTTP wire fixtureを追加し、POST
/contexts/{name}/promote の代表レスポンスとして batches、aliases_dropped、audit、audit_skipped
を含めてください。ok_with_issues_total に渡される PromoteOutcome の形と整合させ、HTTP_CONTRACT と
MCP_CONTRACT は変更しないでください。

In `@src/ingest/rejection.rs`:
- Around line 279-286: Update PreviewSeeds::absorb to store interned concepts
and labels grouped by context, and change predicted_alias_rejection to pass only
the seeds matching batch.context. Preserve rejection for aliases whose canonical
vocabulary is absent in the current context, and add a regression test covering
interleaved batches from different contexts.

In `@src/llm-protocol.md`:
- Line 365: Add an HTTP wire golden fixture for POST /contexts/{name}/promote
under the existing HTTP fixture set, covering batches, aliases_dropped, audit,
audit_skipped, and the omission rules for dry_run responses. Update
HTTP_CONTRACT with the fixture/version entry as needed, while leaving
MCP_CONTRACT version-only and without duplicating the response fixture because
MCP uses HTTP pass-through behavior.

In `@src/main.rs`:
- Line 904: Update promote_sources and the /contexts/{name}/promote route flow
to obtain the shared HeavyOpsLimiter permit only when audit=true and
dry_run=false, holding it for the full landing_audit execution. Do not acquire a
permit for audit=false or dry_run=true, and reuse the existing limiter rather
than creating a separate one.

In `@src/mcp/schema.rs`:
- Around line 581-583: Update the MCP schema’s sources property in the promote
tool definition to require at least one item by adding the appropriate minItems
constraint, matching promote_sources validation. Add a schema validation test
that confirms an empty sources array is rejected.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 36a9d86d-088b-48e6-994e-5982266be9e8

📥 Commits

Reviewing files that changed from the base of the PR and between 47dec07 and c0f0d40.

📒 Files selected for processing (22)
  • CHANGELOG.md
  • adr/0018-graph-path-promotion.md
  • docs/promotion.html
  • examples/skills/memory-promotion/SKILL.md
  • src/api.rs
  • src/api/consolidation.rs
  • src/api/import.rs
  • src/api/promote.rs
  • src/auth.rs
  • src/export.rs
  • src/ingest.rs
  • src/ingest/model.rs
  • src/ingest/rejection.rs
  • src/ingest/tests.rs
  • src/llm-protocol.md
  • src/main.rs
  • src/mcp.rs
  • src/mcp/route.rs
  • src/mcp/schema.rs
  • src/route.rs
  • tests/http_api/main.rs
  • tests/http_api/promote.rs

Comment thread docs/promotion.html Outdated
Comment thread examples/skills/memory-promotion/SKILL.md Outdated
Comment thread src/api/promote.rs
Comment thread src/api/promote.rs
Comment thread src/ingest/rejection.rs Outdated
Comment thread src/llm-protocol.md
Comment thread src/main.rs Outdated
Comment thread src/mcp/schema.rs
…, contract fixtures)

- PreviewSeeds now keys its interned vocabulary by context: a stream
  can interleave contexts, and a sibling's names must not vouch for an
  alias here — the real import refuses UnknownCanonical, so the
  preview does too (regression: interleaved-context stream refuses
  identically on dry run and real).
- landing_audit spends a heavy-ops permit (audit_drift's conditional
  pattern) — the write half never gates, and at the ceiling the audit
  degrades to audit_skipped: "overloaded" instead of shedding a
  request whose batches are already durable.
- The MCP schema declares minItems: 1 on sources, matching the
  handler's refusal; wire fixtures pin the promote and dry-run
  response shapes; docs and the skill template state the HTTP contract
  (?dry_run=true, {into, sources, audit?}, dry runs never audit) and
  the tool call's required context argument.

Claude-Session: https://claude.ai/code/session_01UPjzCLVuXQmxhGkr6phPb9
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