@ultimat3/entity 5.0.0 → 6.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
@@ -4,7 +4,7 @@ Columns + invariants; the row type is derived from the columns. Tier 2.
4
4
 
5
5
  ## Boundary
6
6
 
7
- - May import `@ultimat3/core`, `@ultimat3/schema` and `@ultimat3/db`. Nothing else — `http`,
7
+ - May import `@ultimat3/core`, `@ultimat3/schema`, `@ultimat3/db` and `@ultimat3/time` (tier 1 — `columns.ts` and `columns-data.ts` read its `isValidTimeZone`, so a zone this package accepts is one `@ultimat3/time` can do arithmetic in). Nothing else — `http`,
8
8
  `policy` and `auth` are the same tier.
9
9
  - `db` is tier 1 (it imports only `core`), which is what lets the Postgres driver live **here**
10
10
  rather than in a tier-3 package: `Driver` and its production implementation stay in one place.
@@ -564,7 +564,7 @@ Columns + invariants; the row type is derived from the columns. Tier 2.
564
564
  hole open on exactly the unauthenticated path. **Outside every request context there is no actor
565
565
  to derive from** — a script, a seed, a test harness — so the caller names the tenant itself and
566
566
  `X_TENANCY_UNSCOPED` still refuses a plan that names none. There is no build-time tenancy step in
567
- `x verify` (its 17 steps check none) and the old comment in `tenancy.ts` claiming one was wrong:
567
+ `x verify` (its 19 steps check none) and the old comment in `tenancy.ts` claiming one was wrong:
568
568
  the tenant is a request-time value, so the seam is the enforcement.
569
569
  - **`crossTenant(reason, fn)` (`cross-tenant.ts`) is the ONE way to read across tenants**, for the
570
570
  three cases that have no single one: an admin surface over every org, background reconciliation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/entity",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "A table + its domain type + invariants the database also enforces",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,9 +31,9 @@
31
31
  "test": "bun test"
32
32
  },
33
33
  "dependencies": {
34
- "@ultimat3/core": "5.0.0",
35
- "@ultimat3/db": "5.0.0",
36
- "@ultimat3/schema": "5.0.0",
37
- "@ultimat3/time": "5.0.0"
34
+ "@ultimat3/core": "6.0.0",
35
+ "@ultimat3/db": "6.0.0",
36
+ "@ultimat3/schema": "6.0.0",
37
+ "@ultimat3/time": "6.0.0"
38
38
  }
39
39
  }
package/src/columns.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  isMoneyScale,
11
11
  MAX_MONEY_SCALE,
12
12
  } from '@ultimat3/schema';
13
+ import { isValidTimeZone } from '@ultimat3/time';
13
14
  import {
14
15
  assertColumnName,
15
16
  BARE,
@@ -176,22 +177,22 @@ export const url = (): Column<string> =>
176
177
  { check: (name) => `${name} ~ '^https?://'` },
177
178
  );
178
179
 
179
- const isIanaZone = (value: string): boolean => {
180
- try {
181
- new Intl.DateTimeFormat('en', { timeZone: value }).format(0);
182
- return true;
183
- } catch {
184
- return false;
185
- }
186
- };
187
-
188
180
  /**
189
- * IANA identifiers, checked against `Intl` when the column is declared — a typo or a UTC offset
190
- * is a startup error rather than a row nobody can format. An offset is wrong twice a year.
181
+ * IANA identifiers, checked when the column is declared — a typo, an abbreviation or a UTC offset
182
+ * is a startup error rather than a row nobody can format.
183
+ *
184
+ * The check is `@ultimat3/time`'s own `isValidTimeZone`, imported and never restated: `time` is
185
+ * tier 1 and this package is tier 2, so the one rule is a downward import away. A local
186
+ * `new Intl.DateTimeFormat(…)` probe was the second answer to that question, and the two stopped
187
+ * agreeing — `Intl` answers "can I format this", never "is this IANA", and ICU 78 (Bun 1.4)
188
+ * resolves `CET`, `EST`, `GMT`, `Zulu` and `Japan`, so `tz(['CET'])` declared a column every
189
+ * `format` call above it then refused with `X_TIMEZONE_INVALID`. A leading-sign offset (`+01:00`)
190
+ * resolved under every runtime this framework has shipped on, and carries no DST rule at all, so
191
+ * it is wrong twice a year on top of being unformattable.
191
192
  */
192
193
  export const tz = <const Z extends readonly string[]>(zones: Z): Column<Z[number]> => {
193
194
  for (const zone of zones) {
194
- if (!isIanaZone(zone)) reject('iana-tz', `${zone} is not an IANA time zone`);
195
+ if (!isValidTimeZone(zone)) reject('iana-tz', `${zone} is not an IANA time zone`);
195
196
  }
196
197
  const allowed = new Set<string>(zones);
197
198
  return column<Z[number]>(