@ultimat3/schema 16.0.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -50,18 +50,28 @@ first already owns, can never be routed to — so it is wrong for every input, a
50
50
  of the authoring file is the earliest honest place to say so.
51
51
 
52
52
  `SCHEMA_ERROR_CODES` in `errors.ts` is data, not a `registerErrorCodes()` call — this package is
53
- tier 0 and cannot import `@ultimat3/core` to reach it. `@ultimat3/core`'s `schema-error-codes.ts`
54
- carries a duplicate of these titles and registers them unconditionally, so every process gets the
55
- real titles just by importing core. Add a code here **and** update that duplicate in the same
56
- change — `schema-error-codes-pin.test.ts` in `@ultimat3/cli` fails the build if they disagree.
53
+ tier 0 and cannot import `@ultimat3/core` to reach it, and never will. `@ultimat3/core`'s
54
+ `schema-error-codes.ts` READS this object over the declared `core -> schema` edge and registers it
55
+ unconditionally, so every process gets the real titles just by importing core. **Adding a code here
56
+ is the whole edit `As of 2026-08-27`** — the title and its `terminal` retry classification are both
57
+ derived from this set, where they used to be a hand-kept duplicate in core plus a hand-written
58
+ retry list a fifth code would have been silently missing from.
57
59
 
58
60
  `ERROR_DOCS_URL` in `errors.ts` is the third deliberate tier-0 duplicate, beside `singleLine` and
59
61
  `ULTIMATE_ERROR_BRAND` — one URL, spelled out, because `SchemaError` cannot import
60
62
  `@ultimat3/core`'s constant. There is no per-code URL anywhere in the framework: codes live in
61
63
  `wiki/Error-Codes.md` as table ROWS and a table row has no anchor, so
62
64
  `https://ultimate.dev/errors/<code>` was a 404 on every error and was deleted `As of 2026-08-23`. Change it
63
- here and in `packages/core/src/error-codes.ts` in the same edit. **There is no pin test yet** —
64
- one belongs in `@ultimat3/cli` beside `single-line-pin.test.ts`, which may legally import both.
65
+ here and in `packages/core/src/error-codes.ts` in the same edit. **It is pinned `As of 2026-08-27`**
66
+ — `packages/core/src/single-line-pin.test.ts` asserts a `SchemaError`'s `docs` equals an
67
+ `UltimateError`'s, which was the one copy with no mechanical check at all and the one that decides
68
+ where every schema refusal sends its reader.
69
+
70
+ **These three are the whole of the duplication that remains, and the edge does not reach them.**
71
+ `core -> schema` was declared 2026-08-27 and collapsed five copies on the CORE side; `singleLine`,
72
+ `ERROR_DOCS_URL` and `ULTIMATE_ERROR_BRAND` go the other way, and `schema -> core` stays forbidden
73
+ on its merits — `t` is in every bundle graph an app has, so a dependency here is a dependency
74
+ everywhere.
65
75
 
66
76
  `MoneyValue` in `money-value.ts` — its own file, because it is the only builtin whose *shape* other
67
77
  packages alias — is the framework's **one** declaration of a money value. Tier 0 is
@@ -118,6 +128,14 @@ Gotchas:
118
128
  PROTOTYPE, so a field named after one was unsatisfiable for every input and its `.default()`
119
129
  never fired; a `{}` output let a declared `__proto__` field write through the setter. Never
120
130
  reintroduce either half, and never assume a parsed object has `Object.prototype` on it.
131
+ - **Every map keyed by a DECLARED FIELD NAME is built on `Object.create(null)`** — `objectSchema`'s
132
+ `properties`, `json-schema.ts`'s converted `properties`, and `pick`/`omit`'s rebuilt shape. That
133
+ last pair is the one with teeth: `next['__proto__'] = member` on a `{}` literal hits the setter,
134
+ so `pick('__proto__')` answered a schema with **zero** properties and dropped the one field the
135
+ caller asked to keep from validation, from the IR and from `parse()`'s output. `extend` needs
136
+ nothing: object spread defines own properties and never invokes a setter. A field named
137
+ `__proto__` is declarable — `t.object({ ['__proto__']: t.string })`, a COMPUTED key, which is an
138
+ ordinary own property where the literal `{ __proto__: … }` form is the setter.
121
139
  - `t.date` refuses a clock time with no offset and no `Z` (`iso-date.ts`): a zone-less string is a
122
140
  different instant per host `TZ`, and `coerceQuery` puts it one query parameter from the wire.
123
141
  A date-only string carries no clock time and is UTC by spec, so it still parses.
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@ultimat3/schema",
3
- "version": "16.0.0",
3
+ "version": "18.0.0",
4
4
  "description": "Ultimate's validation seam: Standard Schema interface, the t namespace, JSON Schema output",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "sideEffects": false,
7
8
  "repository": {
8
9
  "type": "git",
9
10
  "url": "git+https://github.com/developerz-ai/ultimate.git",
@@ -24,7 +25,7 @@
24
25
  "LICENSE"
25
26
  ],
26
27
  "engines": {
27
- "bun": ">=1.3.0"
28
+ "bun": ">=1.4.0"
28
29
  },
29
30
  "scripts": {
30
31
  "typecheck": "tsc --noEmit -p tsconfig.json",
package/src/errors.ts CHANGED
@@ -16,17 +16,17 @@ import { formatIssues } from './standard';
16
16
  */
17
17
  // biome-ignore lint/suspicious/noControlCharactersInRegex: escaping them is the point.
18
18
  const CONTROL = /[\u0000-\u001f\u007f\u2028\u2029]/g;
19
- const ESCAPES: Readonly<Record<string, string>> = {
19
+ // Null-prototype, so `ESCAPES[char]` cannot answer an `Object.prototype` member no matter what
20
+ // `CONTROL` matched. The domain argument — a key is exactly one control character, and no
21
+ // prototype member has a single-character name — was true and is no longer load-bearing; a table
22
+ // that cannot reach the prototype needs no argument, and `bun run proto-index` stops reporting it.
23
+ const ESCAPES: Readonly<Record<string, string>> = Object.assign(Object.create(null), {
20
24
  '\n': String.raw`\n`,
21
25
  '\r': String.raw`\r`,
22
26
  '\t': String.raw`\t`,
23
27
  '\b': String.raw`\b`,
24
28
  '\f': String.raw`\f`,
25
- };
26
- // `ESCAPES[char]` is a computed read on a plain object and is safe by DOMAIN, not by luck: the
27
- // key is whatever `CONTROL` matched, so it is exactly one control character — and no
28
- // `Object.prototype` member has a single-character name. Every other computed read in this file
29
- // goes through `Object.hasOwn`.
29
+ });
30
30
  const singleLine = (text: string): string =>
31
31
  text.replace(
32
32
  CONTROL,
package/src/index.ts CHANGED
@@ -91,6 +91,7 @@ export {
91
91
  } from './standard';
92
92
  export type { Infer } from './t';
93
93
  export { t } from './t';
94
+ export { isIanaZoneName } from './time-zone-name';
94
95
  export type {
95
96
  NumberSchema,
96
97
  ObjectSchema,
@@ -231,7 +231,13 @@ function convert(node: SchemaNode): JsonSchema {
231
231
  additionalProperties: false,
232
232
  });
233
233
  case 'object': {
234
- const properties: Record<string, JsonSchema> = {};
234
+ // `Object.create(null)`, the same rule `validators.ts` applies to the IR this reads: a
235
+ // `__proto__` property assigned into a `{}` literal reaches the setter and is published
236
+ // nowhere, so the document would omit a field the server enforces.
237
+ const properties: Record<string, JsonSchema> = Object.create(null) as Record<
238
+ string,
239
+ JsonSchema
240
+ >;
235
241
  for (const [key, child] of Object.entries(node.properties ?? {})) {
236
242
  properties[key] = convert(child);
237
243
  }
@@ -1,8 +1,12 @@
1
- // Single responsibility: is a string an IANA zone NAME? Tier 0's second statement of the one rule
1
+ // Single responsibility: is a string an IANA zone NAME? TIER 0'S ONE STATEMENT of the rule
2
2
  // `@ultimat3/time` enforces everywhere above it — a zone is `Area/Location`, and `UTC` is the one
3
- // exception. It is stated a third time because `schema` is tier 0 and may import neither
4
- // `@ultimat3/time` nor `@ultimat3/core`; `packages/time/src/zone-canonical.ts` is where the rule
5
- // and its reasoning are written down.
3
+ // exception. `@ultimat3/core` imported its own copy of this predicate until 2026-08-27 and now
4
+ // re-exports this one, over the declared `core -> schema` edge.
5
+ //
6
+ // `@ultimat3/time`'s `canonicalTimeZone` is NOT a fourth copy and is deliberately left alone: it
7
+ // answers a different question (the canonical SPELLING, memoised over ~445 listed zones plus a
8
+ // probe cache) and shares only the leading-sign rule below. Collapsing it into this predicate
9
+ // would trade a cache a request header can hit for one it cannot.
6
10
 
7
11
  /**
8
12
  * A LEADING sign is a fixed offset, which carries no DST rules. `Etc/GMT+2` keeps its `+`.
@@ -22,10 +26,9 @@ const NUMERIC_OFFSET = /^[+-]/;
22
26
  * file enforces is that same rule at the EDGE: an identifier is `Area/Location`, `UTC` is the one
23
27
  * legal exception, and a leading sign is an offset rather than a name.
24
28
  *
25
- * `@ultimat3/core`'s `isIanaZoneName` is the same predicate for `app.config.ts`. Nothing below
26
- * tier 5 can compare the two, so `timezone-validator-pin.test.ts` in `@ultimat3/cli` is the
27
- * mechanical half — the arrangement `schema-error-codes-pin.test.ts` already uses for tier 0's
28
- * other deliberate duplicate.
29
+ * `@ultimat3/core` re-exports THIS function for `app.config.ts`, so the edge validator and the
30
+ * config validator are one predicate. They were two, held equal by a 123-line pin test in
31
+ * `@ultimat3/cli` that no rule required to exist.
29
32
  *
30
33
  * Of the three copies this is the one that guards CALLER input: `t.timezone` is a request body
31
34
  * field and a query parameter through `coerceQuery`, so the bare `new Intl.DateTimeFormat(…)`
package/src/validators.ts CHANGED
@@ -155,7 +155,11 @@ function makeNumberSchema(node: SchemaNode): NumberSchema {
155
155
  }
156
156
 
157
157
  export function objectSchema<S extends Shape>(shape: S): ObjectSchema<S> {
158
- const properties: Record<string, SchemaNode> = {};
158
+ // `Object.create(null)` for the same reason the parse output below uses one, on the other side
159
+ // of the same rule: a DECLARED `__proto__` field assigned into a `{}` literal hits the prototype
160
+ // setter and never becomes a key, so the field vanished from the IR `json-schema.ts` publishes
161
+ // and `coerce.ts` walks while the `checks` list beside it went on validating it.
162
+ const properties: Record<string, SchemaNode> = Object.create(null) as Record<string, SchemaNode>;
159
163
  const checks: [string, Check<unknown>][] = [];
160
164
  for (const [key, member] of Object.entries(shape)) {
161
165
  properties[key] = member.node;
@@ -195,14 +199,20 @@ export function objectSchema<S extends Shape>(shape: S): ObjectSchema<S> {
195
199
  ...base,
196
200
  shape,
197
201
  extend: (extra) => objectSchema({ ...shape, ...extra } as Simplified<S & typeof extra>),
202
+ // `Object.create(null)` in both, for the reason the IR above states and with a worse outcome
203
+ // here: `next['__proto__'] = member` on a `{}` literal hits the prototype SETTER, so the field
204
+ // is not mis-published, it is GONE — `pick('__proto__')` answered a schema with no properties
205
+ // at all, dropping the one field the caller asked to keep from validation, from the IR and
206
+ // from `parse()`'s output. `extend` needs none of this: object spread defines own properties
207
+ // and never invokes a setter.
198
208
  pick: (...keys) => {
199
- const next: Record<string, AnySchema> = {};
209
+ const next: Record<string, AnySchema> = Object.create(null) as Record<string, AnySchema>;
200
210
  for (const key of keys) next[key] = shape[key] as AnySchema;
201
211
  return objectSchema(next) as ObjectSchema<Pick<S, (typeof keys)[number]>>;
202
212
  },
203
213
  omit: (...keys) => {
204
214
  const drop = new Set<string>(keys);
205
- const next: Record<string, AnySchema> = {};
215
+ const next: Record<string, AnySchema> = Object.create(null) as Record<string, AnySchema>;
206
216
  for (const [key, member] of Object.entries(shape)) {
207
217
  if (!drop.has(key)) next[key] = member;
208
218
  }