diff --git a/package.json b/package.json index 7a355b2..063c0a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tempo-monorepo", - "version": "2.5.0", + "version": "2.6.0", "private": true, "description": "Magma Computing Monorepo", "repository": { diff --git a/packages/library/package.json b/packages/library/package.json index 6ea2874..fe1ee7d 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/library", - "version": "2.5.0", + "version": "2.6.0", "description": "Shared utility library for Tempo", "author": "Magma Computing Solutions", "license": "MIT", diff --git a/packages/library/src/common/temporal.library.ts b/packages/library/src/common/temporal.library.ts index 507fdb1..5a7acdd 100644 --- a/packages/library/src/common/temporal.library.ts +++ b/packages/library/src/common/temporal.library.ts @@ -118,3 +118,23 @@ export function getTemporalIds(tz: any, cal: any): [string, string] { return [tzId || 'UTC', calId || 'iso8601']; } + +/** + * ## normalizeUtcOffset + * Convert informal UTC offset strings into the `±HH:MM` format required by Temporal. + * Accepts forms like `'UTC+8'`, `'UTC-9'`, `'UTC+08:00'`, `'UTC-05:30'`. + * Returns the input unchanged if it does not match the UTC± pattern. + */ +export function normalizeUtcOffset(zone: string): string { + const match = /^UTC([+-])(\d{1,2})(?::(\d{2}))?$/i.exec(zone); + if (!match) return zone; + + const [, sign, hours, minutes] = match; + const h = Number(hours); + const m = Number(minutes ?? '0'); + + // Temporal-valid range: -12:00 .. +14:00, minutes 0..59 + if (h > 14 || m > 59 || (sign === '+' && h === 14 && m !== 0) || (sign === '-' && h > 12)) return zone; + + return `${sign}${hours.padStart(2, '0')}:${minutes ?? '00'}`; +} diff --git a/packages/tempo/doc/architecture.md b/packages/tempo/doc/architecture.md index 20f2550..2f3ebdc 100644 --- a/packages/tempo/doc/architecture.md +++ b/packages/tempo/doc/architecture.md @@ -66,22 +66,24 @@ Together, these ensure that `new Tempo()` maintains an $O(1)$ constructor execut --- -## 🔁 Iteration & Enumerability (The Shadowing Chain) +## 🔁 Iteration & Enumerability (Delegator Proxies) -When using prototype shadowing, the JavaScript behavior for property inspection changes significantly. This is a trade-off for the performance gains. +A delegator Proxy is a Proxy wrapper whose traps forward operations to an internal target/handler pair; unlike a standard Proxy (which typically mediates access directly against one wrapped target object), delegation is explicit and routed through that intermediate forwarding layer. In Tempo, "delegator Proxy" and "Generic Lazy Delegator Proxy" refer to the same public delegation mechanism, while lazy shadowing for `Tempo.#term`/`Tempo.#fmt` is a separate private-field initialization mechanism. These mechanisms coexist: the `instance.term` / `instance.fmt` public API uses the delegator Proxy path, and `Tempo.#term` / `Tempo.#fmt` private fields are initialized once via lazy shadowing. -### ⚠️ The `Object.keys()` Warning -`Object.keys(instance.fmt)` only returns the **enumerable own properties** of the current link in the shadowing chain. -- **Initially**: Returns `[]` (all evaluated getters are non-enumerable on the base). -- **After 1st Access** (e.g., `.date`): Returns `['date']`. -- **After 2nd Access** (e.g., `.time`): Returns `['time']`. The `.date` property is now located on the **immediate prototype** of the current object. +### ✅ `Object.keys()` Behavior +`Object.keys(instance.fmt)` and `Object.keys(instance.term)` return the enumerable own keys currently registered on each delegator target. -### 🛡️ The Flattening Iterator -Tempo implements a **Flattening Iterator** via `[Symbol.iterator]` which enables iterable consumers like `for...of`, array spread (`[...instance]`), and `Object.fromEntries(instance)` to traverse the shadowing chain (using `Object.getPrototypeOf`) and collect evaluated property entries. +- **Proxy discovery (definition)**: Proxy discovery is the proxy-handler phase that enumerates available target keys and installs enumerable lazy getter properties on the proxy target without reading their values. +- **Triggered by enumeration APIs**: Discovery runs when enumeration APIs execute, including `Object.keys(instance.fmt)`, `for...in`, and `Reflect.ownKeys(...)` on the delegator proxy. +- **Timing**: Discovery happens at enumeration time (before any property `get`), so key visibility is established before value resolution. +- **Before direct access**: Keys can already be visible and probeable. +- **Relation to [Section 1](#1-lazy-evaluation-shadowing)**: Discovery only registers getters; actual value computation and memoization happen later, when a getter is first invoked (for example, `instance.fmt.someKey`). +- **After access**: Getter access memoizes values on the same target object; keys remain stable and do not "move" across prototype links. -- **`[Symbol.iterator]`**: Traverses the shadowing chain to provide a flattened view of all computed state. -- **⚠️ Important**: `for...in` and object spread (`{...instance}`) **do not** use the iterator; instead, they rely on enumerable own/inherited properties and are not supported by the flattening logic. -- **`Tempo.formats` & `Tempo.terms`**: These static getters continue to provide a registry-wide view of **available** keys across the entire system, regardless of their evaluation state. +### 🛡️ Iteration Notes +- **`Object.keys` / `for...in` / object spread**: Operate on enumerable keys exposed by the delegator target after discovery. +- **`[Symbol.iterator]`**: Still provides explicit iterator semantics where implemented. +- **`Tempo.formats` & `Tempo.terms`**: These static getters continue to provide a registry-wide view of available keys across the system, independent of per-instance memoization state. --- diff --git a/packages/tempo/doc/migration-guide.md b/packages/tempo/doc/migration-guide.md index 79f6335..4ee99a0 100644 --- a/packages/tempo/doc/migration-guide.md +++ b/packages/tempo/doc/migration-guide.md @@ -86,5 +86,13 @@ As Tempo grows, it has become much more efficient for our developers to logicall 2. Replace any older internal paths with the current package subpath entries (for example, `@magmacomputing/tempo/duration`, `@magmacomputing/tempo/mutate`, `@magmacomputing/tempo/parse`, and `@magmacomputing/tempo/format`). 3. Do not pin imports in your code directly to internal folder layouts in `dist/`, since those paths may change as modules are reorganized. Instead rely wholly on your import maps. +## 🔁 Migrating from version 2.6.0 + +Season term scope output has been simplified. + +**Action Required**: +1. If you previously relied on the Chinese-specific object attached to `term.season` scope output, remove that dependency. +2. Resolve Chinese season context by creating a dedicated `Tempo` instance with the appropriate Chinese `timeZone` for the interpretation you need. + ## 🧪 Testing and Stability v2.x has been hardened with a 100% pass rate on our regression suite. If you were relying on undocumented "quirks" or bugs in v1.x parsing, you may find that v2.x is more strict and deterministic. diff --git a/packages/tempo/doc/releases/v2.x.md b/packages/tempo/doc/releases/v2.x.md index cdee182..b66fd12 100644 --- a/packages/tempo/doc/releases/v2.x.md +++ b/packages/tempo/doc/releases/v2.x.md @@ -1,4 +1,27 @@ # 📜 Version 2.x History + +## [v2.6.0] - 2026-04-25 +### ⚠️ Migration Notes + + +- New Features + +Added normalizeUtcOffset utility for transforming informal UTC-offset strings to standard format. +Added layoutOrder option to customize parsing element precedence. +- Breaking Changes +- **Season Scope Simplification**: Removed the Chinese-specific object previously attached to all `term.season` scope's output. For Chinese season interpretation, create a dedicated `Tempo` instance with `{timeZone: 'Asia/Shanghai'}` or `{timeZone: '+08:00'}`. Note that Chinese zodiac will still be supported on `term.zodiac.CN` as before. + + +- Bug Fixes + +Fixed layout pattern resolution ordering to respect intended sequence. +Enhanced timezone normalization for UTC offset handling. +- Documentation + +Updated architecture documentation and configuration guidance. +Clarified plugin/module callback parameter ordering in examples. +Added v2.6.0 migration guide for season changes. + ## [v2.5.0] -2026-04-25 ### New Features diff --git a/packages/tempo/doc/sandbox-factory.md b/packages/tempo/doc/sandbox-factory.md index 81b1e7b..967d49e 100644 --- a/packages/tempo/doc/sandbox-factory.md +++ b/packages/tempo/doc/sandbox-factory.md @@ -9,7 +9,7 @@ Historically, `Tempo.init()` modified the global library state. This meant that: 3. Testing multiple configurations required careful cleanup between tests. ## The Solution -`Tempo.create()` returns a **derived class** with its own isolated configuration, registry, and plugin state. Each sandbox inherits from the caller, but runs with independent internal state. +`Tempo.create()` returns a **derived sandboxed class** with its own isolated configuration, registry, and plugin state. Each sandbox inherits from the caller, but runs with independent internal state. ### Example: Creating a Sandbox ```typescript diff --git a/packages/tempo/doc/tempo.config.md b/packages/tempo/doc/tempo.config.md index bb127bb..2b8b8a9 100644 --- a/packages/tempo/doc/tempo.config.md +++ b/packages/tempo/doc/tempo.config.md @@ -43,22 +43,54 @@ Tempo.init({ store: 'userSettings' }); ## 2. Global Discovery -To facilitate configuration in micro-frontend architectures or when using a `