Skip to content

propose: design resolve tool#134

Merged
HumanBean17 merged 1 commit into
masterfrom
propose/resolve-tool
May 15, 2026
Merged

propose: design resolve tool#134
HumanBean17 merged 1 commit into
masterfrom
propose/resolve-tool

Conversation

@HumanBean17
Copy link
Copy Markdown
Owner

Propose: design the resolve tool — strict-frame identifier resolution.

The filter frame (PRs #131#133, propose) named resolve in §3.5 as the escape valve for identifier-shaped lookups but explicitly deferred its design. This propose locks that design.

The decision in one line

resolve(identifier: str, hint_kind: NodeKind | None) → ResolveOutput returns one of three loud states — status="one" with a NodeRef, status="many" with a ranked candidate list and per-candidate reasons, or status="none" with a diagnostic message.

Shape highlights

  • Discriminated envelope — agent code branches once on status. No silent best-guess; no confidence: float shoved into a single-node output.
  • hint_kind only. No microservice co-hint, no hints: dict. Cross-microservice FQN collisions surface honestly as status="many" with the microservice visible on each candidate's NodeRef.
  • All three kinds (Symbol, Route, Client) in one tool. Per-kind tool splits (resolve_symbol / resolve_route / resolve_client) explicitly rejected — same contract, smaller mental model.
  • Closed ResolveReason Literal (8 values: exact_id, exact_fqn, fqn_suffix, short_name, route_template, route_method_path, client_target, client_target_path). Adding a reason is a frame decision, like adding an EdgeType.
  • Three-tier ranking within status="many": reason priority → specificity → stable node.id. score is documented as telemetry-only; agents must not branch on it.
  • Loud-fail on malformed input (empty / whitespace) returns success=False, distinct from status="none" (well-formed input, no match). Matches the strict-frame loud-fail discipline from lossless-permissive frame: tactical no-regret fix for #117 silent-drop bug class #122.

Migration

2 PRs, in order:

  • PR-RESOLVE-1 — ship resolve end-to-end (models, handler, candidate generators, tests covering each ResolveReason plus malformed-input cases).
  • PR-RESOLVE-2 — remove the §3.4.7 pre-resolve fallback wording from all four existing tool descriptions (not amended, removed). Verify by re-reading docstrings: "search + describe-per-candidate" no longer appears as a recommended pattern.

No deprecation aliases (no active users, breaking changes allowed).

What lives in §6, §7, §8 of the propose

  • §6 — per-PR purpose, scope, and named test scenarios (the contract; total count is a side-effect of implementation).
  • §7 — 15 locked decisions, including the "no co-hint", "no per-kind splits", "loud-fail vs status='none' distinction", and the "PR-RESOLVE-2 waits for PR-RESOLVE-1" sequence.
  • §8 — risks table including the "agent reaches for resolve when right tool is search" failure mode and how the description / UC14 / UC15 wording is supposed to prevent it.

Use-case re-walk — 17 rows

Covers Symbol FQN / suffix / short-name, route method+path / template / microservice-qualified, client target / target+path, malformed input, query-shaped input, wildcards (rejected, aligned with filter-frame §3.4.1), and cross-kind disambiguation without hint_kind. The previously-degraded UC7 of the filter-frame propose ("smartcare" → canonical) is now first-class.

How to read

Suggested order: TL;DR → §1 Frame → §3 Proposed surface → §4 Use-case re-walk → §7 Decisions taken. §2 / §5 / §8 are supporting context.

What's out of scope (frame-binding)

Item Why
microservice co-hint Re-introduces smart-bag behavior the filter frame removed.
Open-ended hints: dict Same, worse.
Wildcards / regex in identifier Aligned with filter-frame §3.4.1.
Silent best-guess single node when ambiguous Hides ambiguity from the agent.
Per-kind tools (resolve_symbol, etc.) Fragmented contract; the candidate generator is per-kind internally, not in the public surface.
Bundling describe-payload into ResolveOutput Couples two concerns.
Free-text reason: str Closed Literal is the analog of closed EdgeType.

Doc: propose/RESOLVE-TOOL-PROPOSE.md

@HumanBean17
Copy link
Copy Markdown
Owner Author

Review — propose: design resolve tool

Verdict: Approve with minor edits before implementation.

This is a strong, frame-aligned design doc. CI is green. Scope is correctly limited to propose/RESOLVE-TOOL-PROPOSE.md — appropriate for a propose-only PR.


What works well

Frame continuity. The propose does what the filter-frame doc promised: it designs the deferred §3.5 primitive without reopening the four-tool contract. The search vs resolve boundary (opaque text vs identifier-shaped strings) is clear and matches how server.py already describes the pre-resolve fallback.

Three loud states. The discriminated status: one | many | none envelope is the right abstraction. Rejecting silent best-guess and confidence: float avoids pushing ambiguity back onto the agent.

Hint discipline. hint_kind only — no microservice co-hint, no hints: dict — is consistent with filter-frame principle 1. UC3/UC6 correctly fix filter-frame UC7's outdated hint_kind="microservice" sketch: "smartcare" resolves via client_target, not microservice aliasing.

Closed ResolveReason. Eight values, tied to concrete identifier shapes, mirrors the closed EdgeType pattern and gives implementers a testable contract.

Migration sequencing. PR-RESOLVE-1 (tool) → PR-RESOLVE-2 (description sweep) is correct. Removing §3.4.7 fallback wording before resolve exists would leave agents without a documented path — decision §7.15 is sound.

Implementation hooks match the codebase. References to mcp_v2.py, NodeRef, _resolve_node_kind (rename the internal helper, not the tool), and existing describe(fqn=…) collision behavior are accurate.


Issues to fix in the propose (before or as part of merge)

1. TL;DR example reason doesn't exist in the closed Literal

TL;DR lists "microservice_alias" among example reasons; §3.4 and Appendix A define eight values and no microservice_alias. Drop or replace that example so the doc is self-consistent.

2. success=False path leaves status undefined

§3.3 / UC12–UC13 define loud-fail for empty/whitespace (success=False, Invalid identifier: …) but never say what status must be. Agents branching on status will hit undefined behavior.

Suggestion: Lock one rule, e.g. malformed input → success=False, status="none", non-empty message (and optionally forbid populating node / candidates). Or document that status is omitted/ignored when success=False — but then tool handlers and tests need to say that explicitly.

3. PR-RESOLVE-2 sweep list is incomplete

§6 / §3.7 say "all four existing tool descriptions" — only search and describe in server.py currently mention the fallback. Also update:

Surface Current pre-resolve text
docs/AGENT-GUIDE.md "Identifier resolution (pre-resolve)" section
mcp_v2.py describe_v2 collision hint_message Points at find + search, not resolve
server.py _INSTRUCTIONS Lists four tools only — needs resolve after PR-RESOLVE-1
README.md MCP tool list Likely needs a fifth tool row when implemented

Worth adding a short "description sweep checklist" in §6 so PR-RESOLVE-2 doesn't miss non-server.py prose.

4. Test contract vs realistic outcomes

§6 PR-RESOLVE-1 says:

Each ResolveReason is exercised by at least one status="one" test.

Several reasons (short_name, fqn_suffix, often client_target) are naturally status="many". Tighten to:

  • Every ResolveReason appears in at least one test (any status), and
  • status="one" / many / none / loud-fail each have dedicated scenarios.

Otherwise implementers will write awkward tests or weaken generators to force one.

5. Filter-frame §3.4.2 vs this propose (intentional — document the delta)

The completed filter-frame propose allowed a microservice co-parameter on describe for FQN collisions. Shipped code does first match + hint instead (describe_v2). This propose correctly standardizes collisions on resolve(..., hint_kind="symbol")status="many". Add one sentence in §1 or References: post-resolve, prefer resolve over describe(fqn=…) when FQN may collide; describe may keep first-match behavior or be tightened in a follow-up.

That avoids implementers assuming describe will gain a microservice parameter.

6. ONTOLOGY_VERSION wording in PR-RESOLVE-2

PR-RESOLVE-2 says bump ONTOLOGY_VERSION only if other schema work ships. Good — a new MCP tool should not bump ontology version. Consider stating explicitly: adding resolve does not bump ontology_version.

Per repo rules, put ResolveReason in java_ontology.py (single source of truth) — worth one line in PR-RESOLVE-1 scope.

7. Optional: add plans/PLAN-RESOLVE-TOOL.md when locking

Repo culture expects a plan + CURSOR-PROMPTS-* for multi-PR work. The propose's §6 is already detailed; a thin plan mirroring PR-RESOLVE-1/2 with sentinel greps and exact test_* names would reduce handoff friction. Not blocking for merging this propose.


Smaller nits (non-blocking)

  • Namespace: References acknowledge _resolve_node_kind vs public resolve — rename internal helper in PR-RESOLVE-1 if confusion is likely.
  • Candidate dedup: §3.6 doesn't say whether generate_candidates deduplicates by node.id before counting len(matches). Worth one line so duplicate generator paths don't yield bogus many.
  • Truncation at K: If len(matches) > K, still status="many" with top-K — fine; optional note that message could mention truncation (telemetry-only, not required v1).

Alignment check with current code

Propose claim Master reality
Pre-resolve fallback in tool descriptions Present on search + describe only
describe(fqn=…) for symbols Shipped
FQN collision → disambiguation describe returns first + hint; resolve will own many
Wildcards rejected on structured surfaces Shipped
find strict target_service (no fuzzy) Shipped — "smartcare"resolve, not find

No contradictions with ontology 12 or the four-tool surface.


Recommended next steps

  1. Merge this PR after fixing items 1–2 (and ideally 3–4) in the markdown.
  2. Open plans/PLAN-RESOLVE-TOOL.md + plans/CURSOR-PROMPTS-RESOLVE-TOOL.md when starting implementation.
  3. PR-RESOLVE-1: models in mcp_v2.py, handler + per-kind generators, server.py registration, tests per UC table, ResolveReason in java_ontology.py.
  4. PR-RESOLVE-2: sweep checklist above + README MCP section.

Summary: The design is ready to lock: strict, composable, and it closes the filter-frame's biggest gap (identifier canonicalization without smart filters). Address the status on loud-fail gap, fix the TL;DR reason typo, widen the PR-RESOLVE-2 doc sweep, and relax the per-reason status="one" test rule — then merge and implement in the stated two-PR order.

@HumanBean17 HumanBean17 marked this pull request as ready for review May 15, 2026 10:24
Adds propose/RESOLVE-TOOL-PROPOSE.md.

`resolve(identifier, hint_kind?)` is the strict-frame primitive for
identifier-shaped lookups, named in §3.5 of the filter-frame propose
and now designed in detail:

- Discriminated output: status=one|many|none, with NodeRef / candidate
  list / message accordingly. Status invariants checked at handler exit.
- hint_kind only — no microservice co-hint, no hints: dict.
- Closed ResolveReason Literal (8 values): identifier shape \u2192 reason
  category, ranking by reason priority + specificity + stable id.
- Loud-fail (success=False) on empty / whitespace identifier, distinct
  from status='none' (well-formed input, no match).
- All three kinds in one tool; per-kind splits explicitly out.
- Removes the §3.4.7 pre-resolve fallback wording from all four
  existing tool descriptions (not amended, removed).

17-UC re-walk covers Symbol FQN / suffix / short-name, route
method+path / template / microservice-qualified, client target /
target+path, malformed input, query-shaped input, wildcards, and
cross-kind disambiguation without hint_kind.

15 locked decisions, 2-PR migration (PR-RESOLVE-1 ships the tool,
PR-RESOLVE-2 sweeps tool descriptions). No deprecation aliases —
breaking changes allowed, no active users.
@HumanBean17 HumanBean17 force-pushed the propose/resolve-tool branch from 6138d92 to 0715eac Compare May 15, 2026 10:27
@HumanBean17
Copy link
Copy Markdown
Owner Author

Thanks for the substantive review. Applied items 1\u20136 and the candidate-dedup nit; pushed back on two non-blockers. Force-pushed the revision (one commit, no noise).

Applied

(1) "microservice_alias" typo in TL;DR. Replaced with "client_target" and added \u2014 see \u00a73.4 for the closed set so the bullet doesn't look exhaustive.

(2) status undefined on success=False. Locked: success=False \u27f9 status="none", node=None, candidates=[], message non-empty and starts "Invalid identifier:". Reusing "none" rather than inventing "malformed" keeps status non-nullable; the agent's first branch is success, the second is status only when success == True. Added as decision \u00a77.16; \u00a73.2 now has split invariants for the two success cases.

(3) PR-RESOLVE-2 sweep is incomplete. \u00a76 now carries a sweep checklist with 8 surface rows: server.py tool descriptions for search / describe / find / neighbors, server.py _INSTRUCTIONS, mcp_v2.py describe_v2 collision hint_message, docs/AGENT-GUIDE.md's "Identifier resolution (pre-resolve)" section, and the README.md MCP tool table. The "four tool descriptions" wording in the previous draft was the gap you caught.

(4) Per-reason status="one" test is too strict. Reworded to: every ResolveReason appears in at least one test (any status); status="one" / "many" / "none" / loud-fail each have dedicated scenarios. status="many" scenarios are pinned to UC3 (FQN collision across microservices) and UC5 (short-name ambiguity).

(5) Document delta vs filter-frame \u00a73.4.2. Added a paragraph in \u00a71 plus locked decision \u00a77.20: describe(fqn=\u2026) is not extended with a microservice parameter; cross-microservice FQN collisions go through resolve(\u2026, hint_kind="symbol") \u2192 status="many". The shipped first-match-plus-hint behavior on describe_v2 may keep that shape, or be tightened in a follow-up; either way no co-parameter is added.

(6) ONTOLOGY_VERSION and java_ontology.py placement. Adding resolve does not bump ontology_version (decision \u00a77.18; ontology versioning tracks the graph schema, not the tool surface). ResolveReason lives in java_ontology.py next to the other closed sets (decision \u00a77.17); Pydantic models that reference it stay in mcp_v2.py. PR-RESOLVE-1 scope now names both placements.

Nit \u2014 candidate dedup. \u00a73.6 now has the dedup step in the decision rule, with a comment that generator paths can overlap (e.g., short_name and fqn_suffix both matching the same Symbol). Decision \u00a77.19 locks it; tests have a deduplication scenario.

Drift caught during the revision pass. \u00a75's microservice co-hint row still suggested "the agent disambiguates with find or by picking from the list" \u2014 the find path uses the structured microservice filter, which is fine, but framing it as a primary disambiguation route understated the decision. Tightened: the primary path is picking from the candidate list; find(kind="symbol", filter={"microservice": ...}) is the set-shaped alternative when that's what the workflow actually wants.

Not adopted (with reasoning)

(7) "Add plans/PLAN-RESOLVE-TOOL.md when locking." Agreed it's the next move after the propose locks, but committing to the plan inside the propose violates the artifact boundary the propose-doc-author skill enforces (three artifacts, three scopes \u2014 propose decides what and why, plan decides how exactly). Honored in the workflow when this PR merges; not added to the propose.

Nit \u2014 truncation message at K. Holding the line. Locking "message mentions truncation" hardens into a contract the agent depends on; I'd rather keep truncation as silent telemetry in v1 and earn the message back if a real workflow needs it. The v1 contract is: at K, status="many" with the top-K candidates by ranking. If K was hit, the agent should narrow the identifier or fall through to search \u2014 not paginate.

Nit \u2014 rename _resolve_node_kind. The namespace overlap is already acknowledged in References; the helper is private and the rename is implementer judgment in PR-RESOLVE-1. Not locking it in the propose.

Resulting changes

  • TL;DR migration bullet rewritten (no more "bump ONTOLOGY_VERSION only if other shipping work moves the schema").
  • \u00a71 Frame: new paragraph on the delta vs filter-frame \u00a73.4.2.
  • \u00a73.2: split invariants block (success / failure cases).
  • \u00a73.6: dedup step + comment.
  • \u00a73.7: pointer to \u00a76 sweep checklist (the four-tools framing was too narrow).
  • \u00a76: PR-RESOLVE-1 scope mentions java_ontology.py; named test scenarios reworded; explicit no-ontology_version-bump line. PR-RESOLVE-2 now has the 8-row sweep checklist and a duplicate-section-heading cleanup.
  • \u00a77: decisions \u00a77.16\u2013\u00a77.20 added (malformed status, ResolveReason placement, no version bump, dedup, no describe co-parameter).

Total decisions in \u00a77: 20. PR count, kind count, UC count (17), ResolveReason count (8), and sweep-checklist row count all consistent on a re-run of the consistency pass.

Doc: propose/RESOLVE-TOOL-PROPOSE.md

@HumanBean17 HumanBean17 merged commit 5dad890 into master May 15, 2026
1 check passed
@HumanBean17 HumanBean17 deleted the propose/resolve-tool branch May 23, 2026 16:22
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