feat(routes): resolve relative inputSchema imports into one canonical RouteContract shared by MCP and CLI routes (#593) - #603
Merged
Conversation
…or API Cover parse/extract resolution, CLI AB4838/AB4839, shared RouteContract identity, and two-hop config aliases so the suite fails on current main for the right reasons and goes green after the L1 merge.
…IR, AB4838/AB4839 (#593 lane L5) - docs/diagnostics.md: rewrite the argv-grammar paragraph around static reference resolution, add the RouteContract paragraph, widen the AB4806 string-const form to alias hops across relative modules, qualify AB4814 by the declaring module, add AB4838/AB4839 rows, bump the section range. - docs/entry-conventions.md: schema may be declared in a relative module the route imports; fix the Diagnostics section anchor. - website (en): 'Share one schema between MCP and CLI' (package-entries), 'Route contracts (Application IR)' (authoring index), one paragraph in mcp.mdx, a Routes row in the Workbench page table. - changeset 593-route-contract-imports.md (agent-bundle: patch).
…te contracts (#593, lane L1) Add routes/module-scope.ts, the static module-scope model and reference resolver: top-level consts, named relative imports (moduleCandidates order, rejected outside projectRoot), alias hops any depth, cycle detection, printable chains, structural node slices (no ts.* in exported signatures). config-extract.ts and framework-imports.ts are rewired onto it in the same change (private scopeOf/ModuleScope/ImportedBinding/ resolveImportedConst/insideProject/parseModule/scriptKindOf copies deleted; AB4806 fragments unchanged, const string references now multi-hop). input-schema.ts follows an inputSchema reference to its declaring module and parses the zod chain in that scope: initializer, property initializer, chain root (resolved calls first), z.array(ref), z.enum(ref as-const array), z.object(ref), .default(ref). Grammar issues keep their AB4814 wording with positions in other modules qualified as <module>:<line>:<col>. parseInputSchema/extractInputSchema take InputSchemaExtractionOptions and report origin/resolution; extractInputSchema returns { origin, schema }. cli-argv.ts maps resolution failures to AB4838 (unresolvable) and AB4839 (cycle), adds ExtractedCliArgv.origin, and exports projectInputSchemaOptions so compileCliCommands projects the argv surface from the canonical RouteInputSchema when the graph bound a contract (re-parsing the module only to report why none exists). graph.ts binds routes declaring one schema to one RouteContract (contract:<module>#<binding>) sharing a single frozen input object, sets CompiledAgentRoute.contract and CompiledRouteGraph.contracts (from the final route set, sorted, omitted when empty); an imported contract id joins routeIdentity, a route-local literal does not, so inline-only graphs digest unchanged. types.ts/index.ts add RouteContract and RouteContractOrigin.
…rom agent-bundle/api and RouteManifestContract from dev; align fixtures and docs with the resolver
🦋 Changeset detectedLatest commit: 10d3d92 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
commit: |
ScriptedAlchemy
enabled auto-merge (squash)
September 5, 2026 06:45
Closed
7 tasks
ScriptedAlchemy
added a commit
that referenced
this pull request
Sep 5, 2026
…at/592-authoritative-manifest Resolve docs/diagnostics.md, api.ts, pack-inventory.ts, route-manifest.ts. Project routes.contracts[] and route.contract into the artifact manifest via the shared manifest-routes projection; add them to the shipped JSON Schema and the en/zh reference page (no longer a reserved key).
11 tasks
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 #593. Design comment: #593 (comment) (vocabulary from #592: the compiled route graph is the Application IR; the
RouteContractis its first graph-level node that routes reference instead of carry).What changes
Static resolution of
inputSchemareferences — no module is executed. A route may bind its schema to a declaration elsewhere:New leaf
routes/module-scope.tsis the shared module-scope model and reference resolver: top-levelconstbindings (exported or not), named value imports, known non-const names (let/var, destructuring, functions, classes, enums, namespaces, default and namespace imports); a reference is followed through same-module aliases and relative imports that resolve inside the project (moduleCandidatesorder —.js→.ts/.tsx, extensionless,index.*), across any number of alias hops, with every visited<module>#<binding>recorded so a cycle is detected deterministically.config-extract.ts(AB4806string consts, previously one hop) andframework-imports.tsare rewired onto it in the same change; their privatescopeOf/resolveImportedConst/scriptKindOf/parseModulecopies are deleted.input-schema.tsreads the schema expression in the scope of the module that declares it. Reference positions the grammar accepts: theinputSchemainitializer, a property initializer, the root of a method chain (requestStatusSchema.optional()— the resolved chain's calls come first),z.array(<ref>),z.enum(<ref>)(anas constarray of string literals),z.object(<ref>)/z.strictObject(<ref>)(an object literal),.default(<ref>)(a static literal). The zod grammar itself is unchanged; grammar violations inside a resolved schema stayAB4814, with the position qualified by the declaring module (src/lib/protocol-schemas.ts:2:11).One normalized contract per declared schema.
routes/types.ts:Identity is the declaration site:
cli:statusandtool:hauler/hauler_statusimportingstatusInputSchemabindcontract:src/lib/protocol-schemas.ts#statusInputSchema— one node, tworoutes; a route-local literal iscontract:<route>#inputSchema.compileCliCommandsprojects the argv grammar from the bound contract'sinput(projectInputSchemaOptions) instead of re-parsing the module, so the argv grammar, the static MCPinputSchema, the route manifest (RouteManifest.contracts,RouteManifestRoute.contract), the Workbench Routes page (origin + shared routes), andinspect --routesall read one object.RouteContract/RouteContractOriginare exported fromagent-bundle/api;RouteManifestContractfromagent-bundle/devand the browser contracts.Digest.
routeIdentitygainscontractonly when the contract is declared outside the route's own module; inline-only graphs digest exactly as before (pinned inroute-graph.test.ts).resultSchemamay be imported the same way today (the contract scan only requires the named export; TypeScript types it through the import; the runtime validates with the real zod object). No static result projection is added — no consumer needs one (#594 generates types from the sametypeofchain).Diagnostics (
docs/diagnostics.md; the reference page renders from it)AB4838inputSchemareferences a binding the static resolver cannot follow. Message:CLI route <path> inputSchema: inputSchema -> statusInputSchema (src/lib/protocol-schemas.ts) -> requestStatusSchema -> requestStatuses imported from "@shared/protocol", which is not a relative module path.Boundaries: bare specifier, outside the project, missing/unreadable module, missingexport const, non-constbinding (says what it is), unknown identifier, dynamic initializer (bare call, function, template with substitutions).AB4839Severity: errors only on CLI routes, where the static contract is load-bearing (the command cannot compile without an argv grammar — the same rule as
AB4814). MCP tool/resource/prompt, script, and event routes with an unresolvable schema stay silent as today: the runtime derives MCP JSON Schema from the real zod object, and making it an error would break every project that legitimately imports a schema from a package.Tests
input-schema.test.ts, +21): inline/1-hop/2-hop origins, same-module alias, chain root,z.enum(ref),z.array(ref),z.object(shapeConst),.default(const), cycle, an 11-row unresolved table (bare specifier, outside project, missing module, missing export,let, destructuring, default/namespace/type-only import, dynamic initializer, template), qualifiedAB4814inside an imported schema, nosourceoption.cli-routes.test.ts(+3):AB4838,AB4839, imported-vs-inline twin commands deep-equal.route-graph.test.ts(+3): shared CLI+tool contract, inline contract id, imported contract joins the digest; the pre-Application IR: resolve local schema imports so one route contract can drive MCP, CLI argv, Apps, and generated types #593 digest is pinned unchanged.route-config-extract.test.ts(+1): two-hop string const.route-manifest-routes.test.tsand Workbenchroute-manifest-client/routes-model/routes-pagetests cover the manifest/Workbench projection.route-contract-imports.test.ts, new, registered inrstest.integration-tests.ts): a fixture withsrc/lib/protocol-schemas.tsimportingrequestStatusesfromsrc/daemon/protocol.ts, a CLI route and an MCP tool bothexport const inputSchema = statusInputSchema; export const resultSchema = statusResultSchema;. Asserts one contract with two routes, argv options deep-equal to the inline twin,route.inputSchemaobject identity across the two routes,buildsucceeds, the bin'sstatus --helplists--lane-key --limit --statuses --ticketswith enum choices,status --statuses queued --statuses done --limit 3 --jsonreturns the canonical result,--statuses bogusexits 2, and generatedRouteInput<'cli:status'>≡RouteInput<'tool:hauler/hauler_status'>(typegen probe compiled withtsc, zero diagnostics, identical to the inline fixture). Abarevariant (@shared/protocol) produces exactly oneAB4838naming the chain.Docs
docs/diagnostics.md,docs/entry-conventions.md; website en + zh:guide/authoring/package-entries.mdx("Share one schema between MCP and CLI"),guide/authoring/index.mdx("Route contracts (Application IR)" design section),guide/authoring/mcp.mdx,guide/development/workbench.mdx. Changeset.changeset/593-route-contract-imports.md(agent-bundle: patch).Consumer proof: cargo-hauler dry-run
Harness under
/tmp/593-dryrun(never touches the cargo-hauler checkout):npm packagent-bundle,@agent-bundle/runtime,rsc-markdown-streamfrom the worktree; rsync cargo-hauler to a scratch copy; pointdevDependencies+pnpm-workspace.yamloverrides at the tarballs; apply one patch tosrc/cli/status.tsx(drop the inlinez.object,import { statusInputSchema } from '../lib/protocol-schemas.js'; export const inputSchema = statusInputSchema;); runinspect --routes --json,build, and the built bin.origin/main(d30d9acb6)fe96bde28)inspect --routesstate: invalid,AB4814 (error): CLI route src/cli/status.tsx has an inputSchema outside the argv grammar: the top level must be z.object({ ... }) or z.strictObject({ ... }).state: ready, no diagnosticscontractscontract:src/lib/protocol-schemas.ts#statusInputSchema→routes: ["cli:status", "tool:hauler/hauler_status"];#limitInputSchema→hauler_last+hauler_logagent-bundle builddist/bincargo-hauler status --helplists `--command-contains --cwd --lane-key --limit --session --statuses <requestedThe patched command's flags become the canonical keys (
--lane-key,--tickets,--statuses) instead of cargo-hauler's hand-written--lane/--ticket/--status— precisely the surface-grammar gap #596 closes with an explicit CLI projection; cargo-hauler would adopt the import together with #596 rather than rename its documented flags.Not in scope
Widening the zod grammar (unions, nested objects, transforms), static result-schema projection, executing modules, non-relative specifiers (package schemas keep working at runtime; they are not statically visible), CLI flag renames (#596).
Self-review
Implementation lanes: core resolver/graph on Claude (claude-fable-5-1-thinking-max), manifest/Workbench + integration fixture on GPT-5.6 Sol, unit tests + dry-run harness on Grok 4.6, docs en on Claude, docs zh on GPT-5.6 Sol. Reviewer:
generalPurposesubagent on GPT-5.6 Sol (different model from the core author;change-risk-reviewercannot run while TraceDecay's MCP endpoint is down on this machine), briefed to return concrete merge risks only across: resolver edge cases (re-exports,import { X as Y }, type-only imports,declare const, cycle keying, miss caching), regressions vsmain(the new dynamic-initializer rule,AB4806multi-hop, theframework-imports.tsrewire), digest stability, declaration hygiene (pnpm lint:release), dead code, manifest/Workbench decoder strictness, test coverage of the claimed cases, and docs/changeset accuracy.Findings: none — the reviewer ran
pnpm lint:release(pass), the 275 focused unit tests and the route-contract integration test (pass), and confirmed the changeset suffix. Integrator-side fixes made while integrating the lanes, before the review: (1) a reference that resolves to a dynamic initializer (bare call, function, template with substitutions) is reported asAB4838with the chain, per the design, rather than as anAB4814grammar issue —isStaticInitializerininput-schema.ts; (2) the unresolved chain names the failing binding as written in the importing module and puts the specifier in the reason (inputSchema -> statusInputSchema imported from "./missing", which …), matching the design example; (3) an inline{ ...spread }staysAB4814(it is a grammar issue, not a reference); (4) the integration fixture types its route parameter so the typegen probe compiles understrict; (5)RouteContract/RouteContractOriginexported fromagent-bundle/api,RouteManifestContractfromagent-bundle/devand the browser contracts; docs updated to match all of the above. Mergedorigin/main(#599 docs audit) with one table-row conflict indocs/diagnostics.md, resolved by keeping both rows; full gate (build, typecheck, lint, unit, route-unit, projection, docs site) green after the merge.