Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions docs/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,47 @@ payload = proxy.route_result(tool, {"items": []})
- Adapters are the **runtime** counterpart: they keep old call shapes working.
- Prefer documenting adapters next to intentional renames in release notes.

## Suggesting adapter drafts (#62)

Generate a **reviewable** draft from two snapshots. Suggestions are never
auto-applied by CI — humans must review and opt in before shipping.

```bash
tool-semantics capture examples/github_server_v1.json -o .tool-semantics/v1.json
tool-semantics capture examples/github_server_v2.json -o .tool-semantics/v2.json
tool-semantics suggest-adapter .tool-semantics/v1.json .tool-semantics/v2.json \
-o .tool-semantics/adapter.suggested.json
```

```python
from tool_semantics.scanner import capture_manifest
from tool_semantics.suggest import suggest_adapter
from pathlib import Path

baseline = capture_manifest(Path("examples/github_server_v1.json"))
candidate = capture_manifest(Path("examples/github_server_v2.json"))
draft = suggest_adapter(baseline, candidate)
assert draft.auto_apply is False
print(draft.adapter.aliases, draft.notes)
```

### What gets suggested

| Signal | Action | Confidence |
| --- | --- | --- |
| `tool.renamed` in the report | `ToolAlias` | high |
| Remaining remove+add above suggestion threshold | `ToolAlias` | medium/low |
| Removed+added parameters with name similarity | `ArgumentMap.rename` | high/medium/low |
| Enums with equal cardinality + unique token matches | `EnumMap` | medium |
| Required parameter adds / tied enums / cardinality mismatch | skipped | skipped |

### Confidence limits

- Heuristic renames can be wrong when two unrelated tools look similar.
- Enum remaps are skipped when values cannot be paired unambiguously.
- New required parameters never get invented defaults.
- Treat every draft as a starting point for review, not production config.

## MCP compatibility proxy

`CompatibilityProxy` is an in-process shim. A network MCP proxy that speaks
Expand Down
71 changes: 71 additions & 0 deletions src/tool_semantics/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from tool_semantics.runner import ModelRunner, OpenAICompatibleRunner, RunnerConfig
from tool_semantics.scanner import ManifestError, capture_manifest, read_snapshot, write_snapshot
from tool_semantics.scorecard import build_scorecard
from tool_semantics.suggest import adapter_suggestion_to_json, suggest_adapter

app = typer.Typer(
no_args_is_help=True,
Expand Down Expand Up @@ -1004,3 +1005,73 @@ def compare(
)
if fails_policy:
raise typer.Exit(code=1)


@app.command("suggest-adapter")
def suggest_adapter_cmd(
baseline: Annotated[
Path,
typer.Argument(dir_okay=False, help="Baseline snapshot JSON from `capture`."),
],
candidate: Annotated[
Path,
typer.Argument(dir_okay=False, help="Candidate snapshot JSON from `capture`."),
],
output: Annotated[
Path | None,
typer.Option(
"--output",
"-o",
help="Write reviewable adapter suggestion JSON (never auto-applied).",
),
] = None,
verbose: Annotated[
bool,
typer.Option("--verbose", "-v", help="Log suggestion steps to stderr."),
] = False,
) -> None:
"""Suggest a MigrationAdapter draft from rename / schema diffs (#62).

Output is reviewable JSON only — CI must not apply it without an explicit
human opt-in step outside this command.
"""
_require_snapshot_file(baseline, "Baseline")
_require_snapshot_file(candidate, "Candidate")
try:
baseline_snap = read_snapshot(baseline)
candidate_snap = read_snapshot(candidate)
report = compare_snapshots(baseline_snap, candidate_snap)
suggestion = suggest_adapter(baseline_snap, candidate_snap, report=report)
except (ManifestError, FileNotFoundError, ValueError) as exc:
console.print(f"[red]Adapter suggestion failed:[/red] {exc}")
raise typer.Exit(code=2) from exc

_log_verbose(
verbose,
f"Aliases={len(suggestion.adapter.aliases)} "
f"arg_maps={len(suggestion.adapter.arguments)} "
f"enums={len(suggestion.adapter.enums)} notes={len(suggestion.notes)}",
)
console.print(
f"Suggested adapter draft: "
f"{len(suggestion.adapter.aliases)} alias(es), "
f"{len(suggestion.adapter.arguments)} argument map(s), "
f"{len(suggestion.adapter.enums)} enum map(s). "
f"[yellow]auto_apply={suggestion.auto_apply}[/yellow] — review before use."
)
if suggestion.notes:
table = Table(title="Suggestion notes")
table.add_column("Confidence")
table.add_column("Subject")
table.add_column("Message")
for note in suggestion.notes:
table.add_row(note.confidence.value, note.subject, note.message)
console.print(table)

payload = adapter_suggestion_to_json(suggestion)
if output is not None:
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
console.print(f"Wrote reviewable draft to {output}")
else:
console.print_json(data=payload)
Loading
Loading