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
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.4.0] - 2026-04-24

### Added
- **Sandbox Factory Mode**: Introduced `Tempo.create()`, a static factory method for creating isolated `Tempo` subclasses with independent configurations and registries, preventing global state leakage.
- **Layout Controller Framework**: Added a classification-based layout controller to `engine.layout`, enabling future input-aware parsing optimizations.

### Changed
- **Layout Order Resolver**: Extracted layout-ordering logic into a dedicated module to improve maintainability and testability.
- **Module Path Flattening**: Relocated core modules to `src/module/` for a flatter, more intuitive internal architecture.

### Fixed
- **Determinism Coverage**: Added comprehensive unit tests for layout resolution and multi-pair swap handling.


## [2.3.0] - 2026-04-22

### Added
- **Standalone Parse Engine**: Extracted the natural language engine into a standalone `parse()` function for instance-free datetime resolution.
- **Noise Filtering**: Added an `ignore` option to strip irrelevant words during string parsing.
- **Backtracking Security**: Implemented `Match.backtrack` safety guards in the snippet registry.
- **Ecosystem Installation Guide**: Released comprehensive installation instructions for Node.js, Deno, and standard browser environments.

### Changed
- **Automatic Context Sync**: Hemisphere settings now automatically synchronize with timezone updates.
- **State Optimization**: Refactored the internal parser state machine for reduced memory usage.
- **Interactive Playground**: Enhanced the browser-based demo with live timezone selectors and real-time clock updates.

### Fixed
- **Resolution Resilience**: Hardened the resolution loop with safety valves to prevent infinite loops in extreme date ranges.
- **Type Safety**: Hardened TypeScript definitions for all `parse` and `term` resolution functions.


## [2.2.6] - 2026-04-20

### Added
Expand Down
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.4.0",
"version": "2.5.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.4.0",
"version": "2.5.0",
"description": "Shared utility library for Tempo",
"author": "Magma Computing Solutions",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/library/src/browser/mapper.library.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { asObject } from '#library/object.library.js';
import { CONTEXT, getContext } from '#library/utility.library.js';
import { isNullish } from '#library/type.library.js';
import { isNullish } from '#library/assertion.library.js';
import { instant } from '#library/temporal.library.js';
import { getHemisphere } from '#library/international.library.js';

Expand Down
3 changes: 2 additions & 1 deletion packages/library/src/browser/tapper.class.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { enumify } from '#library/enumerate.library.js';
import { isEmpty, isFunction, type ValueOf } from '#library/type.library.js';
import { isEmpty, isFunction } from '#library/assertion.library.js';
import type { ValueOf } from '#library/type.library.js';

/**
* A Wrapper Class around HammerJS.
Expand Down
3 changes: 2 additions & 1 deletion packages/library/src/browser/webstore.class.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { distinct, ownEntries } from '#library/primitive.library.js';
import { stringify, objectify } from '#library/serialize.library.js';
import { asType, isEmpty, isNullish, isString } from '#library/type.library.js';
import { asType } from '#library/type.library.js';
import { isEmpty, isNullish, isString } from '#library/assertion.library.js';
import type { Property, ValueOf } from '#library/type.library.js';

const STORAGE = {
Expand Down
1 change: 1 addition & 0 deletions packages/library/src/common.index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

export * from './common/array.library.js';
export * from './common/assertion.library.js';
export * from './common/buffer.library.js';
export * from './common/cipher.class.js';
export * from './common/class.library.js';
Expand Down
8 changes: 4 additions & 4 deletions packages/library/src/common/array.library.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { asString } from '#library/coercion.library.js';
import { asString, nullishToValue } from '#library/coercion.library.js';
import { extract, ownEntries } from '#library/primitive.library.js';
import { stringify } from '#library/serialize.library.js';
import { isNumber, isDate, isObject, isDefined, isUndefined, isFunction, nullToValue } from '#library/type.library.js';
import { isNumber, isDate, isObject, isDefined, isUndefined, isFunction } from '#library/assertion.library.js';
import type { Property } from '#library/type.library.js';

// adapted from https://jsbin.com/insert/4/edit?js,output
Expand Down Expand Up @@ -48,8 +48,8 @@ export function sortBy<T extends Property<T>>(...keys: (PropertyKey | SortBy)[])
if (result === 0) { // no need to look further if result !== 0
const dir = key.dir === 'desc' ? -1 : 1;
const field = key.field + (key.index ? `[${key.index}]` : '');
const valueA = extract(left, field, nullToValue(key.default, 0));
const valueB = extract(right, field, nullToValue(key.default, 0));
const valueA = extract(left, field, nullishToValue(key.default, 0));
const valueB = extract(right, field, nullishToValue(key.default, 0));

switch (true) {
case isNumber(valueA) && isNumber(valueB):
Expand Down
109 changes: 109 additions & 0 deletions packages/library/src/common/assertion.library.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { sym } from '#library/symbol.library.js';
import { getType, protoType, asType } from '#library/type.library.js';
import type { Type, Primitive, Nullish, Temporals, Property, GetType } from '#library/type.library.js';

/** assert value is one of a list of Types */
export const isType = <T>(obj: unknown, ...types: Type[]): obj is T => types.includes(getType(obj));

/** Type-Guards: assert \<obj> is of \<type> */
export const isPrimitive = (obj?: unknown): obj is Primitive => isType(obj, 'String', 'Number', 'BigInt', 'Boolean', 'Symbol', 'Undefined', 'Void', 'Null', 'Empty');
export const isReference = (obj?: unknown): obj is Object => !isPrimitive(obj);
export const isIterable = <T>(obj: unknown): obj is Iterable<T> => Symbol.iterator in Object(obj) && !isString(obj);

export const isString = <T>(obj?: T): obj is Extract<T, string> => isType(obj, 'String');
export const isNumber = <T>(obj?: T): obj is Extract<T, number> => isType(obj, 'Number');
export const isFiniteNumber = <T>(obj?: T): obj is Extract<T, number> => isType(obj, 'Number') && isFinite(obj as number);

/** test if can convert String to Numeric */
export function isNumeric(str?: any): boolean {
const type = typeof str;
switch (type) {
case 'number': return isFinite(str);
case 'bigint': return true;
case 'string': {
const val = str.trim();
if (val.length === 0) return false;
return /^-?[0-9]+n$/.test(val) || (!isNaN(parseFloat(val)) && isFinite(Number(val)));
}
default: return false;
}
}
export const isInteger = <T>(obj?: T): obj is Extract<T, bigint> => isType(obj, 'BigInt');
export const isIntegerLike = <T>(obj?: T): obj is Extract<T, string> => isType(obj, 'String') && /^-?[0-9]+n$/.test(obj as string);
export const isDigit = <T>(obj?: T): obj is Extract<T, number | bigint> => isType(obj, 'Number', 'BigInt');
export const isBoolean = <T>(obj?: T): obj is Extract<T, boolean> => isType(obj, 'Boolean');
export const isArray = <T>(obj: unknown): obj is T[] => isType(obj, 'Array');
export const isArrayLike = <T>(obj: any): obj is ArrayLike<T> => protoType(obj) === 'Object' && 'length' in obj && Object.keys(obj).every(key => key === 'length' || !isNaN(Number(key)));
export const isObject = <T>(obj?: T): obj is Extract<T, object> => isType(obj, 'Object');
export const isDate = <T>(obj?: T): obj is Extract<T, Date> => isType(obj, 'Date');
export const isRegExp = <T>(obj?: T): obj is Extract<T, RegExp> => isType(obj, 'RegExp');
export const isRegExpLike = <T>(obj?: T): obj is Extract<T, string> => isType(obj, 'String') && /^\/.*\/$/.test(obj as string);
export const isSymbol = <T>(obj?: T): obj is Extract<T, symbol> => isType(obj, 'Symbol');
export const isSymbolFor = <T>(obj?: T): obj is Extract<T, symbol> => isType<symbol>(obj, 'Symbol') && Symbol.keyFor(obj) !== undefined;
export const isPropertyKey = (obj?: unknown): obj is PropertyKey => isType<PropertyKey>(obj, 'String', 'Number', 'Symbol');

export const isNull = <T>(obj?: T): obj is Extract<T, null> => isType(obj, 'Null');
export const isNullish = <T>(obj: T): obj is Extract<T, Nullish> => isType<undefined | null | void>(obj, 'Null', 'Undefined', 'Void', 'Empty');
export const isUndefined = <T>(obj?: T): obj is undefined => isType<undefined>(obj, 'Undefined', 'Void', 'Empty');
export const isDefined = <T>(obj: T): obj is NonNullable<T> => !isNullish(obj);

export const isClass = <T>(obj?: T): obj is Extract<T, Function> => isType(obj, 'Class');
export const isFunction = <T>(obj?: T): obj is Extract<T, Function> => isType(obj, 'Function', 'AsyncFunction');
export const isPromise = <T>(obj?: T): obj is Extract<T, Promise<any>> => isType(obj, 'Promise');
export const isMap = <T, K = any, V = any>(obj?: T): obj is Extract<T, Map<K, V>> => isType(obj, 'Map');
export const isSet = <T, K = any>(obj?: T): obj is Extract<T, Set<K>> => isType(obj, 'Set');
export const isError = <T>(err?: T): err is Extract<T, Error> => isType(err, 'Error');

export const isTemporal = <T>(obj: T): obj is Extract<T, Temporals> => protoType(obj).startsWith('Temporal.') || (!!(globalThis as any).Temporal && (
(obj as any) instanceof (globalThis as any).Temporal.Instant ||
(obj as any) instanceof (globalThis as any).Temporal.ZonedDateTime ||
(obj as any) instanceof (globalThis as any).Temporal.PlainDate ||
(obj as any) instanceof (globalThis as any).Temporal.PlainTime ||
(obj as any) instanceof (globalThis as any).Temporal.PlainDateTime ||
(obj as any) instanceof (globalThis as any).Temporal.Duration ||
(obj as any) instanceof (globalThis as any).Temporal.PlainYearMonth ||
(obj as any) instanceof (globalThis as any).Temporal.PlainMonthDay
));

export const isInstant = <T>(obj: T): obj is Extract<T, Temporal.Instant> => isType(obj, 'Temporal.Instant') || (!!(globalThis as any).Temporal?.Instant && (obj as any) instanceof (globalThis as any).Temporal.Instant) || (!!obj && (obj as any)[Symbol.toStringTag] === 'Temporal.Instant') || (!!obj && typeof (obj as any).toZonedDateTimeISO === 'function' && isUndefined((obj as any).timeZoneId));
export const isZonedDateTime = <T>(obj: T): obj is Extract<T, Temporal.ZonedDateTime> => isType(obj, 'Temporal.ZonedDateTime') || (!!(globalThis as any).Temporal?.ZonedDateTime && (obj as any) instanceof (globalThis as any).Temporal.ZonedDateTime) || (!!obj && (obj as any)[Symbol.toStringTag] === 'Temporal.ZonedDateTime') || (!!obj && typeof (obj as any).toInstant === 'function' && isDefined((obj as any).timeZoneId));
export const isPlainDate = <T>(obj: T): obj is Extract<T, Temporal.PlainDate> => isType(obj, 'Temporal.PlainDate') || (!!(globalThis as any).Temporal?.PlainDate && (obj as any) instanceof (globalThis as any).Temporal.PlainDate) || (!!obj && (obj as any)[Symbol.toStringTag] === 'Temporal.PlainDate') || (!!obj && typeof (obj as any).toZonedDateTime === 'function' && isUndefined((obj as any).timeZoneId) && isDefined((obj as any).daysInMonth) && isUndefined((obj as any).hour) && isUndefined((obj as any).minute) && isUndefined((obj as any).second) && isUndefined((obj as any).nanosecond));
export const isPlainTime = <T>(obj: T): obj is Extract<T, Temporal.PlainTime> => isType(obj, 'Temporal.PlainTime') || (!!(globalThis as any).Temporal?.PlainTime && (obj as any) instanceof (globalThis as any).Temporal.PlainTime) || (!!obj && (obj as any)[Symbol.toStringTag] === 'Temporal.PlainTime') || (!!obj && typeof (obj as any).toPlainDateTime === 'function' && isUndefined((obj as any).daysInMonth));
export const isPlainDateTime = <T>(obj: T): obj is Extract<T, Temporal.PlainDateTime> => isType(obj, 'Temporal.PlainDateTime') || (!!(globalThis as any).Temporal?.PlainDateTime && (obj as any) instanceof (globalThis as any).Temporal.PlainDateTime) || (!!obj && (obj as any)[Symbol.toStringTag] === 'Temporal.PlainDateTime') || (!!obj && typeof (obj as any).toZonedDateTime === 'function' && isUndefined((obj as any).timeZoneId) && (isDefined((obj as any).hour) || isDefined((obj as any).minute) || isDefined((obj as any).second) || isDefined((obj as any).nanosecond)));
export const isDuration = <T>(obj: T): obj is Extract<T, Temporal.Duration> => isType(obj, 'Temporal.Duration') || (!!(globalThis as any).Temporal?.Duration && (obj as any) instanceof (globalThis as any).Temporal.Duration) || (!!obj && (obj as any)[Symbol.toStringTag] === 'Temporal.Duration');
export const isDurationLike = <T>(obj: T): obj is Extract<T, Temporal.DurationLike | string | Temporal.Duration> => isString(obj) || isDuration(obj) || (isObject(obj) && (
'years' in obj || 'months' in obj || 'weeks' in obj || 'days' in obj ||
'hours' in obj || 'minutes' in obj || 'seconds' in obj ||
'milliseconds' in obj || 'microseconds' in obj || 'nanoseconds' in obj
));
export const isZonedDateTimeLike = <T>(obj: T): obj is Extract<T, Temporal.ZonedDateTimeLike | string | Temporal.ZonedDateTime> => isString(obj) || isZonedDateTime(obj) || (isObject(obj) && (
'year' in obj || 'month' in obj || 'day' in obj || 'hour' in obj || 'minute' in obj || 'second' in obj ||
'millisecond' in obj || 'microsecond' in obj || 'nanosecond' in obj || 'monthCode' in obj || 'offset' in obj || 'timeZone' in obj || 'calendar' in obj
));
export const isPlainYearMonth = <T>(obj: T): obj is Extract<T, Temporal.PlainYearMonth> => isType(obj, 'Temporal.PlainYearMonth') || (!!(globalThis as any).Temporal?.PlainYearMonth && (obj as any) instanceof (globalThis as any).Temporal.PlainYearMonth);
export const isPlainMonthDay = <T>(obj: T): obj is Extract<T, Temporal.PlainMonthDay> => isType(obj, 'Temporal.PlainMonthDay') || (!!(globalThis as any).Temporal?.PlainMonthDay && (obj as any) instanceof (globalThis as any).Temporal.PlainMonthDay);

// non-standard Objects
export const isEnum = <T, E extends Property<any>>(obj?: T): obj is Extract<T, GetType<'Enumify', E>> => isType(obj, 'Enumify');
export const isPledge = <T, P = any>(obj?: T): obj is Extract<T, GetType<'Pledge', P>> => isType(obj, 'Pledge');

/** assert value for secure() */
export const isExtensible = (obj: any): obj is any => !!(obj?.[sym.$Extensible]);
export const isTarget = (obj: any): obj is any => !!(obj?.[sym.$Target]);

/** object has no values */
export const isEmpty = <T>(obj?: T) => false
|| isNullish(obj)
|| (isObject(obj) && (Reflect.ownKeys(obj).length === 0))
|| (isString(obj) && (obj.trim().length === 0))
|| Number.isNaN(obj as any)
|| (isArray(obj) && (obj.length === 0))
|| (isSet(obj) && (obj.size === 0))
|| (isMap(obj) && (obj.size === 0))
Comment thread
magmacomputing marked this conversation as resolved.

export function assertCondition(condition: boolean, message?: string): asserts condition {
if (!condition)
throw new Error(message);
}
export function assertString(str: unknown): asserts str is string { assertCondition(isString(str), `Invalid string: ${str}`) };
export function assertNever(val: never): asserts val is never { throw new Error(`Unexpected object: ${val}`) };
26 changes: 6 additions & 20 deletions packages/library/src/common/coercion.library.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { clone, stringify } from '#library/serialize.library.js';
import { isIntegerLike, isArrayLike, isDefined, isInteger, isIterable, isNullish, isString, isUndefined, asType, isNumber } from '#library/type.library.js';
import { asType } from '#library/type.library.js';
import { isIntegerLike, isArrayLike, isDefined, isInteger, isIterable, isNullish, isString, isUndefined, isNumber, isNumeric } from '#library/assertion.library.js';

/** Coerce {value} into {value[]} ( if not already ), with optional {fill} Object */
export function asArray<T>(arr: Exclude<ArrayLike<T>, string> | undefined): T[];
Expand Down Expand Up @@ -52,25 +53,6 @@ export function asInteger<T extends string | number | bigint>(str?: T) {
}
}

/** test if can convert String to Numeric */
export function isNumeric(str?: string | number | bigint) {
const arg = asType(str);

switch (arg.type) {
case 'Number':
case 'BigInt':
return true;

case 'String':
return isIntegerLike(arg.value)
? true // is Number | BigInt
: !isNaN(asNumber(str)) && isFinite(str as number) // test if Number

default:
return false;
}
}

/** return as Number if possible, else original String */
export const ifNumeric = (str: string | number | bigint, stripZero = false) => {
switch (true) {
Expand All @@ -87,3 +69,7 @@ export const ifNumeric = (str: string | number | bigint, stripZero = false) => {
return str as string; // non-numeric String → as-is
}
}

export const nullishToZero = <T>(obj: T) => obj ?? 0;
export const nullishToEmpty = <T>(obj: T) => obj ?? '';
export const nullishToValue = <T, R>(obj: T, value: R) => obj ?? value;
10 changes: 4 additions & 6 deletions packages/library/src/common/enumerate.library.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { secure } from '#library/utility.library.js';
import { asType, isNumber } from '#library/type.library.js';
import { asType } from '#library/type.library.js';
import { isNumber } from '#library/assertion.library.js';
import { ownEntries } from '#library/primitive.library.js';
import { proxify } from '#library/proxy.library.js';
import { secure, proxify } from '#library/proxy.library.js';
import { Serializable } from '#library/class.library.js';
import { memoizeMethod } from '#library/function.library.js';
import lib from '#library/symbol.library.js';
import type { Property, Index, KeyOf, ValueOf, EntryOf, Invert, LooseKey } from '#library/type.library.js';

declare module '#library/type.library.js' {
Expand Down Expand Up @@ -79,7 +78,7 @@ function value(val: any) {
* ```typescript
* const Status = enumify(['Active', 'Inactive', 'Pending']);
* console.log(Status.Active); // 0
* console.log(Status.has('Active')); // true
* console.log(Status.has('Active')); // true
* console.log(Status.keys()); // ['Active', 'Inactive', 'Pending']
* ```
*/
Expand Down Expand Up @@ -109,7 +108,6 @@ export function enumify<T>(this: any, list: T, frozen = true): any {
}

const target = Object.create(proto, Object.getOwnPropertyDescriptors(stash));
if (!frozen) Object.defineProperty(target, lib.$Extensible, { value: true, enumerable: false });
return proxify(target, true, frozen); // proxy is ALWAYS frozen (read-only), but target is only 'locked' if requested
}

Expand Down
6 changes: 3 additions & 3 deletions packages/library/src/common/function.library.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { secure } from '#library/utility.library.js';
import { isUndefined, type Property } from '#library/type.library.js';
import { secure } from '#library/proxy.library.js';
import type { Property } from '#library/type.library.js';

// https://medium.com/codex/currying-in-typescript-ca5226c85b85
type PartialTuple<T extends any[], X extends any[] = []> =
Expand Down Expand Up @@ -36,7 +36,7 @@ type Curry<Args extends any[], Res> =
* Handles BigInt, Map, Set, Function, Undefined, and Circular refs.
*/
function serialize(val: any, seen = new WeakSet()): string {
return JSON.stringify(val, function(this: any, key: string, value: any) {
return JSON.stringify(val, function (this: any, key: string, value: any) {
if (value === undefined) return '\u0000__undefined__\u0000';
if (typeof value === 'bigint') return `bigint:${value}`;
if (typeof value === 'function') return `function:${value.name || 'anonymous'}`;
Expand Down
Loading