Found while implementing #8140, which narrowed return types only. This is on the input side
and was deliberately not ridden into that PR.
Measured at 1f6d04703.
What is wrong
client.reports.save declares its parameter as any:
save: async(report: any): Promise<SavedReport>=>{constres=awaitthis.fetch(`${this.baseUrl}/api/v1/reports`,{method: 'POST',body: JSON.stringify(report??{}),});returnthis.unwrapResponse<SavedReport>(res);},The type the route's service method actually takes is SaveReportInput
(packages/spec/src/contracts/report-service.ts:89-97), on which query: ReportQuery is
required:
exportinterfaceSaveReportInput{id?: string;name: string;description?: string;object: string;query: ReportQuery;// ← requiredformat?: ReportFormat;ownerId?: string;}And the route does not check it either — POST /reports forwards the body straight through
(packages/rest/src/rest-server.ts:10293):
constrow=awaitsvc.saveReport(req.body??{},context??{});So a caller can send a report definition with no query at all; whether that is refused, stored
half-built, or throws depends entirely on which reports implementation is mounted.
How it was measured — this is not a code-reading claim
While implementing #8140 I briefly bound the parameter to SaveReportInput. tsc immediately
failed on this repo's own test, packages/client/src/client.test.ts:352:
src/client.test.ts(352,35): error TS2345: Argument of type '{ name: string; object: string; }'
is not assignable to parameter of type 'SaveReportInput'.
Property 'query' is missing in type '{ name: string; object: string; }' but required in
type 'SaveReportInput'.
That call is client.reports.save({ name: 'Pipeline', object: 'lead' }) — a fixture that has been
constructing an input the contract refuses, invisibly, because the parameter is any. I reverted
the parameter narrowing (out of #8140's declared scope) rather than adjust the fixture to make a
type error go away, since the fixture is evidence, not noise.
⚠️The fixture is not itself the defect and should not be "fixed" alone: it is a mock-transport
test that never reaches a real service, so adding a query to it silences the signal without
closing anything. The defect is that nothing on the path — SDK parameter, route, or a schema at the
door — states the requirement.
Adjacent, same class
Seven public SDK methods take a parameter typed any (packages/client/src/index.ts):
$ grep -nE '^\s+[a-zA-Z_]+: async \(.*: any[,)]' packages/client/src/index.ts
1161: query: async (payload: any) => {
1185: explain: async (payload: any) => {
3039: upload: async (file: any, scope: string = 'user'): Promise<FileUploadResponse> => {
3264: trigger: async (triggerName: string, payload: any) => {
3302: create: async (name: string, definition: any): Promise<any> => {
3318: update: async (name: string, definition: any): Promise<any> => {
4185: save: async (report: any): Promise<SavedReport> => {
reports.save is the one with a measured consequence, so it is the subject of this card; the
other six are listed so a fix can consider whether the same reachable-contract argument applies
(automation.create / update at 3302 / 3318 are the input half of #11924's routes).
Why it matters beyond typing
This is the contract-first shape from AGENTS.md Prime Directive #12 read from the producer side: an
authoring surface that accepts off-spec input and passes it to a service that requires more. The
right fix is to reject it loudly at the door — a schema at the route, or the parameter typed to the
contract, or both — rather than to keep the SDK permissive and let each reports implementation
decide what a query-less report means.
Generated by Claude Code
Found while implementing #8140, which narrowed return types only. This is on the input side
and was deliberately not ridden into that PR.
Measured at
1f6d04703.What is wrong
client.reports.savedeclares its parameter asany:The type the route's service method actually takes is
SaveReportInput(
packages/spec/src/contracts/report-service.ts:89-97), on whichquery: ReportQueryisrequired:
And the route does not check it either —
POST /reportsforwards the body straight through(
packages/rest/src/rest-server.ts:10293):So a caller can send a report definition with no query at all; whether that is refused, stored
half-built, or throws depends entirely on which reports implementation is mounted.
How it was measured — this is not a code-reading claim
While implementing #8140 I briefly bound the parameter to
SaveReportInput.tscimmediatelyfailed on this repo's own test,
packages/client/src/client.test.ts:352:That call is
client.reports.save({ name: 'Pipeline', object: 'lead' })— a fixture that has beenconstructing an input the contract refuses, invisibly, because the parameter is
any. I revertedthe parameter narrowing (out of #8140's declared scope) rather than adjust the fixture to make a
type error go away, since the fixture is evidence, not noise.
test that never reaches a real service, so adding a
queryto it silences the signal withoutclosing anything. The defect is that nothing on the path — SDK parameter, route, or a schema at the
door — states the requirement.
Adjacent, same class
Seven public SDK methods take a parameter typed
any(packages/client/src/index.ts):reports.saveis the one with a measured consequence, so it is the subject of this card; theother six are listed so a fix can consider whether the same reachable-contract argument applies
(
automation.create/updateat 3302 / 3318 are the input half of #11924's routes).Why it matters beyond typing
This is the contract-first shape from AGENTS.md Prime Directive #12 read from the producer side: an
authoring surface that accepts off-spec input and passes it to a service that requires more. The
right fix is to reject it loudly at the door — a schema at the route, or the parameter typed to the
contract, or both — rather than to keep the SDK permissive and let each reports implementation
decide what a query-less report means.
Generated by Claude Code