@ultimat3/schema
Advanced tools
| // 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 @@ |
+1
-1
| { | ||
| "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); |
+8
-1
@@ -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; |
+1
-15
@@ -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 |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
110765
1.63%20
5.26%2097
0.72%