New:Socket for Asana Is Now Available.Learn more
Sign In

@ultimat3/schema

Package Overview
Dependencies
Maintainers
1
Versions
23
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ultimat3/schema - npm Package Compare versions

Comparing version
7.0.0
to
8.0.0
+20
src/char-count.ts
// Single responsibility: how many CHARACTERS a string has. One definition, because the rule that
// rejects a string and the message that describes it must quote the same number — `validators.ts`
// counts here, `describe-value.ts` renders from here, and they disagreed on any astral value.
/**
* Code points, not UTF-16 code units — the unit `json-schema.ts` publishes `minLength`/`maxLength`
* in (JSON Schema defines them over code points), the unit the messages have always said ("chars"),
* and the unit Postgres' `char_length` counts in. `'👍'.length` is 2, so a count in code units
* refused a value the published schema, a human and the database all call one character — and,
* once `describeValue` also read `.length`, `t.string.min(3).safeParse('👍a')` answered
* "at least 3 chars, received a string of 3 characters", an off-by-one an agent cannot debug.
*
* Only a surrogate makes the two counts differ, so the string is walked only when one is present:
* every ASCII value keeps the O(1) read.
*/
const HAS_SURROGATE = /[\uD800-\uDBFF]/;
export function charCount(value: string): number {
return HAS_SURROGATE.test(value) ? [...value].length : value.length;
}
+8
-3

@@ -8,3 +8,3 @@ # @ultimat3/schema — agent notes

| Deps | none (`bun-types` only) |
| Errors | `SchemaError` mirrors `UltimateError` field-for-field; keep `Symbol.for('ultimate.error')` |
| Errors | `SchemaError` mirrors `UltimateError` field-for-field **and message-for-message** (`code: title — cause`); keep `Symbol.for('ultimate.error')` |
| New validator | add to `validators.ts` **and** `TNamespace` **and** `t.ts` **and** `json-schema.ts` |

@@ -18,3 +18,7 @@ | IR | every schema carries `.node: SchemaNode`; generators read that, never the closure |

Module order (no cycles):
`describe-value → node → builder → money-value → validators → discriminated-union → provider → t`.
`char-count → describe-value → node → builder → money-value → validators → discriminated-union →
provider → t`. `char-count.ts` is imported by BOTH `validators.ts` (which rejects on length) and
`describe-value.ts` (which renders the length in the same message), because they disagreed: the
rule counted code points and the message counted UTF-16 units, so `t.string.min(3)` refused `'👍a'`
with "at least 3 chars, received a string of 3 characters".
`standard.ts` and `errors.ts` depend on nothing but each other. `iso-date.ts` imports nothing and

@@ -29,3 +33,4 @@ is imported by `validators.ts` and `coerce.ts` — the two doors a `t.date` string comes through, so

it. Every rejected value goes through `describeValue`, which reports length and type and nothing
else. No dev flag re-enables the echo — one misconfigured environment is the same breach, and a dev
else, in CHARACTERS (`char-count.ts`) — the unit the rule that rejected it counts in. No dev flag
re-enables the echo — one misconfigured environment is the same breach, and a dev
overlay already holds the raw body. `describe-value.test.ts` is the enforcement.

@@ -32,0 +37,0 @@

{
"name": "@ultimat3/schema",
"version": "7.0.0",
"version": "8.0.0",
"description": "Ultimate's validation seam: Standard Schema interface, the t namespace, JSON Schema output",

@@ -5,0 +5,0 @@ "license": "MIT",

@@ -6,2 +6,4 @@ // Single responsibility: render a REJECTED value as its shape, and the `expected X, received Y`

import { charCount } from './char-count';
/**

@@ -29,6 +31,7 @@ * The shape of a rejected value — **never its content**.

case 'string':
// Code units, not code points: the length checks in `validators.ts` use `.length` too, so a
// message quoting a different count than the rule that rejected it would send an agent
// chasing an off-by-one that is not there.
return countOf(value.length, 'string', 'character');
// `charCount`, never `.length`: the message has to quote the same unit as the rule that
// rejected the value, and `validators.ts` counts code points. Reading `.length` here made
// `t.string.min(3).safeParse('👍a')` say "at least 3 chars, received a string of
// 3 characters" — an off-by-one nobody can debug because it is not in the value.
return countOf(charCount(value), 'string', 'character');
case 'number':

@@ -35,0 +38,0 @@ return describeNumber(value);

@@ -104,3 +104,10 @@ // Single responsibility: this package's error codes. `@ultimat3/schema` is tier 0 and may not

const title = singleLine(TITLES[init.code] ?? humanize(init.code));
super(`${code}: ${title}`, { cause: singleLine(init.cause) });
const cause = singleLine(init.cause);
// The cause is in `message` for the reason `UltimateError`'s constructor gives: `message` is
// the ONLY field a runtime prints when an error escapes uncaught — a worker log, a CI
// transcript, a stack trace — and `code: title` alone names which rule fired but not which
// field, row or value. `format()` still renders the canonical 3 lines from the fields, so the
// two cannot disagree. Kept identical to core's on purpose; both are tier 0 and neither may
// import the other.
super(`${code}: ${title} — ${cause}`, { cause });
this.code = code;

@@ -107,0 +114,0 @@ this.title = title;

@@ -20,2 +20,3 @@ // Single responsibility: the builtin, dependency-free validators behind `t`. Small on purpose —

} from './builder';
import { charCount } from './char-count';
import { expected } from './describe-value';

@@ -66,17 +67,2 @@ import { discriminatedUnionSchema } from './discriminated-union';

/**
* Characters, not UTF-16 code units — the unit `json-schema.ts` already promises, since JSON
* Schema defines `minLength`/`maxLength` over code points and the message here has always said
* "chars". `'👍'.length` is 2, so `t.string.max(1)` refused a value the published schema, a human
* and Postgres' `char_length` all count as one.
*
* Only a surrogate makes the two counts differ, so the string is walked only when one is present
* — every ASCII value keeps the O(1) read this replaced.
*/
const HAS_SURROGATE = /[\uD800-\uDBFF]/;
function charCount(value: string): number {
return HAS_SURROGATE.test(value) ? [...value].length : value.length;
}
/**
* Compiled once per schema, not once per validation. `lastIndex` is reset because a `g` or `y`

@@ -83,0 +69,0 @@ * pattern carries it between calls — a cached global RegExp answers `false` for the second