From e0b20d956d90c6b9c6aa019f20edef7cc2e85ef3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 07:45:11 +0000 Subject: [PATCH] feat(s): add s.null schema helper for the JSON null literal type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add NullSchema type, s.null helper, InferSchema → null, validateSchema null check, and test coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- skills/rig/rig.ts | 12 +++++++++--- src/rig.test.ts | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/skills/rig/rig.ts b/skills/rig/rig.ts index b1cf7c0..d906138 100644 --- a/skills/rig/rig.ts +++ b/skills/rig/rig.ts @@ -3,7 +3,7 @@ * @purpose Minimal TypeScript multi-agent harness: typed input/output schemas, prompt intents, sub-agent delegation, Copilot SDK runtime * @deps @github/copilot-sdk (CopilotClient,RuntimeConnection,approveAll); node:path,url,fs/promises,child_process,util * T:Json type null|bool|num|str|Json[]|{[k]:Json} - * T:Schema type StringSchema|NumberSchema|IntegerSchema|BooleanSchema|UnknownSchema|ArraySchema|ObjectSchema|RecordSchema|EnumSchema|OptionalSchema + * T:Schema type StringSchema|NumberSchema|IntegerSchema|BooleanSchema|NullSchema|UnknownSchema|ArraySchema|ObjectSchema|RecordSchema|EnumSchema|OptionalSchema * T:InferSchema type TS inference from schema descriptor to runtime type * T:AgentInputValue type input accepting raw values or PromptIntent/PromptBuilder at any nesting level * T:Simplify type flattens intersection types for display @@ -26,7 +26,7 @@ * T:LaunchOptions type options for launchRigProgram (server,token,headers,cwd,args) * T:LauncherIo type {stdin,stdout,stderr} override for launcher subprocess * T:JsonSchemaObject type {[key:string]:unknown} plain JSON Schema object - * s.string/number/integer/boolean SchemaHelperFactory primitives; call as value or fn(desc) + * s.string/number/integer/boolean/null SchemaHelperFactory primitives; call as value or fn(desc) * s.array(items,desc?) ArraySchema * s.object(props,desc?) ObjectSchema; s.optional(inner) marks field optional * s.record(valSchema,desc?) RecordSchema keyed by string @@ -68,6 +68,7 @@ export type StringSchema = { type: "string"; description?: string }; export type NumberSchema = { type: "number"; description?: string }; export type IntegerSchema = { type: "integer"; description?: string }; export type BooleanSchema = { type: "boolean"; description?: string }; +export type NullSchema = { type: "null"; description?: string }; export type UnknownSchema = { description?: string }; export type ArraySchema = { type: "array"; items: Item; description?: string }; export type ObjectSchema = Record> = { @@ -87,6 +88,7 @@ export type Schema = | NumberSchema | IntegerSchema | BooleanSchema + | NullSchema | UnknownSchema | ArraySchema | ObjectSchema @@ -126,7 +128,7 @@ function isOptionalSchema(schema: Schema): schema is OptionalSchema { return OPTIONAL_SYMBOL in schema; } -function createTypedPrimitiveSchema(type: T["type"]): SchemaHelperFactory { +function createTypedPrimitiveSchema(type: T["type"]): SchemaHelperFactory { const base = markAsSchema({ type } as T); const factory = Object.assign( markAsSchema(((description?: string) => (description === undefined ? base : markAsSchema({ type, description } as T))) as SchemaHelperFactory), @@ -177,6 +179,7 @@ export type InferSchema = T extends { type: "number" } ? number : T extends { type: "integer" } ? number : T extends { type: "boolean" } ? boolean : + T extends { type: "null" } ? null : T extends { enum: infer Values extends readonly unknown[] } ? Values[number] : T extends { type: "array"; items: infer Item } ? InferSchema[] : T extends { type: "object"; properties: infer Fields extends Record } ? Simplify< @@ -195,6 +198,8 @@ export const s = { integer: createTypedPrimitiveSchema("integer"), /** Schema for a `boolean` value. Call as `s.boolean` or `s.boolean("description")`. */ boolean: createTypedPrimitiveSchema("boolean"), + /** Schema for the JSON `null` literal. Call as `s.null` or `s.null("description")`. */ + null: createTypedPrimitiveSchema("null"), /** Schema for an unconstrained JSON value. Serializes to an empty schema object. */ unknown: createUnknownSchema(), /** Schema for a homogeneous array. `s.array(s.string)` → `string[]`. */ @@ -1392,6 +1397,7 @@ function validateSchema(value: unknown, schema: Schema, path: string, optional: if (schema.type === "number") return typeof value === "number" ? ok() : bad(path, "number", value); if (schema.type === "integer") return (typeof value === "number" && Number.isInteger(value)) ? ok() : bad(path, "integer", value); if (schema.type === "boolean") return typeof value === "boolean" ? ok() : bad(path, "boolean", value); + if (schema.type === "null") return value === null ? ok() : bad(path, "null", value); } return ok(); } diff --git a/src/rig.test.ts b/src/rig.test.ts index 4030a8b..f147cce 100644 --- a/src/rig.test.ts +++ b/src/rig.test.ts @@ -991,6 +991,7 @@ describe("toJsonSchema", () => { expect(toJsonSchema(s.number)).toEqual({ type: "number" }); expect(toJsonSchema(s.integer)).toEqual({ type: "integer" }); expect(toJsonSchema(s.boolean)).toEqual({ type: "boolean" }); + expect(toJsonSchema(s.null)).toEqual({ type: "null" }); expect(toJsonSchema(s.unknown)).toEqual({}); }); @@ -1086,3 +1087,39 @@ describe("s.integer validation", () => { expect(result.ok).toBe(true); }); }); + +describe("s.null", () => { + it("serializes to {type:'null'}", () => { + expect(toJsonSchema(s.null)).toEqual({ type: "null" }); + expect(toJsonSchema(s.null("cleared"))).toEqual({ type: "null", description: "cleared" }); + }); + + it("accepts null in validation", () => { + const result = analyzeResponse("null", s.null, "test", 1); + expect(result.ok).toBe(true); + }); + + it("rejects non-null values", () => { + const result = analyzeResponse(JSON.stringify("oops"), s.null, "test", 1); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.message).toContain("expected null"); + } + }); + + it("infers null type in TypeScript via agent output", () => { + const a = agent({ output: s.null }); + expect(a.outputSchema).toEqual(s.null); + }); + + it("works as an object field", () => { + const schema = s.object({ value: s.null }); + expect(toJsonSchema(schema)).toEqual({ + type: "object", + properties: { value: { type: "null" } }, + required: ["value"], + }); + const result = analyzeResponse(JSON.stringify({ value: null }), schema, "test", 1); + expect(result.ok).toBe(true); + }); +});