Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion skills/rig/rig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
* 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/null SchemaHelperFactory primitives; call as value or fn(desc)
* s.int alias for s.integer; s.nonEmptyString string with minLength:1; s.url string with format:"uri"; s.path string with format:"path"
* s.int alias for s.integer; s.nonEmptyString string with minLength:1; s.url string with format:"uri"; s.path string with format:"path"; s.date string with format:"date" validated as YYYY-MM-DD
* s.positiveInt integer with minimum:1; s.nonNegativeInt integer with minimum:0; NumberSchema/IntegerSchema support minimum/maximum constraints
* s.array(items,desc?) ArraySchema; use for homogeneous lists, e.g. s.array(s.string)
* s.nonEmptyArray(items,desc?) ArraySchema with minItems:1; validates array has at least one element
Expand Down Expand Up @@ -241,6 +241,8 @@ export const s = {
url: createConstrainedStringSchema({ format: "uri" }),
/** Schema for a file system path string (format: "path"). Use instead of `s.string` when the value is a file or directory path; improves readability and hints to the runtime about path-based context resolution. Call as `s.path` or `s.path("description")`. */
path: createConstrainedStringSchema({ format: "path" }),
/** Schema for an ISO 8601 calendar date string (format: "date", pattern `YYYY-MM-DD`). Use when the value is a date-only value (no time component). Validated at runtime: non-conforming strings fail with a clear error. Call as `s.date` or `s.date("description")`. */
date: createConstrainedStringSchema({ format: "date" }),
/** Schema for a `number` value. Call as `s.number` or `s.number("description")`. */
number: createTypedPrimitiveSchema<NumberSchema>("number"),
/** Schema for an integer value. Serializes to `{"type":"integer"}` in JSON Schema. Call as `s.integer` or `s.integer("description")`. */
Expand Down Expand Up @@ -2059,6 +2061,11 @@ function validateSchema(value: unknown, schema: Schema, path: string, optional:
if (format === "uri") {
try { new URL(value); } catch { return { ok: false, error: `${path}: expected a valid URL, got ${JSON.stringify(value)}` }; }
}
if (format === "date") {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The regex /^\d{4}-\d{2}-\d{2}$/ validates the format but not calendar validity — "2024-99-99" passes silently.

💡 Suggested fix

After the regex check, verify the parsed date round-trips:

if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
  return { ok: false, error: `${path}: expected a date string in YYYY-MM-DD format, got ${JSON.stringify(value)}` };
}
const d = new Date(value);
if (isNaN(d.getTime())) {
  return { ok: false, error: `${path}: expected a valid calendar date in YYYY-MM-DD format, got ${JSON.stringify(value)}` };
}

This catches "2024-13-01" and "2024-02-30" which the regex alone accepts. Consider pairing with a test: rejects an out-of-range date like "2024-13-01".

return { ok: false, error: `${path}: expected a date string in YYYY-MM-DD format, got ${JSON.stringify(value)}` };
}
}
return ok();
}
if (schema.type === "number") {
Expand Down
34 changes: 34 additions & 0 deletions src/rig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1466,6 +1466,40 @@ describe("s.path", () => {
});
});

describe("s.date", () => {
it("serializes to {type:'string', format:'date'}", () => {
expect(toJsonSchema(s.date)).toEqual({ type: "string", format: "date" });
expect(toJsonSchema(s.date("release date"))).toEqual({ type: "string", format: "date", description: "release date" });
});

it("accepts a valid YYYY-MM-DD date string", () => {
const result = analyzeResponse(JSON.stringify("2024-03-15"), s.date, "test", 1);
expect(result.ok).toBe(true);
});

it("rejects a string that is not in YYYY-MM-DD format", () => {
const result = analyzeResponse(JSON.stringify("March 15 2024"), s.date, "test", 1);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.message).toContain("YYYY-MM-DD");
}
});

it("rejects a datetime string with time component", () => {
const result = analyzeResponse(JSON.stringify("2024-03-15T10:00:00Z"), s.date, "test", 1);
expect(result.ok).toBe(false);
});

it("is usable as an object field", () => {
const schema = s.object({ createdAt: s.date });
expect(toJsonSchema(schema)).toEqual({
type: "object",
properties: { createdAt: { type: "string", format: "date" } },
required: ["createdAt"],
});
});
});

describe("s.positiveInt", () => {
it("serializes to {type:'integer', minimum:1}", () => {
expect(toJsonSchema(s.positiveInt)).toEqual({ type: "integer", minimum: 1 });
Expand Down
Loading