Resolve tool availability against the boundary the distribution declares - #171
Merged
Merged
Conversation
The distribution boundary says which files never reach an adopter runtime. Nothing in the interface compile chain read it, so the closed interface policy required every adopter to supply a tool the same repository had excluded from adopter runtimes, and the compiler failed closed when the adopter correctly did not have it. This repository could not see the failure, because it holds every tool it declares. Both declarations were internally correct, which is why neither was the thing to change. The boundary excludes a tool whose only job is copying a template tree that is also excluded; the interface policy is a closed enumeration of the call surface, and that tool genuinely has one here. What was missing was anything that read both. tool_availability is that reader, and the only one. Its answer is not "skip whatever is absent": absence is evidence of two opposite things -- a tool the boundary excludes, and a carried tool that failed to arrive -- and a rule that cannot tell them apart turns the second into a silent pass. It partitions a declared tool list into included, boundary-excluded, and unregistered-absent, and the third stays a refusal that names the tool, the boundary and the target. The projection target is declared, never inferred. A distribution mid-checkout would otherwise identify itself as an adopter runtime and excuse exactly the absence that needs reporting, so --projection-target is required to write. With --check it falls back to the target the stored artifact records, which is reading a declaration rather than guessing at one, and an artifact recording no target is refused rather than assigned one. The compiled contract now binds four things that let it answer whose projection it is: the target, the boundary path and hash, and the included and excluded tool sets. Each has its own remedy and each says so rather than reporting "stale": another target is another projection, a changed boundary hash makes every derived artifact stale, and a recorded tool set that disagrees with this repository was compiled somewhere else and cannot stand in for a local build. That last case was detectable but never detected, because no adopter runs the interface checks -- run_gates preflighted compose_vocab and compose_page_contract and stopped there. Both interface artifacts now join that freshness set, which is what turns "regenerate in the target repository" from documentation into a gate an adopter trips. render_interface_projection needed no change; it already consumed only the compiled contract. Fourteen cases, all derived from the declaration rather than naming a tool: a test that spelled the name would keep passing after the declaration changed and would be asserting its own memory. The generic carried-runtime fixture is built by applying the boundary to a copy of Tools/, compiles, renders, and answers a real MCP initialize and tools/list with every served tool backed by an implementation. Deleting a carried tool from it still fails the compile. Full suite 1,890 tests, no new failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #170.
What was wrong
distribution-boundary.yamlsays which files never reach an adopter runtime. Nothing in the interface compile chain read it. So the closed interface policy required every adopter to supply a tool the same repository had excluded, andcompile_cli_contractfailed closed when the adopter correctly did not have it. This repository cannot observe that, because it holds every tool it declares.Both declarations were internally correct, which is why neither was the thing to change. The boundary excludes a tool whose only job is copying a template tree that is also excluded. The interface policy is a closed enumeration of the call surface, and that tool genuinely has one here. What was missing was anything that read both —
grepfound exactly one module inTools/that reads the boundary at all, and it was not in this chain.The resolver, and what it refuses to do
Tools/tool_availability.pyis the single reader. Its answer is deliberately not "skip whatever is absent". Absence is evidence of two opposite things — a tool the boundary excludes, and a carried tool that failed to arrive — and a rule that cannot tell them apart converts the second into a silent pass.partition()returns three sets, and the third (unregistered-absent) stays a refusal that names the tool, the boundary and the target.The projection target is declared, never inferred. A distribution mid-checkout would otherwise identify itself as an adopter runtime and excuse exactly the absence that needs reporting.
--projection-targetis required to write. With--checkit falls back to the target the stored artifact records — reading a declaration, not guessing at one — and an artifact that records no target is refused rather than assigned one.The artifact now says whose projection it is
Four bindings, each with its own remedy, each reported as itself rather than as "stale":
projection_targetdistribution_boundary.sha256included_toolsexcluded_toolsOne thing the issue did not ask for, and why it is here
The foreign-artifact case was detectable but never detected: no adopter runs the interface checks.
run_gatespreflightedcompose_vocabandcompose_page_contractand stopped there. Both interface artifacts now join that freshness set — which is what turns "regenerate in the target repository" from documentation into a gate an adopter actually trips.render_interface_projectionneeded no change. It already consumed only the compiled contract, so requirement 6 was already met.Acceptance
Fourteen cases, all derived from the declaration rather than naming a tool. A test that spelled the name would keep passing after the declaration changed and would be asserting its own memory. One case fails loudly if the boundary ever declares no tool module at all, so the rest cannot go quietly vacuous.
source-distributionincludes every boundary-excluded tool, and excludes nothing.carried-runtimefixture — built by applying the boundary to a copy ofTools/— compiles, renders, and answers a real MCPinitializeandtools/list, with every served tool backed by an implementation.included_toolsreports as compiled elsewhere.Verified before and after on the fixture from the issue:
initializewent from-32002toOK, with 39 tools served and no missing implementations.Full suite 1,890 tests, no new failures. No adopter-specific case appears in the code or the tests.