diff --git a/.changeset/remove-remaining-route-shapes.md b/.changeset/remove-remaining-route-shapes.md new file mode 100644 index 000000000..561e0ce28 --- /dev/null +++ b/.changeset/remove-remaining-route-shapes.md @@ -0,0 +1,5 @@ +--- +"agent-bundle": minor +--- + +Make `CompiledAgentRoute.resultSchemaState` required on every compiled route; require `{ kind: 'resource' }` for `agent-bundle/test` App and resource contract fixtures (bare `{}` rejected); stop reporting AB4840 for removed `before`/`preflight` event exports. (#840) diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 36cbcc068..e6209886a 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -1153,8 +1153,8 @@ compile has no correct partial output, so every finding is an error An event route uses a `.ts` lightweight handler or a `.tsx` rendered handler. A `.ts` handler can return `ctx.render('./name.view.js', data)` to load its separate `.view.tsx` sibling. Data must be strict JSON. The compiler uses explicit module boundaries without -extracting closures. Removed `before`/`preflight` exports, invalid view modules, and event -helper/path mismatches report `AB4840`. Definition helpers require a direct default call +extracting closures. Invalid view modules and event helper/path mismatches report +`AB4840`. Definition helpers require a direct default call with an inline object literal; computed options and wrappers report `AB4810`. Providers load on first `context.provider('')` access. Concurrent readers share the @@ -1203,7 +1203,7 @@ projections; no static provider subset declaration is required. | `AB4837` | error | A compiled executable imports a compiler-carrying framework entry (`agent-bundle`, `/api`, `/config`, `/eval`, `/rstest`, `/test`, or `/test/browser`). The build checks requests after transformation and resolved package export identities, including aliases, and names the importing module. Type-only imports erased by the configured transform are legal. Source-only `inspect` and `validate` do not prove this dependency boundary. Keep compiler calls in a host process, or use `import type` for framework types. | | `AB4838` | none | Retired. Schema imports are resolved by the bundler, not an inspection-time interpreter. | | `AB4839` | none | Retired. Inspection does not follow schema alias chains. | -| `AB4840` | error | An event exports removed `before` or `preflight` bindings, its helper disagrees with the conventional path, or its `.view.tsx` sibling is not a valid rendered event module. Use a `.ts` handler and `ctx.render('./name.view.js', data)` for an explicit rendered view. | +| `AB4840` | error | An event helper disagrees with the conventional path, its `.view.tsx` sibling exports `config`, or its `.view.tsx` sibling is not a valid rendered event module. Use a `.ts` handler and `ctx.render('./name.view.js', data)` for an explicit rendered view. | | `AB4843` | error | A `.cli.{ts,tsx}` module under `src/mcp//tools/` has no sibling tool route `.{ts,tsx}` (orphan), a `.cli.{ts,tsx}` module sits under `resources/`, `prompts/`, or `apps/`, or a second projection module (`.cli.ts` beside `.cli.tsx`) names the same tool, the first in path order wins and the second is reported. The suffix is reserved under `src/mcp/**` only. The message is `CLI projection for tool:/: .` (`has no sibling tool route …`, ` already projects this tool …`); a misplaced module names no tool, so its message is `CLI projection : sits under resources/, prompts/, or apps/ …`. `sourcePath` is the projection module's absolute path. Recovery: rename the file to match the sibling tool, or prefix `_` to park it, then inspect again. It is an error because a projection that cannot compile has no correct partial output. | | `AB4844` | error | A CLI projection config does not satisfy its closed metadata contract, combines JSON input mode with flag mapping, or relaxes a required key without declaring `mapInput`. The generated runtime checks that a loaded `mapInput` is callable, awaits it, and validates the result through the original input schema. | | `AB4845` | error | A CLI projection's grammar does not bind to the tool's contract: `flags`/`positionals` name a key absent from the tool's `RouteContract.input`; a `name`/alias is not kebab-case, is reserved (`help`, `json`, `ndjson`, `version`, and `yes` when confirm), or collides with another option's spelling or alias; `flags..name` or `flags..aliases` is declared on a key `positionals` consumes as a bare argument (`description`, `default`, and `required: false` still apply there); the tool's contract has a key `yes` while the command confirms, the shell keys parsed values by canonical key and strips `yes` as the confirmation, so no `name` override reaches the tool (`set confirm: false or rename the key`); or a `command` segment is not a safe identity segment. The message is `CLI projection for tool:/: .` and `sourcePath` is the projection module's absolute path. Recovery names the offending key or spelling and the accepted form, then says to inspect again. It is an error because a projection that cannot compile has no correct partial output. | diff --git a/packages/agent-bundle/README.md b/packages/agent-bundle/README.md index 74f955d50..ae0373104 100644 --- a/packages/agent-bundle/README.md +++ b/packages/agent-bundle/README.md @@ -912,7 +912,7 @@ registers app resources, `packed-stdio`, `packed-deleted-source`, compiled `ui://` URI in `listResources` and `sweep` reads that resource. With the default `apps: 'auto'` an app route needs no fixture entry: `coverage` passes with a reason naming the auto-covered sweep. An explicit -`{ kind: 'resource' }` (or legacy `{}`) entry is always accepted, and +`{ kind: 'resource' }` entry is always accepted, and `apps: 'explicit'` makes a missing app entry a `coverage` failure again. At `mcp-in-memory` apps are never registered, so `apps` has no effect there. diff --git a/packages/agent-bundle/src/build/entries.ts b/packages/agent-bundle/src/build/entries.ts index 8c26ba581..758ce780f 100644 --- a/packages/agent-bundle/src/build/entries.ts +++ b/packages/agent-bundle/src/build/entries.ts @@ -228,6 +228,7 @@ export const planScriptsSurface = async ( id: rendered.routeId, kind: 'script', provenance: { kind: 'conventional', relativePath: `scripts/${name}` }, + resultSchemaState: 'unknown', source, }], ...(options.noticeRetention === undefined ? {} : { noticeRetention: options.noticeRetention }), diff --git a/packages/agent-bundle/src/dev/routes/application-tree.ts b/packages/agent-bundle/src/dev/routes/application-tree.ts index e7aa81bd6..20df726df 100644 --- a/packages/agent-bundle/src/dev/routes/application-tree.ts +++ b/packages/agent-bundle/src/dev/routes/application-tree.ts @@ -32,7 +32,7 @@ export interface ApplicationLeaf { readonly label: string; readonly handler?: string; readonly ref: ApplicationNodeRef; - readonly resultSchemaState?: RouteManifestResultSchemaState; + readonly resultSchemaState: RouteManifestResultSchemaState; readonly routeId?: string; readonly source?: string; } @@ -179,7 +179,7 @@ const leafForRoute = ( label: routeLabel(ref), ...(route.execution?.handler === undefined ? {} : { handler: route.execution.handler }), ref, - ...(route.resultSchemaState === undefined ? {} : { resultSchemaState: route.resultSchemaState }), + resultSchemaState: route.resultSchemaState, routeId: route.id, source: route.source, }); @@ -269,6 +269,7 @@ const configuredHookLeaves = ( key, label: hook.event, ref, + resultSchemaState: 'absent', source: hook.path, })); } @@ -291,6 +292,7 @@ const configuredScriptLeaves = ( key, label: script.name, ref, + resultSchemaState: 'absent', ...(script.file === undefined ? {} : { source: script.file.path }), })); } @@ -306,6 +308,7 @@ const skillLeaves = (skills: readonly ApplicationTreeSkill[]): readonly Applicat key: applicationNodeKey(ref), label: skill.label, ref, + resultSchemaState: 'absent' as const, ...(skill.source === undefined ? {} : { source: skill.source }), }); })); @@ -321,6 +324,7 @@ const staticDocumentLeaves = ( key: applicationNodeKey(ref), label: document.name, ref, + resultSchemaState: 'absent' as const, source: document.provenance.sourcePath, }); })); diff --git a/packages/agent-bundle/src/dev/routes/route-manifest.ts b/packages/agent-bundle/src/dev/routes/route-manifest.ts index 3fe7527fd..a98cf177d 100644 --- a/packages/agent-bundle/src/dev/routes/route-manifest.ts +++ b/packages/agent-bundle/src/dev/routes/route-manifest.ts @@ -65,8 +65,8 @@ export type RouteManifestContract = ArtifactManifestRouteContract; */ export interface RouteManifestRoute extends ArtifactManifestRoute { readonly config: readonly RouteManifestConfigEntry[]; - /** Static declaration/projection evidence; absent only on manifests from older dev servers. */ - readonly resultSchemaState?: RouteManifestResultSchemaState; + /** Static declaration/projection evidence from the compiler pass. */ + readonly resultSchemaState: RouteManifestResultSchemaState; } /** Result schemas execute as authored, so a declaration is known without inventing a static schema projection. */ @@ -164,7 +164,7 @@ const configSummary = (config: Readonly>): readonly Rout const manifestRoute = (route: CompiledAgentRoute): RouteManifestRoute => ({ ...artifactRouteFor(route), config: configSummary(route.config), - ...(route.resultSchemaState === undefined ? {} : { resultSchemaState: route.resultSchemaState }), + resultSchemaState: route.resultSchemaState, }); const manifestServer = (server: CompiledServerSurface): RouteManifestServer => ({ diff --git a/packages/agent-bundle/src/routes/event-handler.ts b/packages/agent-bundle/src/routes/event-handler.ts index b8523cb31..88e6d7d92 100644 --- a/packages/agent-bundle/src/routes/event-handler.ts +++ b/packages/agent-bundle/src/routes/event-handler.ts @@ -9,21 +9,18 @@ export const eventHandlerEntry = ( relativePath: string, sourcePath: string, ): CompiledEventHandler | undefined => { - const { named, definition } = scanRouteModuleExports(text, relativePath); + const { definition } = scanRouteModuleExports(text, relativePath); if (definition?.event !== undefined) { const expected = relativePath.match(/(?:^|\/)src\/events\/(.+)\.tsx?$/u)?.[1]; if (expected !== definition.event) throw new TypeError(`Event definition ${definition.event} disagrees with conventional path ${relativePath}.`); } - if (named.has('preflight') || named.has('before')) { - throw new TypeError(`Event ${relativePath} must use a .ts handler and ctx.render('./${relativePath.split('/').at(-1)!.replace(/\.tsx?$/u, '.view.js')}', data); before/preflight exports are no longer supported.`); - } if (!relativePath.endsWith('.ts')) return undefined; const view = sourcePath.replace(/\.ts$/u, '.view.tsx'); const hasView = existsSync(view); if (hasView) { const viewText = readFileSync(view, 'utf8'); const viewExports = scanRouteModuleExports(viewText, view).named; - if (viewExports.has('config') || viewExports.has('preflight') || viewExports.has('before')) { + if (viewExports.has('config')) { throw new TypeError(`Keep event configuration and control flow in ${relativePath}; ${view} only renders JSX.`); } const diagnostics = validateEventRouteModuleContract(viewText, relativePath.replace(/\.ts$/u, '.view.tsx'), view); diff --git a/packages/agent-bundle/src/routes/types.ts b/packages/agent-bundle/src/routes/types.ts index 672d9ce83..90479c26a 100644 --- a/packages/agent-bundle/src/routes/types.ts +++ b/packages/agent-bundle/src/routes/types.ts @@ -145,8 +145,8 @@ export interface CompiledAgentRoute { /** Static cheap gate; present only on event routes that declare a valid relative default re-export. */ readonly handler?: CompiledEventHandler; readonly provenance: RouteProvenance; - /** Omitted only by legacy or manually assembled graphs, where consumers must treat the declaration as unknown. */ - readonly resultSchemaState?: RouteResultSchemaState; + /** Static declaration evidence: absent, unknown (module text unavailable), or unprojectable (named `resultSchema`). */ + readonly resultSchemaState: RouteResultSchemaState; /** The owning MCP server id (`mcp:`); MCP route kinds only. */ readonly serverId?: string; /** Absolute route module path. */ diff --git a/packages/agent-bundle/src/test/contract.ts b/packages/agent-bundle/src/test/contract.ts index 9a748636b..d96a196a3 100644 --- a/packages/agent-bundle/src/test/contract.ts +++ b/packages/agent-bundle/src/test/contract.ts @@ -34,7 +34,7 @@ * `ui://` URI in `listResources`, and `sweep` reads that resource. With the * default `apps: 'auto'` an app route needs no fixture entry — `coverage` * passes with a reason naming the auto-covered sweep. An explicit - * `{ kind: 'resource' }` (or legacy `{}`) fixture is always accepted; + * `{ kind: 'resource' }` fixture is always accepted; * `apps: 'explicit'` restores the requirement that every app route be listed. * * Stateful lifecycle fixtures replay over one open client at every boundary. @@ -161,8 +161,7 @@ export interface ContractResourceFixture { export interface ContractRouteFixture { /** * `'resource'` marks a resource/MCP App fixture (see `ContractResourceFixture`). - * Omit it for tool and prompt fixtures; a legacy `{}` still covers a - * resource or app route. + * Required for resource and app routes; omit it for tool and prompt fixtures. */ readonly kind?: ContractResourceFixture['kind']; /** Valid input for the sweep invocation (tools/prompts; resources need none). */ @@ -1578,6 +1577,12 @@ const resolveRouteFixture = ( fixture: undefined, }; } + if ((descriptor.kind === 'resource' || descriptor.kind === 'app') && fixture.kind !== 'resource') { + return { + coverage: failed(`resource/app route fixture must be { kind: "resource" }`), + fixture: undefined, + }; + } return { coverage: passed(), fixture }; }; diff --git a/packages/agent-bundle/tests/application-tree.test.ts b/packages/agent-bundle/tests/application-tree.test.ts index 12ff63b26..f0bb5a9d4 100644 --- a/packages/agent-bundle/tests/application-tree.test.ts +++ b/packages/agent-bundle/tests/application-tree.test.ts @@ -21,6 +21,7 @@ const route = ( id, kind, provenance: { kind: 'conventional' }, + resultSchemaState: kind === 'event-route' || kind === 'app' ? 'absent' : 'unprojectable', source, ...extra, }); diff --git a/packages/agent-bundle/tests/cli-projection.test.ts b/packages/agent-bundle/tests/cli-projection.test.ts index 6db07313f..ee36c3aae 100644 --- a/packages/agent-bundle/tests/cli-projection.test.ts +++ b/packages/agent-bundle/tests/cli-projection.test.ts @@ -388,6 +388,7 @@ describe('MCP tool CLI surface projections', () => { id: 'tool:demo/submit', kind: 'tool', provenance: { kind: 'conventional', relativePath: toolPath }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:demo', source: '/project/src/mcp/demo/tools/submit.tsx', }; diff --git a/packages/agent-bundle/tests/cli-routes.test.ts b/packages/agent-bundle/tests/cli-routes.test.ts index 92b26e902..79a9a3635 100644 --- a/packages/agent-bundle/tests/cli-routes.test.ts +++ b/packages/agent-bundle/tests/cli-routes.test.ts @@ -620,6 +620,7 @@ describe('generated bin normalization', () => { id: 'cli:inspect', kind: 'cli', provenance: { kind: 'conventional', relativePath: 'src/cli/inspect.ts' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/cli/inspect.ts', }], ...overrides, diff --git a/packages/agent-bundle/tests/entry-shell.test.ts b/packages/agent-bundle/tests/entry-shell.test.ts index 1f0d7e0b0..86870fbde 100644 --- a/packages/agent-bundle/tests/entry-shell.test.ts +++ b/packages/agent-bundle/tests/entry-shell.test.ts @@ -197,6 +197,7 @@ describe('generated entry templates', () => { id: 'cli:report', kind: 'cli' as const, provenance: { kind: 'conventional' as const, relativePath: 'src/cli/report.ts' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/cli/report.ts', }; const command = { aliases: [], exitCode: 'zero' as const, options: [], path: ['report'], rendered: false, routeId: 'cli:report' }; @@ -262,6 +263,7 @@ describe('generated entry templates', () => { id: 'cli:status', kind: 'cli' as const, provenance: { kind: 'conventional' as const, relativePath: 'src/cli/status.ts' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/cli/status.ts', }; const command = { @@ -575,6 +577,7 @@ it('fails the build on an MCP route the generated server cannot register', () => id: 'tool:curator/notice-inbox', kind: 'tool', provenance: { kind: 'conventional', relativePath: 'src/mcp/curator/tools/notice-inbox.tsx' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/mcp/curator/tools/notice-inbox.tsx', }], serverName: 'curator', @@ -593,6 +596,7 @@ it('fails the build on an MCP route the generated server cannot register', () => id: 'resource:curator/other', kind: 'resource', provenance: { kind: 'conventional', relativePath: 'src/mcp/curator/resources/other.tsx' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/mcp/curator/resources/other.tsx', }], serverName: 'curator', @@ -615,6 +619,7 @@ it('journals the lineage registry through sqlite only for workspace-durable proj id: 'tool:curator/inspect', kind: 'tool', provenance: { kind: 'conventional', relativePath: 'src/mcp/curator/tools/inspect.tsx' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/mcp/curator/tools/inspect.tsx', }], serverName: 'curator', @@ -724,6 +729,7 @@ it('generates bulk-projected MCP commands with the CLI invocation and preserves id: 'tool:curator/read_item', kind: 'tool' as const, provenance: { kind: 'conventional' as const, relativePath: 'src/mcp/curator/tools/read_item.tsx' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:curator', source: '/project/src/mcp/curator/tools/read_item.tsx', }; @@ -766,6 +772,7 @@ it('imports explicit CLI projections and maps their input before canonical valid id: 'tool:curator/submit', kind: 'tool' as const, provenance: { kind: 'conventional' as const, relativePath: 'src/mcp/curator/tools/submit.tsx' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:curator', source: '/project/src/mcp/curator/tools/submit.tsx', }; @@ -840,6 +847,7 @@ it('mounts the shell-probed terminal on every routed-CLI surface and forwards it id: 'cli:doctor', kind: 'cli' as const, provenance: { kind: 'conventional' as const, relativePath: 'src/cli/doctor.ts' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/cli/doctor.ts', }; const bin = entryShellModule.generatedCliBinEntrySource({ @@ -938,6 +946,7 @@ it('generates deterministic per-request provider execution in the shared Flight id: 'tool:curator/inspect', kind: 'tool', provenance: { kind: 'conventional', relativePath: 'src/mcp/curator/tools/inspect.tsx' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/mcp/curator/tools/inspect.tsx', }], serverName: 'curator', @@ -965,6 +974,7 @@ it('mounts deterministic per-request providers for plain routed CLI commands (#3 id: 'cli:doctor', kind: 'cli' as const, provenance: { kind: 'conventional' as const, relativePath: 'src/cli/doctor.ts' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/cli/doctor.ts', }; const command = { @@ -1056,6 +1066,7 @@ it('mounts deterministic per-request providers in rendered route workers', () => id: 'tool:curator/inspect', kind: 'tool', provenance: { kind: 'conventional', relativePath: 'src/mcp/curator/tools/inspect.tsx' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/mcp/curator/tools/inspect.tsx', }], }); @@ -1101,6 +1112,7 @@ it('keeps the generated provider loop and the in-process execution helper identi id: 'cli:report', kind: 'cli', provenance: { kind: 'conventional', relativePath: 'src/cli/report.tsx' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/cli/report.tsx', }], }); @@ -1204,6 +1216,7 @@ it('composes the root and server layout chain around generated MCP routes and ne id: 'tool:curator/inspect', kind: 'tool', provenance: { kind: 'conventional', relativePath: 'src/mcp/curator/tools/inspect.tsx' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:curator', source: '/project/src/mcp/curator/tools/inspect.tsx', }, @@ -1212,6 +1225,7 @@ it('composes the root and server layout chain around generated MCP routes and ne id: 'resource:other/catalog', kind: 'resource', provenance: { kind: 'conventional', relativePath: 'src/mcp/other/resources/catalog.tsx' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:other', source: '/project/src/mcp/other/resources/catalog.tsx', }, @@ -1250,6 +1264,7 @@ it('imports only the layouts some route of the worker composes through, never an id: 'cli:library/audit', kind: 'cli', provenance: { kind: 'conventional', relativePath: 'src/cli/library/audit.tsx' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/cli/library/audit.tsx', }, { @@ -1257,6 +1272,7 @@ it('imports only the layouts some route of the worker composes through, never an id: 'script:rebuild-index', kind: 'script', provenance: { kind: 'conventional', relativePath: 'src/scripts/rebuild-index.tsx' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/scripts/rebuild-index.tsx', }, ], @@ -1275,6 +1291,7 @@ it('imports only the layouts some route of the worker composes through, never an id: 'resource:other/catalog', kind: 'resource', provenance: { kind: 'conventional', relativePath: 'src/mcp/other/resources/catalog.tsx' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:other', source: '/project/src/mcp/other/resources/catalog.tsx', }], @@ -1292,6 +1309,7 @@ it('imports only the layouts some route of the worker composes through, never an id: 'cli:library/audit', kind: 'cli' as const, provenance: { kind: 'conventional' as const, relativePath: 'src/cli/library/audit.tsx' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/cli/library/audit.tsx', }]; expect(entryShellModule.generatedRenderedRouteWorkerSource({ layouts: serverOnly, routes: cliRoutes })) @@ -1306,6 +1324,7 @@ it('emits an identity composition when no layout exists so layout-free workers r id: 'tool:curator/inspect', kind: 'tool', provenance: { kind: 'conventional', relativePath: 'src/mcp/curator/tools/inspect.tsx' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:curator', source: '/project/src/mcp/curator/tools/inspect.tsx', }], @@ -1326,6 +1345,7 @@ it('hands rendered CLI, projected MCP, and script routes their layout chain and id: 'cli:library/audit', kind: 'cli', provenance: { kind: 'conventional', relativePath: 'src/cli/library/audit.tsx' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/cli/library/audit.tsx', }, { @@ -1333,6 +1353,7 @@ it('hands rendered CLI, projected MCP, and script routes their layout chain and id: 'tool:curator/inspect', kind: 'tool', provenance: { kind: 'conventional', relativePath: 'src/mcp/curator/tools/inspect.tsx' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:curator', source: '/project/src/mcp/curator/tools/inspect.tsx', }, @@ -1341,6 +1362,7 @@ it('hands rendered CLI, projected MCP, and script routes their layout chain and id: 'script:rebuild-index', kind: 'script', provenance: { kind: 'conventional', relativePath: 'src/scripts/rebuild-index.tsx' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/scripts/rebuild-index.tsx', }, ], @@ -1357,6 +1379,7 @@ it('conditionally emits generated state mounting without leaking sqlite into vol id: 'tool:curator/inspect', kind: 'tool', provenance: { kind: 'conventional', relativePath: 'src/mcp/curator/tools/inspect.tsx' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/mcp/curator/tools/inspect.tsx', } as const; const state = (lifetime: 'process' | 'request' | 'workspace-durable') => ({ diff --git a/packages/agent-bundle/tests/function-authoring.test.ts b/packages/agent-bundle/tests/function-authoring.test.ts index bafd19039..2de648faa 100644 --- a/packages/agent-bundle/tests/function-authoring.test.ts +++ b/packages/agent-bundle/tests/function-authoring.test.ts @@ -7,7 +7,6 @@ import { validateRouteModuleContract } from '../src/routes/contract.ts'; import { defineTool, normalizeRouteModule } from '../src/routes/definitions.ts'; import { parseModule } from '../src/routes/syntax.ts'; import { readRouteDefinition } from '../src/routes/definition-syntax.ts'; -import { eventHandlerEntry } from '../src/routes/event-handler.ts'; import { events } from '../src/routes/event-definitions.ts'; import { executeEventHandler } from '../src/events/handler.ts'; import { createCanonicalEventProps, projectEventHandlerResult } from '../src/events/projection.ts'; @@ -46,8 +45,7 @@ it('normalizes the same inferred handler for generated and source execution', as () => module.default({ input: inputSchema.parse({}), signal: new AbortController().signal }))).toBe('false:ready'); }); -it('rejects closure extraction and dynamic definition composition', () => { - expect(() => eventHandlerEntry('export function before() {} export default async function View() {}', 'before.tsx', '/before.tsx')).toThrow('no longer supported'); +it('rejects dynamic definition composition', () => { for (const declaration of [ 'const tool = createMyTool(); export default tool;', 'const opts = getOptions(); export default defineTool(opts, handler);', diff --git a/packages/agent-bundle/tests/generated-module-evidence.test.ts b/packages/agent-bundle/tests/generated-module-evidence.test.ts index ce56fa6b1..82bf33bb0 100644 --- a/packages/agent-bundle/tests/generated-module-evidence.test.ts +++ b/packages/agent-bundle/tests/generated-module-evidence.test.ts @@ -73,6 +73,7 @@ const cliRoute = (root: string): CompiledAgentRoute => ({ id: 'cli:report', kind: 'cli', provenance: { kind: 'conventional', relativePath: 'src/cli/report.ts' }, + resultSchemaState: 'unprojectable' as const, source: join(root, 'src', 'cli', 'report.ts'), }); diff --git a/packages/agent-bundle/tests/lifecycle-replay-routes.test.ts b/packages/agent-bundle/tests/lifecycle-replay-routes.test.ts index 226c1a89e..f22d9b022 100644 --- a/packages/agent-bundle/tests/lifecycle-replay-routes.test.ts +++ b/packages/agent-bundle/tests/lifecycle-replay-routes.test.ts @@ -28,6 +28,7 @@ const graph = Object.freeze({ id: 'event:prompt/submit', kind: 'event-route', provenance: Object.freeze({ kind: 'conventional', relativePath: 'src/events/prompt/submit.tsx' }), + resultSchemaState: 'absent' as const, source: '/project/src/events/prompt/submit.tsx', }, { @@ -36,6 +37,7 @@ const graph = Object.freeze({ id: 'event:tool/after', kind: 'event-route', provenance: Object.freeze({ kind: 'conventional', relativePath: 'src/events/tool/after.tsx' }), + resultSchemaState: 'absent' as const, source: '/project/src/events/tool/after.tsx', }, { @@ -44,6 +46,7 @@ const graph = Object.freeze({ id: 'event:tool/failure', kind: 'event-route', provenance: Object.freeze({ kind: 'conventional', relativePath: 'src/events/tool/failure.tsx' }), + resultSchemaState: 'absent' as const, source: '/project/src/events/tool/failure.tsx', }, ]), diff --git a/packages/agent-bundle/tests/lifecycle-replay-service.test.ts b/packages/agent-bundle/tests/lifecycle-replay-service.test.ts index c3dd3c32f..305935313 100644 --- a/packages/agent-bundle/tests/lifecycle-replay-service.test.ts +++ b/packages/agent-bundle/tests/lifecycle-replay-service.test.ts @@ -15,6 +15,7 @@ const graph = Object.freeze({ id: 'event:compact/after', kind: 'event-route', provenance: Object.freeze({ kind: 'conventional', relativePath: 'src/events/compact/after.tsx' }), + resultSchemaState: 'absent' as const, source: '/project/src/events/compact/after.tsx', }, { @@ -23,6 +24,7 @@ const graph = Object.freeze({ id: 'event:compact/before', kind: 'event-route', provenance: Object.freeze({ kind: 'conventional', relativePath: 'src/events/compact/before.tsx' }), + resultSchemaState: 'absent' as const, source: '/project/src/events/compact/before.tsx', }, { @@ -31,6 +33,7 @@ const graph = Object.freeze({ id: 'event:prompt/submit', kind: 'event-route', provenance: Object.freeze({ kind: 'conventional', relativePath: 'src/events/prompt/submit.tsx' }), + resultSchemaState: 'absent' as const, source: '/project/src/events/prompt/submit.tsx', }, { @@ -39,6 +42,7 @@ const graph = Object.freeze({ id: 'event:session/end', kind: 'event-route', provenance: Object.freeze({ kind: 'conventional', relativePath: 'src/events/session/end.tsx' }), + resultSchemaState: 'absent' as const, source: '/project/src/events/session/end.tsx', }, { @@ -47,6 +51,7 @@ const graph = Object.freeze({ id: 'event:tool/after', kind: 'event-route', provenance: Object.freeze({ kind: 'conventional', relativePath: 'src/events/tool/after.tsx' }), + resultSchemaState: 'absent' as const, source: '/project/src/events/tool/after.tsx', }, { @@ -55,6 +60,7 @@ const graph = Object.freeze({ id: 'event:tool/failure', kind: 'event-route', provenance: Object.freeze({ kind: 'conventional', relativePath: 'src/events/tool/failure.tsx' }), + resultSchemaState: 'absent' as const, source: '/project/src/events/tool/failure.tsx', }, ]), diff --git a/packages/agent-bundle/tests/normalization.test.ts b/packages/agent-bundle/tests/normalization.test.ts index 7343afe2d..4e08fe466 100644 --- a/packages/agent-bundle/tests/normalization.test.ts +++ b/packages/agent-bundle/tests/normalization.test.ts @@ -1120,6 +1120,7 @@ const scriptRouteFixture = (root: string, relativePath: string): CompiledAgentRo id: `script:${identity}`, kind: 'script', provenance: { kind: 'conventional', relativePath }, + resultSchemaState: 'unprojectable' as const, source: `${root}/${relativePath}`, }; }; @@ -1313,6 +1314,7 @@ const routeGraphWithGeneratedServer = (root: string): CompiledRouteGraph => { id: 'tool:curator/inspect', kind: 'tool', provenance: { kind: 'conventional', relativePath: 'src/mcp/curator/tools/inspect.tsx' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:curator', source: `${root}/src/mcp/curator/tools/inspect.tsx`, }; @@ -1339,6 +1341,7 @@ it('carries event-route handler provenance and provider selection into normalize view: `${root}/src/events/tool/after.view.tsx`, }, provenance: { kind: 'conventional', relativePath: 'src/events/tool/after.tsx' }, + resultSchemaState: 'absent' as const, source: `${root}/src/events/tool/after.tsx`, }; const inherited: CompiledAgentRoute = { @@ -1347,6 +1350,7 @@ it('carries event-route handler provenance and provider selection into normalize id: 'event:session/start', kind: 'event-route', provenance: { kind: 'conventional', relativePath: 'src/events/session/start.tsx' }, + resultSchemaState: 'absent' as const, source: `${root}/src/events/session/start.tsx`, }; const routeGraph: CompiledRouteGraph = { diff --git a/packages/agent-bundle/tests/route-graph.test.ts b/packages/agent-bundle/tests/route-graph.test.ts index 35682bc43..c4e22c33a 100644 --- a/packages/agent-bundle/tests/route-graph.test.ts +++ b/packages/agent-bundle/tests/route-graph.test.ts @@ -1407,6 +1407,7 @@ it('generates deterministic route-specific types from the compiled graph', () => id: 'event:workspace/open', kind: 'event-route', provenance: { kind: 'conventional', relativePath: 'src/events/workspace/open.tsx' }, + resultSchemaState: 'absent' as const, source: '/workspace/project/src/events/workspace/open.tsx', }], providers: [], @@ -1415,6 +1416,7 @@ it('generates deterministic route-specific types from the compiled graph', () => id: 'script:rebuild-index', kind: 'script', provenance: { kind: 'conventional', relativePath: 'src/scripts/rebuild-index.ts' }, + resultSchemaState: 'unprojectable' as const, source: '/workspace/project/src/scripts/rebuild-index.ts', }], servers: [{ @@ -1426,6 +1428,7 @@ it('generates deterministic route-specific types from the compiled graph', () => id: 'tool:curator/inspect', kind: 'tool', provenance: { kind: 'conventional', relativePath: 'src/mcp/curator/tools/inspect.tsx' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:curator', source: '/workspace/project/src/mcp/curator/tools/inspect.tsx', }], @@ -1483,6 +1486,7 @@ it('omits the App registration for graphs without an MCP tool route', () => { id: `${kind}:curator/${name}`, kind, provenance: { kind: 'conventional', relativePath: `src/mcp/curator/${collection}/${name}.ts` }, + resultSchemaState: kind === 'app' ? 'absent' : 'unprojectable', serverId: 'mcp:curator', source: `/workspace/project/src/mcp/curator/${collection}/${name}.ts`, }); @@ -1496,6 +1500,7 @@ it('omits the App registration for graphs without an MCP tool route', () => { id: 'cli:report', kind: 'cli', provenance: { kind: 'conventional', relativePath: 'src/cli/report.ts' }, + resultSchemaState: 'unprojectable' as const, source: '/workspace/project/src/cli/report.ts', }], }, @@ -1507,6 +1512,7 @@ it('omits the App registration for graphs without an MCP tool route', () => { id: 'event:workspace/open', kind: 'event-route', provenance: { kind: 'conventional', relativePath: 'src/events/workspace/open.tsx' }, + resultSchemaState: 'absent' as const, source: '/workspace/project/src/events/workspace/open.tsx', }], providers: [{ @@ -1584,6 +1590,7 @@ it('generates provider declarations and the runtime augmentation in execution or id: 'tool:curator/inspect', kind: 'tool', provenance: { kind: 'conventional', relativePath: 'src/mcp/curator/tools/inspect.tsx' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:curator', source: '/workspace/project/src/mcp/curator/tools/inspect.tsx', }], @@ -1978,18 +1985,6 @@ it('pairs a lightweight handler with its explicit JSX view', async () => { }); }); -it('rejects removed before and preflight exports with migration guidance', async () => { - const root = await createRoot(); - await writeTree(root, { - 'src/events/tool/before.tsx': "export { default as preflight } from '../../gate.js'; export default async function View() {}", - 'src/events/session/start.tsx': 'export function before() {} export default async function View() {}', - 'src/gate.ts': "export default () => 'execute';", - }); - const graph = await compileRouteGraph(root, fixtureConfig()); - expect(graph.diagnostics.map(({ code }) => code)).toEqual(['AB4840', 'AB4840']); - expect(graph.diagnostics.every(({ recovery }) => recovery?.includes('.view.tsx'))).toBe(true); -}); - it('fails unavailable event routes before packaging while admitting supported targets', async () => { const eventSource = 'export default async function WorkspaceOpen() { return undefined; }\n'; const configSource = [ diff --git a/packages/agent-bundle/tests/route-invocation-retention.test.ts b/packages/agent-bundle/tests/route-invocation-retention.test.ts index 812817180..ec42844ed 100644 --- a/packages/agent-bundle/tests/route-invocation-retention.test.ts +++ b/packages/agent-bundle/tests/route-invocation-retention.test.ts @@ -26,6 +26,7 @@ const echoRoute = { id: 'tool:fixture/echo', kind: 'tool', provenance: { kind: 'conventional' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:fixture', source: 'src/mcp/fixture/tools/echo.tsx', } as const; diff --git a/packages/agent-bundle/tests/route-invocation-service.test.ts b/packages/agent-bundle/tests/route-invocation-service.test.ts index 4cb2bb86d..79ab137c6 100644 --- a/packages/agent-bundle/tests/route-invocation-service.test.ts +++ b/packages/agent-bundle/tests/route-invocation-service.test.ts @@ -184,6 +184,7 @@ it('publishes correlated invocation and kernel entries with slim details', async id: 'tool:fixture/echo', kind: 'tool', provenance: { kind: 'conventional' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:fixture', source: 'src/mcp/fixture/tools/echo.tsx', } as const; @@ -335,6 +336,7 @@ it('publishes failed event invocations with native provenance', async () => { id: 'event:tool/after', kind: 'event-route', provenance: { kind: 'conventional' }, + resultSchemaState: 'absent' as const, source: 'src/events/tool/after.tsx', } as const; const trace = collectingTrace(); @@ -484,6 +486,7 @@ it.each([ id: 'event:tool/after', kind: 'event-route', provenance: { kind: 'conventional' }, + resultSchemaState: 'absent' as const, source: 'src/events/tool/after.tsx', } as const; let production: RouteInvocationChildRequest['production']; @@ -556,6 +559,7 @@ const echoRoute = { id: 'tool:fixture/echo', kind: 'tool', provenance: { kind: 'conventional' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:fixture', source: 'src/mcp/fixture/tools/echo.tsx', } as const; @@ -1097,6 +1101,7 @@ it('rejects a canonical event surface when the compiled route has handler', asyn execution: { fallback: 'standalone', handler: 'src/events/tool/before.handler.ts', runtime: 'standalone' }, kind: 'event-route', provenance: { kind: 'conventional' }, + resultSchemaState: 'absent' as const, source: 'src/events/tool/before.tsx', } as const; let leases = 0; @@ -1137,6 +1142,7 @@ it('rejects a globally supported host absent from the route executable bindings' execution: { fallback: 'standalone', handler: 'src/events/tool/before.handler.ts', runtime: 'standalone' }, kind: 'event-route', provenance: { kind: 'conventional' }, + resultSchemaState: 'absent' as const, source: 'src/events/tool/before.tsx', } as const; let childStarts = 0; @@ -1218,6 +1224,7 @@ const routeProject = async ( id: `tool:fixture/${name}`, kind: 'tool', provenance: { kind: 'conventional', relativePath }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:fixture', source, } as const; @@ -1244,6 +1251,7 @@ const routeProject = async ( id: compiled.id, kind: compiled.kind, provenance: { kind: 'conventional' }, + resultSchemaState: compiled.resultSchemaState, serverId: compiled.serverId, source: relativePath, }], @@ -1525,6 +1533,7 @@ it('forwards kernel events from tool and event routes rendered in the real child id: 'tool:fixture/traced', kind: 'tool', provenance: { kind: 'conventional', relativePath: 'src/mcp/fixture/tools/traced.tsx' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:fixture', source: toolSource, } as const; @@ -1534,6 +1543,7 @@ it('forwards kernel events from tool and event routes rendered in the real child id: 'event:tool/before', kind: 'event-route', provenance: { kind: 'conventional', relativePath: 'src/events/tool/before.tsx' }, + resultSchemaState: 'absent' as const, source: eventSource, } as const; const graph = { @@ -1553,6 +1563,7 @@ it('forwards kernel events from tool and event routes rendered in the real child id: eventRoute.id, kind: eventRoute.kind, provenance: { kind: 'conventional' }, + resultSchemaState: eventRoute.resultSchemaState, source: eventRoute.provenance.relativePath, }], providers: [], @@ -1566,6 +1577,7 @@ it('forwards kernel events from tool and event routes rendered in the real child id: toolRoute.id, kind: toolRoute.kind, provenance: { kind: 'conventional' }, + resultSchemaState: toolRoute.resultSchemaState, serverId: toolRoute.serverId, source: toolRoute.provenance.relativePath, }], @@ -1698,6 +1710,7 @@ it('reports an event route kind for unit-render provenance', async () => { id: 'event:tool/after', kind: 'event-route', provenance: { kind: 'conventional' }, + resultSchemaState: 'absent' as const, source: 'src/events/tool/after.tsx', } as const; const service = new RouteInvocationService({ diff --git a/packages/agent-bundle/tests/route-manifest-routes.test.ts b/packages/agent-bundle/tests/route-manifest-routes.test.ts index 20b0b0cc1..730aa4529 100644 --- a/packages/agent-bundle/tests/route-manifest-routes.test.ts +++ b/packages/agent-bundle/tests/route-manifest-routes.test.ts @@ -259,6 +259,7 @@ it('projects a CLI surface projection and option aliases without leaking project inputSchema: input, kind: 'tool' as const, provenance: { kind: 'conventional' as const, relativePath: 'src/mcp/hauler/tools/hauler_request.tsx' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:hauler', source: '/project/src/mcp/hauler/tools/hauler_request.tsx', }; @@ -389,6 +390,7 @@ it('passes the bounded input schema through as the optional manifest wire field' inputSchema, kind: 'script', provenance: { kind: 'conventional', relativePath: 'src/scripts/inspect.ts' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/scripts/inspect.ts', }], }; @@ -418,6 +420,7 @@ it('projects shared route contracts and omits them from contract-free graphs', ( inputSchema: input, kind: 'cli' as const, provenance: { kind: 'conventional' as const, relativePath: 'src/cli/status.ts' }, + resultSchemaState: 'unprojectable' as const, source: '/project/src/cli/status.ts', }; const toolRoute = { @@ -427,6 +430,7 @@ it('projects shared route contracts and omits them from contract-free graphs', ( inputSchema: input, kind: 'tool' as const, provenance: { kind: 'conventional' as const, relativePath: 'src/mcp/hauler/tools/hauler_status.ts' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:hauler', source: '/project/src/mcp/hauler/tools/hauler_status.ts', }; diff --git a/packages/agent-bundle/tests/route-typegen-write.test.ts b/packages/agent-bundle/tests/route-typegen-write.test.ts index d8b76152e..47796dd4c 100644 --- a/packages/agent-bundle/tests/route-typegen-write.test.ts +++ b/packages/agent-bundle/tests/route-typegen-write.test.ts @@ -41,6 +41,7 @@ const oneRouteGraph = (root: string): CompiledRouteGraph => ({ id: 'tool:curator/inspect', kind: 'tool', provenance: { kind: 'conventional', relativePath: 'src/mcp/curator/tools/inspect.tsx' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:curator', source: join(root, 'src/mcp/curator/tools/inspect.tsx'), }], @@ -82,6 +83,7 @@ describe('writeRouteTypes', () => { id: 'prompt:curator/brief', kind: 'prompt', provenance: { kind: 'conventional', relativePath: 'src/mcp/curator/prompts/brief.ts' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:curator', source: join(root, 'src/mcp/curator/prompts/brief.ts'), }], diff --git a/packages/agent-bundle/tests/support/contract-matrix-fixtures.ts b/packages/agent-bundle/tests/support/contract-matrix-fixtures.ts index 08f82486b..e3ff78304 100644 --- a/packages/agent-bundle/tests/support/contract-matrix-fixtures.ts +++ b/packages/agent-bundle/tests/support/contract-matrix-fixtures.ts @@ -83,7 +83,7 @@ const lifecycleFixture = (revisionOffset = 0): ContractRouteFixture => ({ /** Shared route-harness fixtures for projection-level contract matrix tests. */ export const routeHarnessContractFixtures = (): Record => ({ 'prompt:harness/summarize': { input: { note: 'chapter one' } }, - 'resource:harness/notes': {}, + 'resource:harness/notes': { kind: 'resource' }, 'tool:harness/catalog': { input: { genre: 'mystery' }, resultCompat: 'additive' }, 'tool:harness/context': { resultCompat: 'closed' }, 'tool:harness/echo': { input: { message: 'contract matrix' }, resultCompat: 'additive' }, diff --git a/packages/agent-bundle/tests/web-config.test.ts b/packages/agent-bundle/tests/web-config.test.ts index e4554ee5c..24e94763d 100644 --- a/packages/agent-bundle/tests/web-config.test.ts +++ b/packages/agent-bundle/tests/web-config.test.ts @@ -41,6 +41,7 @@ const route = ( id: `${kind}:catalog/${name}`, kind, provenance: { kind: 'conventional', relativePath: `src/mcp/catalog/${kind}s/${name}.ts` }, + resultSchemaState: kind === 'event-route' || kind === 'app' ? 'absent' : 'unprojectable', serverId: 'mcp:catalog', source: `${root}/src/mcp/catalog/${kind}s/${name}.ts`, }); @@ -79,12 +80,14 @@ const discovered = (options: { id: 'cli:web', kind: 'cli', provenance: { kind: 'conventional', relativePath: 'src/cli/web.ts' }, + resultSchemaState: 'unprojectable', source: `${root}/src/cli/web.ts`, }, { config: { aliases: ['web'] }, id: 'cli:dashboard', kind: 'cli', provenance: { kind: 'conventional', relativePath: 'src/cli/dashboard.ts' }, + resultSchemaState: 'unprojectable', source: `${root}/src/cli/dashboard.ts`, }], } as const, diff --git a/packages/workbench/src/application/route-inspector.tsx b/packages/workbench/src/application/route-inspector.tsx index 558c35cef..d5591eee2 100644 --- a/packages/workbench/src/application/route-inspector.tsx +++ b/packages/workbench/src/application/route-inspector.tsx @@ -119,7 +119,7 @@ const resultSchemaRows = ( ? 'Unavailable · this invocation recorded no structured result.' : 'Available · open Structured result.', ); - const state = leaf.resultSchemaState ?? 'unknown'; + const state = leaf.resultSchemaState; switch (state) { case 'absent': return [ diff --git a/packages/workbench/src/routes/route-manifest-client.ts b/packages/workbench/src/routes/route-manifest-client.ts index bbf45e3cf..5759ed8f1 100644 --- a/packages/workbench/src/routes/route-manifest-client.ts +++ b/packages/workbench/src/routes/route-manifest-client.ts @@ -116,7 +116,7 @@ const routeSchema: z.ZodType = z.strictObject({ inputSchema: inputSchema.optional(), kind: z.enum(['app', 'cli', 'event-route', 'prompt', 'resource', 'script', 'tool']), provenance: z.strictObject({ kind: z.literal('conventional') }), - resultSchemaState: z.enum(['absent', 'unknown', 'unprojectable']).optional(), + resultSchemaState: z.enum(['absent', 'unknown', 'unprojectable']), serverId: z.string().optional(), source: z.string(), }); diff --git a/packages/workbench/tests/application-tree.test.tsx b/packages/workbench/tests/application-tree.test.tsx index eaf624786..63a1fd2c8 100644 --- a/packages/workbench/tests/application-tree.test.tsx +++ b/packages/workbench/tests/application-tree.test.tsx @@ -26,6 +26,7 @@ const tree: ApplicationTree = { key: '/routes/mcp/library/tool/search', label: 'search', ref: { kind: 'tool', name: 'search', server: 'library' }, + resultSchemaState: 'unprojectable' as const, routeId: 'tool:library/search', source: 'src/mcp/library/tools/search.ts', }], @@ -41,6 +42,7 @@ const tree: ApplicationTree = { key: '/routes/skills/review', label: 'Review', ref: { id: 'review', kind: 'skill' }, + resultSchemaState: 'absent' as const, source: 'skills/review/SKILL.md', }], }], diff --git a/packages/workbench/tests/build-status-model.test.ts b/packages/workbench/tests/build-status-model.test.ts index 4da52db60..adf4ed31b 100644 --- a/packages/workbench/tests/build-status-model.test.ts +++ b/packages/workbench/tests/build-status-model.test.ts @@ -182,6 +182,7 @@ const treeWith = (source: string): ApplicationTree => ({ key: '/routes/scripts/sync', label: 'sync', ref: { kind: 'script', name: 'sync' }, + resultSchemaState: 'unprojectable' as const, routeId: 'script:sync', source, }], diff --git a/packages/workbench/tests/dev-server-backend.test.ts b/packages/workbench/tests/dev-server-backend.test.ts index 2f98e2c6e..a2a3f74e9 100644 --- a/packages/workbench/tests/dev-server-backend.test.ts +++ b/packages/workbench/tests/dev-server-backend.test.ts @@ -58,6 +58,7 @@ const leaf = Object.freeze({ key: '/routes/mcp/curator/tool/search_audible', label: 'Search Audible', ref: Object.freeze({ kind: 'tool' as const, name: 'search_audible', server: 'curator' }), + resultSchemaState: 'unprojectable' as const, routeId: invocation.routeId, }) satisfies ApplicationLeaf; diff --git a/packages/workbench/tests/invocation-model.test.ts b/packages/workbench/tests/invocation-model.test.ts index 4f0ba15cb..66b714ce2 100644 --- a/packages/workbench/tests/invocation-model.test.ts +++ b/packages/workbench/tests/invocation-model.test.ts @@ -56,6 +56,7 @@ const leaf = Object.freeze({ key: '/routes/mcp/curator/tool/search_audible', label: 'Search Audible', ref: Object.freeze({ kind: 'tool' as const, name: 'search_audible', server: 'curator' }), + resultSchemaState: 'unprojectable' as const, routeId: invocation.routeId, }) satisfies ApplicationLeaf; diff --git a/packages/workbench/tests/route-manifest-client.test.ts b/packages/workbench/tests/route-manifest-client.test.ts index 01db0d44b..9c60594fd 100644 --- a/packages/workbench/tests/route-manifest-client.test.ts +++ b/packages/workbench/tests/route-manifest-client.test.ts @@ -29,6 +29,7 @@ const manifest = { id: 'cli:library/audit', kind: 'cli', provenance: { kind: 'conventional' }, + resultSchemaState: 'unprojectable', source: 'src/cli/library/audit.ts', }], }, @@ -56,6 +57,7 @@ const manifest = { id: 'event:tool/after', kind: 'event-route', provenance: { kind: 'conventional' }, + resultSchemaState: 'absent', source: 'src/events/tool/after.ts', }], providers: [{ id: 'provider:library', name: 'library', source: 'src/providers/library.ts' }], @@ -64,6 +66,7 @@ const manifest = { id: 'script:convert', kind: 'script', provenance: { kind: 'conventional' }, + resultSchemaState: 'unprojectable', source: 'src/scripts/convert.ts', }], servers: [{ diff --git a/packages/workbench/tests/route-workspace.test.ts b/packages/workbench/tests/route-workspace.test.ts index 51f3424f0..60d49efe0 100644 --- a/packages/workbench/tests/route-workspace.test.ts +++ b/packages/workbench/tests/route-workspace.test.ts @@ -31,7 +31,7 @@ import { const noop = (): void => undefined; -const schemaLeaf = (resultSchemaState: NonNullable): ApplicationLeaf => +const schemaLeaf = (resultSchemaState: ApplicationLeaf['resultSchemaState']): ApplicationLeaf => ({ ...toolLeaf, resultSchemaState }); const renderSchemaInspector = (leaf: ApplicationLeaf, envelope?: RouteInvocation): string => diff --git a/packages/workbench/tests/routes-model.test.ts b/packages/workbench/tests/routes-model.test.ts index 1bf980cd1..58cb5d8c5 100644 --- a/packages/workbench/tests/routes-model.test.ts +++ b/packages/workbench/tests/routes-model.test.ts @@ -33,6 +33,7 @@ const manifest: RouteManifest = { id: 'cli:library/audit', kind: 'cli', provenance: { kind: 'conventional' }, + resultSchemaState: 'unprojectable' as const, source: 'src/cli/library/audit.ts', }], }, @@ -55,6 +56,7 @@ const manifest: RouteManifest = { id: 'event:tool/after', kind: 'event-route', provenance: { kind: 'conventional' }, + resultSchemaState: 'absent' as const, source: 'src/events/tool/after.ts', }], providers: [ @@ -66,6 +68,7 @@ const manifest: RouteManifest = { id: 'script:convert', kind: 'script', provenance: { kind: 'conventional' }, + resultSchemaState: 'unprojectable' as const, source: 'src/scripts/convert.ts', }], servers: [ @@ -78,6 +81,7 @@ const manifest: RouteManifest = { id: 'prompt:zeta/summarize', kind: 'prompt', provenance: { kind: 'conventional' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:zeta', source: 'src/mcp/zeta/prompts/summarize.ts', }], @@ -104,6 +108,7 @@ const manifest: RouteManifest = { }, kind: 'tool', provenance: { kind: 'conventional' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:alpha', source: 'src/mcp/alpha/tools/echo.ts', }, @@ -112,6 +117,7 @@ const manifest: RouteManifest = { id: 'tool:alpha/build', kind: 'tool', provenance: { kind: 'conventional' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:alpha', source: 'src/mcp/alpha/tools/build.ts', }, @@ -120,6 +126,7 @@ const manifest: RouteManifest = { id: 'resource:alpha/notes', kind: 'resource', provenance: { kind: 'conventional' }, + resultSchemaState: 'unprojectable' as const, serverId: 'mcp:alpha', source: 'src/mcp/alpha/resources/notes.ts', }, @@ -258,6 +265,7 @@ it('exposes a CLI surface projection and option aliases on the catalog entry', ( }, kind: 'tool', provenance: { kind: 'conventional' }, + resultSchemaState: 'unprojectable' as const, source: 'src/mcp/hauler/tools/hauler_request.tsx', }], }, diff --git a/packages/workbench/tests/runtime-backend.test.ts b/packages/workbench/tests/runtime-backend.test.ts index 6a74c4ccd..0fa9a152d 100644 --- a/packages/workbench/tests/runtime-backend.test.ts +++ b/packages/workbench/tests/runtime-backend.test.ts @@ -70,6 +70,7 @@ const leaf = Object.freeze({ key: '/routes/mcp/curator/tool/search_audible', label: 'Search Audible', ref: Object.freeze({ kind: 'tool' as const, name: 'search_audible', server: 'curator' }), + resultSchemaState: 'unprojectable' as const, routeId: 'tool:curator/search_audible', source: 'src/mcp/curator/tools/search_audible.tsx', }) satisfies ApplicationLeaf; diff --git a/packages/workbench/tests/support/workspace-fixtures.ts b/packages/workbench/tests/support/workspace-fixtures.ts index 526c19ca2..7a86fb82a 100644 --- a/packages/workbench/tests/support/workspace-fixtures.ts +++ b/packages/workbench/tests/support/workspace-fixtures.ts @@ -83,6 +83,7 @@ export const cliLeaf: ApplicationLeaf = Object.freeze({ key: '/routes/cli/audible/search', label: 'audible search', ref: Object.freeze({ kind: 'cli' as const, path: Object.freeze(['audible', 'search']) }), + resultSchemaState: 'unprojectable', routeId: 'cli:audible/search', source: 'src/cli/audible/search.tsx', }); @@ -94,6 +95,7 @@ export const eventLeaf: ApplicationLeaf = Object.freeze({ key: '/routes/events/tool/before', label: 'tool/before', ref: Object.freeze({ event: 'tool/before', kind: 'event' as const }), + resultSchemaState: 'absent', routeId: 'event:tool/before', source: 'src/events/tool/before.tsx', }); @@ -104,6 +106,7 @@ export const appLeaf: ApplicationLeaf = Object.freeze({ key: '/routes/mcp/curator/app/library', label: 'library', ref: Object.freeze({ kind: 'app' as const, name: 'library', server: 'curator' }), + resultSchemaState: 'absent', routeId: 'app:curator/library', source: 'src/mcp/curator/apps/library.tsx', }); @@ -114,6 +117,7 @@ export const skillLeaf: ApplicationLeaf = Object.freeze({ key: '/routes/skills/skill%3Areview', label: 'review', ref: Object.freeze({ id: 'skill:review', kind: 'skill' as const }), + resultSchemaState: 'absent', source: 'skills/review/SKILL.md', }); @@ -123,6 +127,7 @@ export const ruleLeaf: ApplicationLeaf = Object.freeze({ key: '/routes/rules/style', label: 'style', ref: Object.freeze({ id: 'style', kind: 'rule' as const }), + resultSchemaState: 'absent', source: 'rules/style.md', }); diff --git a/packages/workbench/tests/workbench-capabilities.test.ts b/packages/workbench/tests/workbench-capabilities.test.ts index 803c6750a..0b7473541 100644 --- a/packages/workbench/tests/workbench-capabilities.test.ts +++ b/packages/workbench/tests/workbench-capabilities.test.ts @@ -111,6 +111,7 @@ const route = (id: string, kind: RouteManifest['events'][number]['kind'], relati id, kind, provenance: { kind: 'conventional' as const }, + resultSchemaState: kind === 'event-route' || kind === 'app' ? 'absent' as const : 'unprojectable' as const, source: relativePath, }); diff --git a/website/docs/en/guide/authoring/hooks.mdx b/website/docs/en/guide/authoring/hooks.mdx index c939044ee..b4f5e2cdc 100644 --- a/website/docs/en/guide/authoring/hooks.mdx +++ b/website/docs/en/guide/authoring/hooks.mdx @@ -133,14 +133,6 @@ disposer restores the previous sink. The trace types and helpers are exported fr Rendered events default to `standalone`; opt into `runtime: 'shared'` to reuse a generated MCP server. Lightweight `.ts` handlers always run in the hook wrapper. -### Migrating existing events - -Move the old preflight function to the default export of the `.ts` event file, move JSX to -its `.view.tsx` sibling, and replace `execute` results with `ctx.render('./name.view.js', data)`. -Remove the `preflight` re-export. Inline `before()` and legacy `preflight` exports now fail -with `AB4840`. `continue` and `deny` retain their meanings. Remove `config.providers` and use -`context.provider(key)`. Native config hooks remain an advanced compatibility surface. - ### Request providers Call `await context.provider('writePolicy')` in a function event, or diff --git a/website/docs/zh/guide/authoring/hooks.mdx b/website/docs/zh/guide/authoring/hooks.mdx index ade130c68..1098b0b31 100644 --- a/website/docs/zh/guide/authoring/hooks.mdx +++ b/website/docs/zh/guide/authoring/hooks.mdx @@ -119,13 +119,6 @@ export default async function BeforeWrite( 渲染事件默认使用 `standalone`;设置 `runtime: 'shared'` 可复用生成的 MCP 服务器。轻量 `.ts` 处理器始终在钩子包装器中执行。 -### 迁移已有事件 - -将原预检函数改为 `.ts` 事件文件的默认导出,将 JSX 移至同级 `.view.tsx`,并用 -`ctx.render('./name.view.js', data)` 替换 `execute` 结果。删除 `preflight` 重导出。 -内联 `before()` 和旧 `preflight` 导出现在都会触发 `AB4840`。`continue` 和 `deny` 含义不变。 -删除 `config.providers`,改用 `context.provider(key)`。原生配置钩子保留为高级兼容接口。 - ### 请求级 provider 普通事件中使用 `await context.provider('writePolicy')`,JSX 中使用