Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tempo-monorepo",
"version": "2.5.0",
"version": "2.6.0",
"private": true,
"description": "Magma Computing Monorepo",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/library/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
20 changes: 20 additions & 0 deletions packages/library/src/common/temporal.library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'}`;
}
26 changes: 14 additions & 12 deletions packages/tempo/doc/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Comment thread
magmacomputing marked this conversation as resolved.
### ⚠️ 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.

---

Expand Down
8 changes: 8 additions & 0 deletions packages/tempo/doc/migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
23 changes: 23 additions & 0 deletions packages/tempo/doc/releases/v2.x.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion packages/tempo/doc/sandbox-factory.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 40 additions & 8 deletions packages/tempo/doc/tempo.config.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,54 @@ Tempo.init({ store: 'userSettings' });

## 2. Global Discovery

To facilitate configuration in micro-frontend architectures or when using a `<script>` tag, Tempo automatically "discovers" a global configuration object before any instances are created.
To facilitate configuration in micro-frontend architectures or script-first bootstraps, `Tempo` can discover a Discovery object from `globalThis` during `Tempo.init()`.

### Using a Static Method (Recommended)
This is the most secure and ergonomic method to provide configuration, and is compatible with ESM hoisting.
The intended flow is:
1. Write a Discovery object into `globalThis` under the configured discovery symbol key.
2. Import a module containing `Tempo`.
3. `Tempo` class static initialization runs `Tempo.init()`.
4. `Tempo.init()` reads the global discovery slot and merges it.

By default, the key is `Symbol.for('$Tempo')`.

### Pre-Bootstrap Discovery (globalThis)

```javascript
// Must run before the first Tempo module is evaluated
globalThis[Symbol.for('$Tempo')] = {
options: { timeZone: 'Europe/Paris' },
timeZones: { MYTZ: 'Asia/Dubai' },
formats: { myFormat: '{dd}!!{mm}!!{yyyy}' },
terms: [myCustomTermPlugin]
};

// Load Tempo after the discovery object is in place
const { Tempo } = await import('@magmacomputing/tempo');
```

::: info
With static ESM imports, import evaluation happens before module body execution. If you need discovery to apply on first load, assign `globalThis` in an earlier script/module, or use dynamic `import()` as shown above.
:::

### Explicit Runtime Registration (Not Global Discovery)
Using `Tempo.extend(...)` is explicit registration after `Tempo` is loaded. It is ergonomic and strongly recommended for normal application code, but it is a different mechanism from pre-bootstrap global discovery.

```javascript
import { Tempo } from '@magmacomputing/tempo';

Tempo.extend({
options: { timeZone: 'Europe/Paris' },
timeZones: { 'MYTZ': 'Asia/Dubai' },
formats: { 'myFormat': '{dd}!!{mm}!!{yyyy}' },
terms: [ myCustomTermPlugin ]
});
options: { timeZone: 'Europe/Paris' },
timeZones: { MYTZ: 'Asia/Dubai' },
formats: { myFormat: '{dd}!!{mm}!!{yyyy}' },
terms: [myCustomTermPlugin]
});
```

### Security and Ergonomics Notes
- Global Discovery is convenient for host-controlled bootstraps and cross-bundle handoff.
- `Tempo.extend(...)` is usually safer in app code because configuration is explicit, local, and easier to trace.
- Use Global Discovery when you must configure `Tempo` before the first `Tempo` import executes.

### Discovery Contract
Tempo looks for the following structure:

Expand Down
2 changes: 1 addition & 1 deletion packages/tempo/doc/tempo.modularity.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ You can create your own modules to extend Tempo's internal engine or its public
```typescript
import { defineModule } from '@magmacomputing/tempo/plugin';

export const MyModule = defineModule((options, TempoClass) => {
export const MyModule = defineModule((TempoClass, options) => {
// Add instance methods
TempoClass.prototype.sayHello = function() { return 'Hello!'; };

Expand Down
16 changes: 8 additions & 8 deletions packages/tempo/doc/tempo.plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ The most efficient way to author a plugin is using the `definePlugin` factory. T
```typescript
import { definePlugin } from '@magmacomputing/tempo/plugin';

export const MyPlugin = definePlugin((options, TempoClass, factory) => {
export const MyPlugin = definePlugin((TempoClass, options, factory) => {
/**
* options: The global configuration object
* TempoClass: The internal Tempo class (for static methods)
* factory: A helper to create new Tempo instances without 'new'
* TempoClass: The internal Tempo class (for static methods)
* options: The global configuration object
* factory: A helper to create new Tempo instances without 'new'
*/
Comment thread
magmacomputing marked this conversation as resolved.

// 1. Add a static method
Expand All @@ -48,7 +48,7 @@ If you prefer not to use the factory (e.g. for plugin that should *not* self-reg
```typescript
import type { Tempo } from '@magmacomputing/tempo/core';

export const ManualPlugin: Tempo.Plugin = (options, TempoClass, factory) => {
export const ManualPlugin: Tempo.Plugin = (TempoClass, options, factory) => {
// ... implementation ...
};
```
Expand Down Expand Up @@ -197,7 +197,7 @@ class MyPluginInstance implements MyPluginTypes.Descriptor {
Use a `Proxy` in your `definePlugin` factory to handle the callability trap. This allows your plugin to act as a function (the shortcut) and an object (the stateful class) simultaneously.

```typescript
export const MyPlugin = definePlugin((options, TempoClass, factory) => {
export const MyPlugin = definePlugin((TempoClass, options, factory) => {
(TempoClass as any).myTool = function(arg1: any): MyPluginTypes.Instance {
const instance = new MyPluginInstance(arg1);

Expand Down Expand Up @@ -229,7 +229,7 @@ If your plugin requires its own configuration, export a **factory function** tha
import { defineModule } from '@magmacomputing/tempo/plugin';

export const HolidayModule = (pluginOptions = {}) => {
return defineModule((tempoOptions, TempoClass, factory) => {
return defineModule((TempoClass, tempoOptions, factory) => {
// ... use pluginOptions here ...
});
};
Expand All @@ -244,7 +244,7 @@ import { defineModule } from '@magmacomputing/tempo/plugin';
import { PluginA } from './plugin.a.js';
import { PluginB } from './plugin.b.js';

export const MyFeatureModule = defineModule((options, TempoClass) => {
export const MyFeatureModule = defineModule((TempoClass, options) => {
TempoClass.extend([PluginA, PluginB]);
});
```
Expand Down
4 changes: 2 additions & 2 deletions packages/tempo/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/tempo",
"version": "2.5.0",
"version": "2.6.0",
"description": "The Tempo core library",
"author": "Magma Computing Solutions",
"license": "MIT",
Expand Down Expand Up @@ -232,7 +232,7 @@
},
"devDependencies": {
"@js-temporal/polyfill": "^0.5.1",
"@magmacomputing/library": "2.5.0",
"@magmacomputing/library": "2.6.0",
"@rollup/plugin-alias": "^6.0.0",
"cross-env": "^7.0.3",
"magic-string": "^0.30.21",
Expand Down
124 changes: 124 additions & 0 deletions packages/tempo/plan/slick-syntax-duration-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Slick Syntax Extension for Duration Keys

## Status
- Proposed for a future release (not scheduled in the current release).
- Intent: evaluate design and rollout without breaking existing term-based Slick behavior.

## Context
Today, Slick Syntax is centered on Term mutation resolution (for example `#term` and `#term.modifier` forms).
Mutation routing for object inputs currently distinguishes:
- Term-style mutations (`#...` or discovered term plugins)
- Standard unit mutations (`year`, `month`, `week`, `day`, etc.)
- Parser-backed set operations (`period`, `event`, `time`, `date`, `dow`, `wkd`)

This creates an opportunity to support human-friendly modifier strings on selected duration/unit keys.

## Goal
Allow selected non-term mutation keys to accept Slick-style strings, starting with the most intuitive use-cases.

Example candidate syntax:
- `t1.set({ week: ">2Mon" })`
`t1.set({ week: ">2Mon" })` - advances to the 2nd following Monday.

## Key Architectural Principle
Keep two concepts distinct:
- Term Slick: range-based/cycle-aware term navigation.
- Unit Slick: field-oriented mutation grammar interpreted per key.

Do not merge these conceptually into one generic resolver until semantics are explicit and deterministic per key.

## Why Not Full Unification Immediately
Term ranges and unit fields have different semantics:
- Terms represent named cyclical windows.
- Duration keys represent arithmetic or calendar-field operations.

A single generic parser without key-scoped rules risks ambiguous behavior and regressions.

## Proposed Direction
Introduce key-scoped Slick handling for duration/unit keys in phases.

### Phase 1 (Recommended First Step)
Add Slick support for weekday-style keys only:
- `wkd`
- `dow`

Illustrative Phase 1 examples:
```javascript
// assign by weekday name (short/full)
t1.set({ wkd: 'Mon' });
t1.set({ wkd: 'Monday' });

// relative token: move to the next occurrence of the current weekday
t1.set({ dow: 'next' });

// numeric-relative offset: advance N weekdays
t1.set({ dow: '>2' });
```

Rationale:
- Strong existing parser support for weekday-relative logic.
- Lowest ambiguity.
- High utility for natural expressions.

### Phase 2
Evaluate `week` key support with explicit semantics.

Possible contract (to validate):
- `">2Mon"` means advance to the second following Monday using a defined week anchor policy.

Must define:
- What is the anchor week?
- How cross-boundary transitions are counted.
- Whether current-week matches are included/excluded.

### Phase 3 (Optional)
Evaluate month/year key Slick forms only after policies are formalized for overflow and day clamping.

## Suggested Semantics Guardrails
- Parse grammar should be key-aware, not global.
- Unsupported key+pattern combinations should throw descriptive errors.
- Illustrative template (invalid pattern for `month` key): `Invalid Slick pattern for key "month": ">>3". Expected numeric offset or supported month-token form.`
- Illustrative template (unsupported week-anchor modifier): `Unsupported week-anchor modifier for key "week": "^Mon". Supported modifiers are ">", ">=", "<", "<=" with a valid weekday anchor.`
- These are illustrative templates for consistent feedback when throwing key+pattern validation errors in Slick syntax parsing logic.
- Keep existing numeric/object mutation behavior unchanged.
- Preserve term Slick behavior exactly unless explicitly version-gated.

## Compatibility & Risk Notes
- Main risk: user confusion between term and unit semantics when both accept similar modifiers.
- Mitigation:
- Explicit per-key docs and examples.
- Validation errors for unsupported combinations.
- Incremental rollout with tests per key.
- Backward compatibility: Scan for existing Slick patterns that could collide with new unit-key syntax, add compatibility tests for known legacy patterns, and use a staged deprecation/rollout strategy (for example opt-in flag + warnings before default-on behavior).
- Performance: Measure parser impact in hot paths, publish benchmark/profile results, and add mitigation tactics such as caching parse results and fast-path guards for common valid forms.

## Testing Strategy (Future)
- Add table-driven tests for each supported key (`wkd`, `dow`, `week`).
- For each key (`wkd`, `dow`, `week`), include explicit edge-case categories:
- DST transitions (spring-forward/fall-back behavior).
- Leap-year boundaries (Feb 29 <-> Feb 28).
- End-of-month rollovers (for example Jan 31 -> Feb 28/29).
- Cross-year transitions (Dec -> Jan).
- For each edge-case category, include timezone-sensitive variants.
- Add regression tests proving existing Slick term behavior is unchanged.

## Open Questions
1. Should `week` Slick be interpreted relative to current date, start-of-week, or nearest matching weekday?
2. Should symbolic modifiers (`>`, `>=`, `<`, `<=`) map exactly to current term Slick semantics?
- Recommended answer (Question 2): Yes. Symbolic modifiers (`>`, `>=`, `<`, `<=`) should map exactly to current term Slick semantics to preserve consistency and reduce cognitive load for existing users.
- Justification: Reusing established modifier behavior minimizes migration friction, strengthens backward-compatibility expectations, and avoids introducing a second mental model for nearly identical syntax.
- Action item (implementation/testing): Codify an explicit operator mapping table shared between term and unit Slick paths, and add parity tests plus backward-compatibility regression tests proving modifier behavior is unchanged.
3. Do we allow number words (`two`) in unit Slick counts, or numbers only?
4. Should this ship behind a parse/mutate option flag first?

## Recommendation
### Timeline & Priority (Optional)
- Priority: Medium
- Estimated effort:
- Phase 1 (`wkd`/`dow`): ~2-3 days (spec + implementation + tests + docs)
- Phase 2 (`week`): ~3-5 days due to additional semantic complexity

Proceed with a conservative, phased implementation:
1. Formal spec for `wkd`/`dow` first.
2. Implement + test + document.
3. Evaluate `week` syntax (`">2Mon"`) after semantics are signed off.
Loading