From d40a7c9a1bea187f6c8377bfb3bb651b40a80cd2 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Wed, 6 May 2026 18:41:39 +1000 Subject: [PATCH 01/16] starting point --- packages/tempo/plan/RELEASE-D.md | 10 +++++----- packages/tempo/src/tempo.class.ts | 3 ++- packages/tempo/src/tempo.type.ts | 2 ++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/tempo/plan/RELEASE-D.md b/packages/tempo/plan/RELEASE-D.md index f2caf2e..24da58c 100644 --- a/packages/tempo/plan/RELEASE-D.md +++ b/packages/tempo/plan/RELEASE-D.md @@ -14,11 +14,11 @@ This release focuses on modularizing and refactoring the parsing and pattern-mat - [ ] Update documentation and references ### Alias Resolution Engine Extraction -- [ ] Extract alias resolution logic to new module -- [ ] Define interfaces for registration, lookup, collision -- [ ] Refactor engine and plugins to use new APIs -- [ ] Add/expand unit tests for alias/collision -- [ ] Update documentation and references +- [x] Extract alias resolution logic to new module +- [x] Define interfaces for registration, lookup, collision +- [x] Refactor engine and plugins to use new APIs +- [x] Add/expand unit tests for alias/collision +- [x] Update documentation and references ### Guard Builder Extraction (Assessment) - [ ] Identify all guard-building/token-ingestion logic diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index 18705c6..b317287 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -22,10 +22,11 @@ import { registerTerm, getTermRange } from './plugin/term.util.js'; import type { TermPlugin, Plugin } from './plugin/plugin.type.js'; import { AliasEngine } from './engine/engine.alias.js'; +import { PatternCompiler } from './engine/engine.pattern.js'; import { resolveMonthDay } from './support/tempo.util.js'; import { DEFAULT_LAYOUT_CLASS, resolveLayoutOrder, getLayoutOrder } from './parse/parse.layout.js'; import { datePattern } from './support/tempo.default.js'; -import { setProperty, proto, hasOwn, compileRegExp, setPatterns, normalizeLayoutOrder } from './support/tempo.util.js'; +import { setProperty, proto, hasOwn, normalizeLayoutOrder } from './support/tempo.util.js'; import { sym, markConfig, TermError, getRuntime, init, extendState, isTempo, registryUpdate, registryReset, onRegistryReset, Match, Token, Snippet, Layout, Event, Period, Ignore, Default, Guard, enums, STATE, DISCOVERY, $Internal, $setConfig, $logError, $logDebug, $Identity, $setEvents, $setPeriods, $setAliases, $buildGuard, $IsBase, $Tempo, $Register, $Logify, $errored, $dbg, $guard, $Discover, $setDiscovery } from '#tempo/support'; import * as t from './tempo.type.js'; // namespaced types (Tempo.*) diff --git a/packages/tempo/src/tempo.type.ts b/packages/tempo/src/tempo.type.ts index 441552f..efe690d 100644 --- a/packages/tempo/src/tempo.type.ts +++ b/packages/tempo/src/tempo.type.ts @@ -16,6 +16,7 @@ import type { TermPlugin, Plugin } from '#tempo/plugin/plugin.type.js'; import type { Token } from '#tempo/support/tempo.symbol.js'; import type { Tempo } from '#tempo/tempo.class.js'; import { AliasEngine } from './engine/engine.alias.js'; +import { PatternCompiler } from './engine/engine.pattern.js'; declare global { interface globalThis { @@ -225,6 +226,7 @@ export namespace Internal { /** @internal current ZonedDateTime during parsing */ zdt?: Temporal.ZonedDateTime; /** @internal has the parse operation errored? */ errored?: boolean; /** @internal Alias engine for this Tempo instance */ aliasEngine?: AliasEngine; + /** @internal Pattern compiler for this Tempo instance */ patternCompiler?: PatternCompiler; } /** debug a Tempo instantiation */ From 1aaba36e9246bbda67ea417e1d1a3606d3e8fce6 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Wed, 6 May 2026 18:41:57 +1000 Subject: [PATCH 02/16] new content --- doc/main_branch_protection.md | 126 ++++++++++++++++++ packages/tempo/src/engine/engine.pattern.ts | 139 ++++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 doc/main_branch_protection.md create mode 100644 packages/tempo/src/engine/engine.pattern.ts diff --git a/doc/main_branch_protection.md b/doc/main_branch_protection.md new file mode 100644 index 0000000..f62f0fa --- /dev/null +++ b/doc/main_branch_protection.md @@ -0,0 +1,126 @@ +# Preventing Writes to the Main Branch Locally + +To prevent accidental writes (commits and pushes) to the `main` branch on your local workstation, you can use **Git Hooks**. + +## Summary of Changes +I have already set up local hooks for the `magma` repository: +1. **`pre-commit`**: Blocks direct commits to the `main` branch. +2. **`pre-push`**: Blocks pushing changes to the remote `main` branch. + +### How to Override +If you genuinely need to write to `main` (e.g., for an urgent fix), you have two options: +- **Environment Variable**: Prepend the command with the override flag: + - `ALLOW_MAIN_COMMIT=true git commit -m "Urgent fix"` + - `ALLOW_MAIN_PUSH=true git push` +- **Skip Hooks**: Use the standard Git flag: + - `git commit --no-verify` + - `git push --no-verify` + +--- + +## Applying This Globally (Recommended) +If you want this protection to apply to **every repository** on your workstation, you can configure a global hooks directory. + +### 1. Create a Global Hooks Directory +Choose a location (e.g., `~/.git-hooks`) and move the hook scripts there: + +```bash +mkdir -p ~/.git-hooks +# Copy the hooks I created for you +cp /home/michael/Project/magma/.git/hooks/pre-commit ~/.git-hooks/ +cp /home/michael/Project/magma/.git/hooks/pre-push ~/.git-hooks/ +chmod +x ~/.git-hooks/* +``` + +### 2. Configure Git Globally +Run this command to tell Git to use your new global hooks directory: + +```bash +git config --global core.hooksPath ~/.git-hooks +``` + +--- + +## Hook Implementation Details + +### pre-commit +This script checks the current branch before every commit. + +```bash +#!/bin/bash +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) +if [ "$CURRENT_BRANCH" = "main" ]; then + if [ "$ALLOW_MAIN_COMMIT" != "true" ]; then + echo "❌ ERROR: Direct commit to 'main' branch is prohibited." + exit 1 + fi +fi +``` + +### pre-push +This script checks the remote branch being pushed to. + +```bash +#!/bin/bash +while read local_ref local_sha remote_ref remote_sha +do + if [ "$remote_ref" = "refs/heads/main" ]; then + if [ "$ALLOW_MAIN_PUSH" != "true" ]; then + echo "❌ ERROR: Pushing to 'main' branch is prohibited." + exit 1 + fi + fi +done +``` + +--- + +## 🆘 I'm on 'main' and have changes, what do I do? + +If you've already made changes on `main` and the hook blocks your commit, **don't panic and don't drop your stash!** You can easily move your work to a new branch. + +### The "Magic" Command: Just Create a New Branch +Git allows you to create and switch to a new branch while keeping your uncommitted changes. + +```bash +# 1. Create and switch to a new branch +git checkout -b feature/my-cool-feature +# OR (modern syntax) +git switch -c feature/my-cool-feature + +# 2. Now you can commit normally +git add . +git commit -m "My feature changes" +``` + +### If you want to be extra safe (The Stash Method) +If you have a lot of complex changes and want to ensure `main` stays clean: + +```bash +# 1. Save your work temporarily +git stash + +# 2. Create and switch to the new branch +git checkout -b feature/my-cool-feature + +# 3. Bring your changes back +git stash pop + +# 4. Commit +git commit -am "My feature changes" +``` + +### "I accidentally committed before I added the hook!" +If you have local commits on `main` that you haven't pushed yet, you can move them to a new branch: + +```bash +# 1. Create a new branch at your current (accidental) commit +git branch feature/my-feature + +# 2. Reset your local 'main' back to where it should be (the remote version) +git reset --hard origin/main + +# 3. Switch to your new branch to continue working +git checkout feature/my-feature +``` + diff --git a/packages/tempo/src/engine/engine.pattern.ts b/packages/tempo/src/engine/engine.pattern.ts new file mode 100644 index 0000000..c523ef7 --- /dev/null +++ b/packages/tempo/src/engine/engine.pattern.ts @@ -0,0 +1,139 @@ +// engine.pattern.ts +// Pattern Compiler and Cache Engine for Tempo +// Responsible for snippet/layout expansion, regex compilation, and pattern caching + +import { isRegExp, isNullish, isEmpty, isString } from '#library/assertion.library.js'; +import { ownEntries, ownKeys } from '#library/primitive.library.js'; +import { Match, Snippet, Layout } from '../support/tempo.default.js'; +import { getSymbol, logError } from '../support/tempo.util.js'; +import { Token } from '../support/tempo.symbol.js'; +import enums from '../support/tempo.enum.js'; +import type * as t from '../tempo.type.js'; + +export interface PatternCompilerOptions { + state: t.Internal.State; +} + +export class PatternCompiler { + #state: t.Internal.State; + #cache: Map = new Map(); + + constructor(options: PatternCompilerOptions) { + this.#state = options.state; + } + + /** + * Translates {layout} into an anchored, case-insensitive RegExp. + * Includes recursive expansion of placeholders using snippet registries. + */ + compileRegExp(layout: string | RegExp, snippet?: Snippet): RegExp { + const source = isRegExp(layout) ? layout.source : layout; + + // Simple cache check for the raw source + const cacheKey = `${source}:${snippet ? 'custom' : 'global'}`; + if (this.#cache.has(cacheKey)) { + return this.#cache.get(cacheKey)!; + } + + const matcher = (src: string, d = 0): string => { + if (d > 10) return src; // prevent infinite recursion + + if (src.startsWith('/') && src.endsWith('/')) + src = src.substring(1, src.length - 1); + if (src.startsWith('^') && src.endsWith('$')) + src = src.substring(1, src.length - 1); + + return src.replace(new RegExp(Match.braces, 'g'), (match, name) => { + const token = getSymbol(name); + const customs = snippet?.[token as keyof Snippet]?.source ?? snippet?.[name as keyof Snippet]?.source; + const globals = this.#state.parse.snippet[token as keyof Snippet]?.source ?? this.#state.parse.snippet[name as keyof Snippet]?.source; + const stateLayout = this.#state.parse.layout[token as keyof Layout] ?? this.#state.parse.layout[name as keyof Layout]; + const defaultLayout = Layout[token as keyof Layout]; + + let res = customs ?? globals ?? stateLayout ?? defaultLayout; + + if (isNullish(res) && name.includes('.')) { + const prefix = name.split('.')[0]; + const pToken = getSymbol(prefix); + res = snippet?.[pToken as keyof Snippet]?.source ?? snippet?.[prefix as keyof Snippet]?.source + ?? this.#state.parse.snippet[pToken as keyof Snippet]?.source ?? this.#state.parse.snippet[prefix as keyof Snippet]?.source + ?? this.#state.parse.layout[pToken as keyof Layout] ?? this.#state.parse.layout[prefix as keyof Layout] + ?? Layout[pToken as keyof Layout]; + } + + if (res && name.includes('.')) { + const safeName = name.replace(/\./g, '_'); + if (!res.startsWith(`(?<${safeName}>`)) + res = `(?<${safeName}>${res})`; + } + + return (isNullish(res) || res === match) + ? match + : matcher(res, d + 1); + }); + }; + + try { + const expanded = matcher(source); + const compiled = new RegExp(`^(${expanded})$`, 'i'); + this.#cache.set(cacheKey, compiled); + return compiled; + } catch (e: any) { + const fallback = new RegExp(`^${Match.escape(layout as string)}$`, 'i'); + this.#cache.set(cacheKey, fallback); + return fallback; + } + } + + /** + * Build RegExp patterns into the state. + * Re-evaluates all snippets and layouts. + */ + setPatterns() { + const state = this.#state; + // ensure we have our own isolated mutable containers before mutation + state.parse.snippet = { ...state.parse.snippet }; + state.parse.pattern = new Map(); + + const snippet = state.parse.snippet; + + // 1. ensure numeric snippets are current + if (enums?.NUMBER) { + const keys = Object.keys(enums.NUMBER).map(w => Match.escape(w)); + const nbr = new RegExp(`(?[0-9]+|${keys.sort((a, b) => b.length - a.length).join('|')})`); + + snippet[Token.nbr] = nbr; + snippet[Token.mod] = new RegExp(`((?${Match.modifier.source})?${nbr.source}? *)`); + snippet[Token.afx] = new RegExp(`((s)? (?${Match.affix.source}))?${snippet[Token.sep].source}?`); + } + + // 2. build ignore pattern + const ignores = ownKeys(state.parse.ignore, true); + + if (!isEmpty(ignores)) { + const words = ignores + .filter(isString) + .map(w => Match.escape(w.toLowerCase())) + .join('|'); + + state.parse.ignorePattern = new RegExp(`\\b(${words})\\b`, 'gi'); + } else { + delete state.parse.ignorePattern; + } + + // 3. build the patterns + ownEntries(state.parse.layout).forEach(([key, layout]) => { + const symbol = getSymbol(key); + const compiled = this.compileRegExp(layout, snippet); + + state.parse.pattern.set(symbol, compiled); + }); + } + + /** + * Clear the pattern cache. + */ + clearCache() { + this.#cache.clear(); + } +} From 7e3928a75dd2f77e3ade3f79c34c70c1ce29a278 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Wed, 6 May 2026 18:50:43 +1000 Subject: [PATCH 03/16] refactor: migrate regex compilation logic to PatternCompiler class for improved state management and caching --- packages/tempo/src/engine/engine.pattern.ts | 12 +++ packages/tempo/src/support/support.index.ts | 3 +- packages/tempo/src/support/tempo.util.ts | 91 --------------------- packages/tempo/src/tempo.class.ts | 10 ++- 4 files changed, 21 insertions(+), 95 deletions(-) diff --git a/packages/tempo/src/engine/engine.pattern.ts b/packages/tempo/src/engine/engine.pattern.ts index c523ef7..825ba53 100644 --- a/packages/tempo/src/engine/engine.pattern.ts +++ b/packages/tempo/src/engine/engine.pattern.ts @@ -90,6 +90,7 @@ export class PatternCompiler { * Re-evaluates all snippets and layouts. */ setPatterns() { + this.clearCache(); const state = this.#state; // ensure we have our own isolated mutable containers before mutation state.parse.snippet = { ...state.parse.snippet }; @@ -137,3 +138,14 @@ export class PatternCompiler { this.#cache.clear(); } } + +/** + * Functional wrapper for the PatternCompiler. + * Handles engine instantiation and pattern building for a given state. + */ +export function setPatterns(state: t.Internal.State) { + if (!state.patternCompiler) { + state.patternCompiler = new PatternCompiler({ state }); + } + state.patternCompiler.setPatterns(); +} diff --git a/packages/tempo/src/support/support.index.ts b/packages/tempo/src/support/support.index.ts index 71d06f0..8a6f6af 100644 --- a/packages/tempo/src/support/support.index.ts +++ b/packages/tempo/src/support/support.index.ts @@ -32,5 +32,6 @@ export { $Tempo, $Register, $Interpreter, $logError, $logDebug, $dbg, $guard, $e export { registryUpdate, registryReset, onRegistryReset } from './tempo.register.js'; export { getRuntime, TempoRuntime } from './tempo.runtime.js'; export { Match, Snippet, Layout, Event, Period, Ignore, Guard, Default } from './tempo.default.js'; -export { SCHEMA, getLargestUnit, setPatterns, logError, logWarn, logDebug } from './tempo.util.js'; +export { SCHEMA, getLargestUnit, logError, logWarn, logDebug } from './tempo.util.js'; +export { setPatterns } from '../engine/engine.pattern.js'; export { init, extendState } from './tempo.init.js'; \ No newline at end of file diff --git a/packages/tempo/src/support/tempo.util.ts b/packages/tempo/src/support/tempo.util.ts index c43beb2..2755124 100644 --- a/packages/tempo/src/support/tempo.util.ts +++ b/packages/tempo/src/support/tempo.util.ts @@ -115,97 +115,6 @@ export function getLargestUnit(list: any[]): string { return 'nanosecond'; } -/** @internal translates {layout} into an anchored, case-insensitive RegExp. */ -export function compileRegExp(layout: string | RegExp, state: t.Internal.State, snippet?: Snippet) { - // helper function to replace {name} placeholders with their corresponding snippets - const matcher = (source: string, d = 0): string => { - if (d > 10) return source; // prevent infinite recursion - - if (source.startsWith('/') && source.endsWith('/')) - source = source.substring(1, source.length - 1); // remove the leading/trailing "/" - if (source.startsWith('^') && source.endsWith('$')) - source = source.substring(1, source.length - 1); // remove the leading/trailing anchors (^ $) - - return source.replace(new RegExp(Match.braces, 'g'), (match, name) => { // iterate over "{}" pairs in the source string - const token = getSymbol(name); // get the symbol for this {name} - const customs = snippet?.[token as keyof Snippet]?.source ?? snippet?.[name as keyof Snippet]?.source; - const globals = state.parse.snippet[token as keyof Snippet]?.source ?? state.parse.snippet[name as keyof Snippet]?.source; - const stateLayout = state.parse.layout[token as keyof Layout] ?? state.parse.layout[name as keyof Layout]; - const defaultLayout = Layout[token as keyof Layout]; // get resolution source (layout) - - let res = customs ?? globals ?? stateLayout ?? defaultLayout; // get the snippet/layout source - - if (isNullish(res) && name.includes('.')) { // if no definition found, try fallback - const prefix = name.split('.')[0]; // get the base token name - const pToken = getSymbol(prefix); - res = snippet?.[pToken as keyof Snippet]?.source ?? snippet?.[prefix as keyof Snippet]?.source - ?? state.parse.snippet[pToken as keyof Snippet]?.source ?? state.parse.snippet[prefix as keyof Snippet]?.source - ?? state.parse.layout[pToken as keyof Layout] ?? state.parse.layout[prefix as keyof Layout] - ?? Layout[pToken as keyof Layout]; - } - - if (res && name.includes('.')) { // wrap dotted extensions for identification - const safeName = name.replace(/\./g, '_'); - if (!res.startsWith(`(?<${safeName}>`)) - res = `(?<${safeName}>${res})`; - } - - return (isNullish(res) || res === match) // if no definition found, - ? match // return the original match - : matcher(res, d + 1); // else recurse to see if snippet contains embedded "{}" pairs - }); - }; - - try { - const source = isRegExp(layout) ? layout.source : layout; - const expanded = matcher(source); - return new RegExp(`^(${expanded})$`, 'i'); - } catch (e: any) { - return new RegExp(`^${Match.escape(layout as string)}$`, 'i'); - } -} - -/** @internal build RegExp patterns into the state */ -export function setPatterns(state: t.Internal.State) { - // ensure we have our own isolated mutable containers before mutation - state.parse.snippet = { ...state.parse.snippet }; - state.parse.pattern = new Map(); - - const snippet = state.parse.snippet; - - // 1. ensure numeric snippets are current - if (enums?.NUMBER) { - const keys = Object.keys(enums.NUMBER).map(w => Match.escape(w)); // escape each key - const nbr = new RegExp(`(?[0-9]+|${keys.sort((a, b) => b.length - a.length).join('|')})`); - - snippet[Token.nbr] = nbr; - snippet[Token.mod] = new RegExp(`((?${Match.modifier.source})?${nbr.source}? *)`); - snippet[Token.afx] = new RegExp(`((s)? (?${Match.affix.source}))?${snippet[Token.sep].source}?`); - } - - // 2. build ignore pattern - const ignores = ownKeys(state.parse.ignore, true); - - if (!isEmpty(ignores)) { - const words = ignores - .filter(isString) - .map(w => Match.escape(w.toLowerCase())) - .join('|'); - - state.parse.ignorePattern = new RegExp(`\\b(${words})\\b`, 'gi'); - } else { - delete state.parse.ignorePattern; - } - - // 3. build the patterns - ownEntries(state.parse.layout).forEach(([key, layout]) => { - const symbol = getSymbol(key); - const compiled = compileRegExp(layout, state, snippet); - - state.parse.pattern.set(symbol, compiled); - }); -} - /** * @internal Normalize a MonthDay configuration value against a base. * @param value The user-supplied value to normalize diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index b317287..5c2ad0e 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -27,7 +27,7 @@ import { resolveMonthDay } from './support/tempo.util.js'; import { DEFAULT_LAYOUT_CLASS, resolveLayoutOrder, getLayoutOrder } from './parse/parse.layout.js'; import { datePattern } from './support/tempo.default.js'; import { setProperty, proto, hasOwn, normalizeLayoutOrder } from './support/tempo.util.js'; -import { sym, markConfig, TermError, getRuntime, init, extendState, isTempo, registryUpdate, registryReset, onRegistryReset, Match, Token, Snippet, Layout, Event, Period, Ignore, Default, Guard, enums, STATE, DISCOVERY, $Internal, $setConfig, $logError, $logDebug, $Identity, $setEvents, $setPeriods, $setAliases, $buildGuard, $IsBase, $Tempo, $Register, $Logify, $errored, $dbg, $guard, $Discover, $setDiscovery } from '#tempo/support'; +import { sym, markConfig, TermError, getRuntime, init, extendState, setPatterns, isTempo, registryUpdate, registryReset, onRegistryReset, Match, Token, Snippet, Layout, Event, Period, Ignore, Default, Guard, enums, STATE, DISCOVERY, $Internal, $setConfig, $logError, $logDebug, $Identity, $setEvents, $setPeriods, $setAliases, $buildGuard, $IsBase, $Tempo, $Register, $Logify, $errored, $dbg, $guard, $Discover, $setDiscovery } from '#tempo/support'; import * as t from './tempo.type.js'; // namespaced types (Tempo.*) declare module '#library/type.library.js' { @@ -872,7 +872,7 @@ export class Tempo { /** @internal lookup or registers a new `Symbol` for a given key. */ static getSymbol(key?: string | symbol) { if (isUndefined(key)) { - const usr = `usr.${++Tempo.#usrCount}`; // allocate a prefixed 'user' key + const usr = `usr.${++Tempo.#usrCount}`; // allocate a prefixed 'user' key return Token[usr] = Symbol(usr); // add to Symbol register } @@ -888,7 +888,11 @@ export class Tempo { /** @internal translates {layout} into an anchored, case-insensitive RegExp. */ static regexp(layout: string | RegExp, snippet?: Snippet) { - return compileRegExp(layout, (this as any)[$Internal](), snippet as any); + const state = (this as any)[$Internal](); + + state.patternCompiler ??= new PatternCompiler({ state }); + + return state.patternCompiler.compileRegExp(layout, snippet as any); } /** Compares two `Tempo` instances or date-time values. */ From 3b7a1218d00e2b99d702e7b9716579818ef427fb Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Thu, 7 May 2026 08:20:50 +1000 Subject: [PATCH 04/16] PR 1st review --- .agent/workflows/interactive-testing.md | 6 +- .github/workflows/ci.yml | 35 -------- doc/main_branch_protection.md | 25 +++++- packages/tempo/index.md | 5 +- packages/tempo/plan/RELEASE-D.md | 13 +-- packages/tempo/src/engine/engine.alias.ts | 11 +-- packages/tempo/src/engine/engine.pattern.ts | 88 ++++++++++++--------- packages/tempo/src/support/tempo.init.ts | 12 ++- packages/tempo/src/support/tempo.util.ts | 25 +++--- packages/tempo/src/tempo.class.ts | 3 +- packages/tempo/src/tempo.type.ts | 2 +- 11 files changed, 116 insertions(+), 109 deletions(-) diff --git a/.agent/workflows/interactive-testing.md b/.agent/workflows/interactive-testing.md index 3ee4d48..15da7b3 100644 --- a/.agent/workflows/interactive-testing.md +++ b/.agent/workflows/interactive-testing.md @@ -6,12 +6,12 @@ To use a NodeJS interactive session to test your Tempo library, you can use the // turbo ```bash -npx tsx -i --import ./test/repl.ts +npx tsx --conditions=development -i --harmony-temporal --import ./bin/repl.ts ``` ### Purpose -This command starts a Node.js REPL (Read-Eval-Print Loop) while pre-loading the `Tempo` class and the `Temporal` polyfill into the global scope. This allows you to try different invocations of `Tempo` directly without writing a script. +This command starts a Node.js REPL (Read-Eval-Print Loop) while pre-loading the `Tempo` class and the `Temporal` support into the global scope. This allows you to try different invocations of `Tempo` directly without writing a script. ### Usage Examples Once the REPL has started, you can run commands like: @@ -32,4 +32,4 @@ t1.add({ days: 5 }).format('plain'); ### Why this works - `npx tsx`: Uses the `tsx` runner to handle TypeScript files on the fly. - `-i`: Explicitly requests an interactive session. -- `--import ./test/repl.ts`: Loads the helper script before starting the REPL, which attaches `Tempo` to `globalThis`. \ No newline at end of file +- `--import ./bin/repl.ts`: Loads the helper script before starting the REPL, which attaches `Tempo` to `globalThis`. \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30d121b..f851e78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,39 +34,4 @@ jobs: run: npm test working-directory: packages/tempo - test-parse-prefilter: - name: Test with parse.preFilter enabled - runs-on: ubuntu-latest - timeout-minutes: 30 - if: (github.event_name == 'push' || github.event_name == 'pull_request') && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/release/D' || github.event.pull_request.base.ref == 'main' || github.event.pull_request.base.ref == 'release/D') - steps: - - uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: 'npm' - - name: Install monorepo dependencies - run: npm ci - working-directory: ${{ github.workspace }} - - name: Run all tests with parse.preFilter - run: npm test - working-directory: packages/tempo - env: - TEMPO_PREFILTER_CI: 'true' - - name: Run end-to-end benchmark - run: npx tsx --conditions=development bench/bench.parse.prefilter.e2e.ts > bench-output.json 2> bench-error.log - working-directory: packages/tempo - - name: Upload benchmark output - if: always() - uses: actions/upload-artifact@v4 - with: - name: bench-parse-prefilter-e2e - path: | - packages/tempo/bench-output.json - packages/tempo/bench-error.log - - name: Validate benchmark output - run: | - node -e "const r=require('./packages/tempo/bench-output.json');if(!r.success){console.error('Benchmark failed:',r.errors);process.exit(1)}else{console.log('Benchmark passed.')}" - working-directory: ${{ github.workspace }} diff --git a/doc/main_branch_protection.md b/doc/main_branch_protection.md index f62f0fa..9e51236 100644 --- a/doc/main_branch_protection.md +++ b/doc/main_branch_protection.md @@ -26,19 +26,36 @@ Choose a location (e.g., `~/.git-hooks`) and move the hook scripts there: ```bash mkdir -p ~/.git-hooks -# Copy the hooks I created for you -cp /home/michael/Project/magma/.git/hooks/pre-commit ~/.git-hooks/ -cp /home/michael/Project/magma/.git/hooks/pre-push ~/.git-hooks/ +# Copy the hooks from your repository (replace /path/to/repo with your repo root or use the command below) +# Example using git to find the repo root: +cp $(git rev-parse --show-toplevel)/.git/hooks/pre-commit ~/.git-hooks/ +cp $(git rev-parse --show-toplevel)/.git/hooks/pre-push ~/.git-hooks/ chmod +x ~/.git-hooks/* ``` -### 2. Configure Git Globally + +### 2. Configure Git Globally (with Important Warning) Run this command to tell Git to use your new global hooks directory: ```bash git config --global core.hooksPath ~/.git-hooks ``` +**⚠️ Warning:** Setting `core.hooksPath` with the `--global` flag disables all per-repository `.git/hooks/*` scripts. This will break tools that rely on per-project hooks, such as Husky, lefthook, lint-staged, and others. Any hooks defined in individual repositories will be ignored as long as the global `core.hooksPath` is set. + +#### Recommended Alternatives + +- **Chain per-repo hooks from your global hook scripts:** + - Manually update your global hook scripts (in `~/.git-hooks/`) to call any existing hooks in each repository’s `.git/hooks/` directory, so you don’t lose project-specific logic. +- **Scope the setting to individual repositories:** + - Instead of using `--global`, set the hooks path only for the current repository: + ```bash + git config core.hooksPath ~/.git-hooks + ``` + - This way, only the current repo is affected, and other repos keep their own `.git/hooks/*` scripts. + +For more details, see the [Git documentation on `core.hooksPath`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-corehookspath). + --- ## Hook Implementation Details diff --git a/packages/tempo/index.md b/packages/tempo/index.md index e50cff6..83be48f 100644 --- a/packages/tempo/index.md +++ b/packages/tempo/index.md @@ -556,11 +556,12 @@ function focusActiveCard() { } .tempo-btn-brand { - background-color: var(--vp-c-brand-1); + background-color: #3498db; color: white; } .tempo-btn-brand:hover { - background-color: var(--vp-c-brand-2); + background-color: #2980b9; + color: white; } .tempo-btn-alt { diff --git a/packages/tempo/plan/RELEASE-D.md b/packages/tempo/plan/RELEASE-D.md index 24da58c..54e8017 100644 --- a/packages/tempo/plan/RELEASE-D.md +++ b/packages/tempo/plan/RELEASE-D.md @@ -5,13 +5,14 @@ This release focuses on modularizing and refactoring the parsing and pattern-mat ## Task Breakdown & Tracking + ### Pattern Compiler + Cache Extraction -- [ ] Extract `compileRegExp`, `setPatterns`, and helpers to new module -- [ ] Integrate memoization/caching logic as needed -- [ ] Refactor engine and consumers to use new module -- [ ] Ensure compatibility with snippet/layout definitions -- [ ] Add/expand unit tests for pattern logic and cache -- [ ] Update documentation and references +- [x] Extract `compileRegExp`, `setPatterns`, and helpers to new module (PatternCompiler) +- [x] Integrate memoization/caching logic as needed (PatternCompiler cache) +- [x] Refactor engine and consumers to use new PatternCompiler module +- [x] Ensure compatibility with snippet/layout definitions +- [x] Add/expand unit tests for pattern logic and cache +- [x] Update documentation and references ### Alias Resolution Engine Extraction - [x] Extract alias resolution logic to new module diff --git a/packages/tempo/src/engine/engine.alias.ts b/packages/tempo/src/engine/engine.alias.ts index dab7848..5b1561f 100644 --- a/packages/tempo/src/engine/engine.alias.ts +++ b/packages/tempo/src/engine/engine.alias.ts @@ -86,17 +86,14 @@ export class AliasEngine { this.#config = options.config; this.#id = AliasEngine.#idCounter++; - if (this.#parent) { - if (!(this.#parent instanceof AliasEngine)) { - const msg = "Parent engine must be an instance of AliasEngine"; - this.#logger?.error(this.#config, msg); - throw new TypeError(msg); - } - + if (this.#parent instanceof AliasEngine) { this.#depth = this.#parent.#depth + 1; this.#state = Object.create(this.#parent.#state); // create a new state object that inherits from the parent engine's state this.#words = Object.create(this.#parent.#words); // create a new words object that inherits from the parent engine's words for collision detection } else { + if (this.#parent) + this.#logger?.error(this.#config, "Parent engine must be an instance of AliasEngine"); + this.#depth = 0; this.#state = Object.create(null); // initialize an empty state for the root engine (no parent) this.#words = Object.create(null); // initialize an empty words object for the root engine (no parent) diff --git a/packages/tempo/src/engine/engine.pattern.ts b/packages/tempo/src/engine/engine.pattern.ts index 825ba53..de6389d 100644 --- a/packages/tempo/src/engine/engine.pattern.ts +++ b/packages/tempo/src/engine/engine.pattern.ts @@ -5,7 +5,7 @@ import { isRegExp, isNullish, isEmpty, isString } from '#library/assertion.library.js'; import { ownEntries, ownKeys } from '#library/primitive.library.js'; import { Match, Snippet, Layout } from '../support/tempo.default.js'; -import { getSymbol, logError } from '../support/tempo.util.js'; +import { getSymbol, hasOwn, logWarn, logError } from '../support/tempo.util.js'; import { Token } from '../support/tempo.symbol.js'; import enums from '../support/tempo.enum.js'; import type * as t from '../tempo.type.js'; @@ -16,7 +16,8 @@ export interface PatternCompilerOptions { export class PatternCompiler { #state: t.Internal.State; - #cache: Map = new Map(); + #globalCache: Map = new Map(); + #snippetCache: WeakMap> = new WeakMap(); constructor(options: PatternCompilerOptions) { this.#state = options.state; @@ -27,61 +28,71 @@ export class PatternCompiler { * Includes recursive expansion of placeholders using snippet registries. */ compileRegExp(layout: string | RegExp, snippet?: Snippet): RegExp { + const state = this.#state; const source = isRegExp(layout) ? layout.source : layout; - - // Simple cache check for the raw source - const cacheKey = `${source}:${snippet ? 'custom' : 'global'}`; - if (this.#cache.has(cacheKey)) { - return this.#cache.get(cacheKey)!; + let cache: Map; + if (snippet) { + if (!this.#snippetCache.has(snippet)) { + this.#snippetCache.set(snippet, new Map()); + } + cache = this.#snippetCache.get(snippet)!; + } else { + cache = this.#globalCache; + } + if (cache.has(source)) { + return cache.get(source)!; } - const matcher = (src: string, d = 0): string => { - if (d > 10) return src; // prevent infinite recursion + const matcher = (source: string, d = 0): string => { + if (d > 10) { // Emit a diagnostic if recursion limit is hit (likely circular placeholder) + logWarn?.(this.#state?.config, `[PatternCompiler] Recursion limit exceeded in matcher (d > 10) for src:`, source, `depth:`, d); + return source; + } - if (src.startsWith('/') && src.endsWith('/')) - src = src.substring(1, src.length - 1); - if (src.startsWith('^') && src.endsWith('$')) - src = src.substring(1, src.length - 1); + if (source.startsWith('/') && source.endsWith('/')) + source = source.substring(1, source.length - 1); // remove the leading/trailing "/" + if (source.startsWith('^') && source.endsWith('$')) + source = source.substring(1, source.length - 1); // remove the leading/trailing anchors (^ $) - return src.replace(new RegExp(Match.braces, 'g'), (match, name) => { - const token = getSymbol(name); + return source.replace(new RegExp(Match.braces, 'g'), (match, name) => { // iterate over "{}" pairs in the source string + const token = getSymbol(name); // get the symbol for this {name} const customs = snippet?.[token as keyof Snippet]?.source ?? snippet?.[name as keyof Snippet]?.source; - const globals = this.#state.parse.snippet[token as keyof Snippet]?.source ?? this.#state.parse.snippet[name as keyof Snippet]?.source; - const stateLayout = this.#state.parse.layout[token as keyof Layout] ?? this.#state.parse.layout[name as keyof Layout]; - const defaultLayout = Layout[token as keyof Layout]; + const globals = state.parse.snippet[token as keyof Snippet]?.source ?? state.parse.snippet[name as keyof Snippet]?.source; + const stateLayout = state.parse.layout[token as keyof Layout] ?? state.parse.layout[name as keyof Layout]; + const defaultLayout = Layout[token as keyof Layout]; // get resolution source (layout) - let res = customs ?? globals ?? stateLayout ?? defaultLayout; + let res = customs ?? globals ?? stateLayout ?? defaultLayout; // get the snippet/layout source - if (isNullish(res) && name.includes('.')) { - const prefix = name.split('.')[0]; + if (isNullish(res) && name.includes('.')) { // if no definition found, try fallback + const prefix = name.split('.')[0]; // get the base token name const pToken = getSymbol(prefix); res = snippet?.[pToken as keyof Snippet]?.source ?? snippet?.[prefix as keyof Snippet]?.source - ?? this.#state.parse.snippet[pToken as keyof Snippet]?.source ?? this.#state.parse.snippet[prefix as keyof Snippet]?.source - ?? this.#state.parse.layout[pToken as keyof Layout] ?? this.#state.parse.layout[prefix as keyof Layout] + ?? state.parse.snippet[pToken as keyof Snippet]?.source ?? state.parse.snippet[prefix as keyof Snippet]?.source + ?? state.parse.layout[pToken as keyof Layout] ?? state.parse.layout[prefix as keyof Layout] ?? Layout[pToken as keyof Layout]; } - if (res && name.includes('.')) { + if (res && name.includes('.')) { // wrap dotted extensions for identification const safeName = name.replace(/\./g, '_'); if (!res.startsWith(`(?<${safeName}>`)) res = `(?<${safeName}>${res})`; } - return (isNullish(res) || res === match) - ? match - : matcher(res, d + 1); + return (isNullish(res) || res === match) // if no definition found, + ? match // return the original match + : matcher(res, d + 1); // else recurse to see if snippet contains embedded "{}" pairs }); }; try { const expanded = matcher(source); const compiled = new RegExp(`^(${expanded})$`, 'i'); - this.#cache.set(cacheKey, compiled); + cache.set(source, compiled); return compiled; } catch (e: any) { - const fallback = new RegExp(`^${Match.escape(layout as string)}$`, 'i'); - this.#cache.set(cacheKey, fallback); - return fallback; + // Use the computed source for fallback, do not cache fallback, and log error + logError?.(e, { context: 'pattern compile failed', pattern: source }); + return new RegExp(`^${Match.escape(source)}$`, 'i'); } } @@ -105,7 +116,7 @@ export class PatternCompiler { snippet[Token.nbr] = nbr; snippet[Token.mod] = new RegExp(`((?${Match.modifier.source})?${nbr.source}? *)`); - snippet[Token.afx] = new RegExp(`((s)? (?${Match.affix.source}))?${snippet[Token.sep].source}?`); + snippet[Token.afx] = new RegExp(`((s)? (?${Match.affix.source}))?${snippet[Token.sep]?.source || ''}?`); } // 2. build ignore pattern @@ -123,7 +134,7 @@ export class PatternCompiler { } // 3. build the patterns - ownEntries(state.parse.layout).forEach(([key, layout]) => { + ownEntries(state.parse.layout, true).forEach(([key, layout]) => { const symbol = getSymbol(key); const compiled = this.compileRegExp(layout, snippet); @@ -135,7 +146,9 @@ export class PatternCompiler { * Clear the pattern cache. */ clearCache() { - this.#cache.clear(); + this.#globalCache.clear(); + // WeakMap has no clear(), so re-instantiate to drop all snippet-specific caches + this.#snippetCache = new WeakMap(); } } @@ -144,8 +157,9 @@ export class PatternCompiler { * Handles engine instantiation and pattern building for a given state. */ export function setPatterns(state: t.Internal.State) { - if (!state.patternCompiler) { + // 🛡️ Critical fix: ensure we use an OWN PatternCompiler for each state to avoid cross-pollution + if (!hasOwn(state, 'patternCompiler')) state.patternCompiler = new PatternCompiler({ state }); - } - state.patternCompiler.setPatterns(); + + state.patternCompiler!.setPatterns(); } diff --git a/packages/tempo/src/support/tempo.init.ts b/packages/tempo/src/support/tempo.init.ts index 9241a92..e2cd4a7 100644 --- a/packages/tempo/src/support/tempo.init.ts +++ b/packages/tempo/src/support/tempo.init.ts @@ -9,7 +9,7 @@ import { isString, isObject, isUndefined, isDefined, isRegExp } from '#library/a import { ownEntries } from '#library/primitive.library.js'; import { getRuntime } from './tempo.runtime.js'; -import { setProperty, setProperties, hasOwn, create, collect, normalizeLayoutOrder, resolveMonthDay } from './tempo.util.js'; +import { setProperty, setProperties, hasOwn, create, collect, normalizeLayoutOrder, resolveMonthDay, logError } from './tempo.util.js'; import { sym, Token } from './tempo.symbol.js'; import { Match, Snippet, Layout, Event, Period, Ignore, Default } from './tempo.default.js'; import enums, { STATE } from './tempo.enum.js'; @@ -141,8 +141,14 @@ export function extendState(state: t.Internal.State, options: t.Options) { if (optKey === 'snippet') { const pattern = isRegExp(v) ? v.source : String(v); // 🛡️ Security Check: Prevent catastrophic backtracking and malicious patterns - if (pattern.length > 500) throw new Error(`[Tempo#extend] Snippet pattern too long (max 500 chars).`); - if (Match.backtrack.test(pattern)) throw new Error(`[Tempo#extend] Snippet contains suspicious nested quantifiers.`); + if (pattern.length > 500) { + logError(state.config, `[Tempo#extend] Snippet pattern too long (max 500 chars).`); + return new RegExp(Match.escape(pattern)); + } + if (Match.backtrack.test(pattern)) { + logError(state.config, `[Tempo#extend] Snippet contains suspicious nested quantifiers.`); + return new RegExp(Match.escape(pattern)); + } return new RegExp(pattern); } return isRegExp(v) ? v.source : v; diff --git a/packages/tempo/src/support/tempo.util.ts b/packages/tempo/src/support/tempo.util.ts index 2755124..4de9b99 100644 --- a/packages/tempo/src/support/tempo.util.ts +++ b/packages/tempo/src/support/tempo.util.ts @@ -4,7 +4,7 @@ import { sym, Token } from './tempo.symbol.js'; import { asType } from '#library/type.library.js'; import { asArray } from '#library/coercion.library.js'; import { isSymbol, isUndefined, isDefined, isString, isRegExp, isNullish, isObject, isEmpty } from '#library/assertion.library.js'; -import { ownEntries, ownKeys } from '#library/primitive.library.js'; +import { ownEntries, ownKeys, unwrap } from '#library/primitive.library.js'; import { getRuntime } from './tempo.runtime.js'; import { Match, Snippet, Layout } from './tempo.default.js'; import enums from './tempo.enum.js'; @@ -49,17 +49,24 @@ export function logDebug(config: any, ...msg: any[]) { rt.logger?.debug(config ?? rt.state?.config, ...msg); } -/** @internal return the Prototype parent of an object */ -export const proto = (obj: object) => Object.getPrototypeOf(obj); +/** @internal check if an object is a proxy */ +export const isProxy = (obj: any): boolean => !!obj && !!(obj as any)[sym.$Target]; -/** @internal test object has own property with the given key */ -export const hasOwn = (obj: object, key: PropertyKey) => Object.hasOwn(obj, key); +/** @internal check if an object has an own property (respects Proxy/Shadowing) */ +export const hasOwn = (obj: any, key: PropertyKey): boolean => { + return Object.hasOwn(unwrap(obj), key); +} + +/** @internal get the prototype of an object */ +export const proto = (obj: any): any => Object.getPrototypeOf(unwrap(obj)); -/** @internal create an object based on a prototype */ -export const create = (obj: object, name: string): T => { +/** @internal create a new shadowed object from a prototype */ +export function create(obj: any, name: string): T { const entry = proto(obj)[name]; - if (!isObject(entry)) - throw new TypeError(`[Tempo#create] Failed to create shadowed object for '${name}'. The prototype entry from proto(obj) is missing or not an object (received: ${typeof entry}).`); + if (!isObject(entry)) { + logError(null, `[Tempo#create] Failed to create shadowed object for '${name}'. The prototype entry from proto(obj) is missing or not an object (received: ${typeof entry}).`); + return {} as T; + } return { ...entry } as T; } diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index 5c2ad0e..f243c38 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -23,10 +23,9 @@ import type { TermPlugin, Plugin } from './plugin/plugin.type.js'; import { AliasEngine } from './engine/engine.alias.js'; import { PatternCompiler } from './engine/engine.pattern.js'; -import { resolveMonthDay } from './support/tempo.util.js'; +import { resolveMonthDay, setProperty, proto, hasOwn, normalizeLayoutOrder } from './support/tempo.util.js'; import { DEFAULT_LAYOUT_CLASS, resolveLayoutOrder, getLayoutOrder } from './parse/parse.layout.js'; import { datePattern } from './support/tempo.default.js'; -import { setProperty, proto, hasOwn, normalizeLayoutOrder } from './support/tempo.util.js'; import { sym, markConfig, TermError, getRuntime, init, extendState, setPatterns, isTempo, registryUpdate, registryReset, onRegistryReset, Match, Token, Snippet, Layout, Event, Period, Ignore, Default, Guard, enums, STATE, DISCOVERY, $Internal, $setConfig, $logError, $logDebug, $Identity, $setEvents, $setPeriods, $setAliases, $buildGuard, $IsBase, $Tempo, $Register, $Logify, $errored, $dbg, $guard, $Discover, $setDiscovery } from '#tempo/support'; import * as t from './tempo.type.js'; // namespaced types (Tempo.*) diff --git a/packages/tempo/src/tempo.type.ts b/packages/tempo/src/tempo.type.ts index efe690d..dc18935 100644 --- a/packages/tempo/src/tempo.type.ts +++ b/packages/tempo/src/tempo.type.ts @@ -16,7 +16,7 @@ import type { TermPlugin, Plugin } from '#tempo/plugin/plugin.type.js'; import type { Token } from '#tempo/support/tempo.symbol.js'; import type { Tempo } from '#tempo/tempo.class.js'; import { AliasEngine } from './engine/engine.alias.js'; -import { PatternCompiler } from './engine/engine.pattern.js'; +import type { PatternCompiler } from './engine/engine.pattern.js'; declare global { interface globalThis { From 97c2f0c77c7f2157a3cb34af9db20b62abd015b6 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Thu, 7 May 2026 10:59:30 +1000 Subject: [PATCH 05/16] PR 2nd review --- doc/main_branch_protection.md | 5 ++++- packages/tempo/src/engine/engine.pattern.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/main_branch_protection.md b/doc/main_branch_protection.md index 9e51236..abb6c19 100644 --- a/doc/main_branch_protection.md +++ b/doc/main_branch_protection.md @@ -134,8 +134,11 @@ If you have local commits on `main` that you haven't pushed yet, you can move th # 1. Create a new branch at your current (accidental) commit git branch feature/my-feature -# 2. Reset your local 'main' back to where it should be (the remote version) +# 2. Make sure your local reference to 'origin/main' is up-to-date +git fetch origin +# 3. Reset your local 'main' back to where it should be (the remote version) git reset --hard origin/main +# (Fetching first ensures you don't accidentally reset to a stale origin/main reference) # 3. Switch to your new branch to continue working git checkout feature/my-feature diff --git a/packages/tempo/src/engine/engine.pattern.ts b/packages/tempo/src/engine/engine.pattern.ts index de6389d..fe1cf92 100644 --- a/packages/tempo/src/engine/engine.pattern.ts +++ b/packages/tempo/src/engine/engine.pattern.ts @@ -91,7 +91,7 @@ export class PatternCompiler { return compiled; } catch (e: any) { // Use the computed source for fallback, do not cache fallback, and log error - logError?.(e, { context: 'pattern compile failed', pattern: source }); + logError({ context: 'pattern compile failed', pattern: source }, e); return new RegExp(`^${Match.escape(source)}$`, 'i'); } } From 35b2dd3453ebad5ef77fc0a2e71b0994b8a72869 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Thu, 7 May 2026 12:16:08 +1000 Subject: [PATCH 06/16] PR 3rd review --- doc/main_branch_protection.md | 13 +++--- packages/tempo/doc/tempo.layout.md | 12 +++++ packages/tempo/package.json | 8 ++-- packages/tempo/src/discrete/discrete.parse.ts | 4 +- packages/tempo/src/engine/engine.layout.ts | 21 ++------- packages/tempo/src/engine/engine.pattern.ts | 36 ++++++++------- ....layout.resolver.ts => engine.resolver.ts} | 0 packages/tempo/src/parse/parse.layout.ts | 21 ++------- packages/tempo/src/parse/parse.resolver.ts | 27 ------------ packages/tempo/src/plugin/plugin.util.ts | 2 +- .../tempo/src/plugin/term/term.quarter.ts | 2 +- packages/tempo/src/plugin/term/term.season.ts | 2 +- .../{tempo.default.ts => support.default.ts} | 6 +-- .../{tempo.enum.ts => support.enum.ts} | 2 +- packages/tempo/src/support/support.index.ts | 16 +++---- .../{tempo.init.ts => support.init.ts} | 10 ++--- .../{tempo.intl.ts => support.intl.ts} | 0 ...{tempo.register.ts => support.register.ts} | 8 ++-- .../{tempo.runtime.ts => support.runtime.ts} | 2 +- .../{tempo.symbol.ts => support.symbol.ts} | 38 ++++++++-------- .../{tempo.util.ts => support.util.ts} | 8 ++-- packages/tempo/src/tempo.class.ts | 6 +-- packages/tempo/src/tempo.type.ts | 8 ++-- .../tempo/test/engine/parse.layout.test.ts | 2 +- .../pattern_compiler_optimization.test.ts | 44 +++++++++++++++++++ .../test/plugins/plugin_registration.test.ts | 2 +- 26 files changed, 153 insertions(+), 147 deletions(-) rename packages/tempo/src/engine/{engine.layout.resolver.ts => engine.resolver.ts} (100%) delete mode 100644 packages/tempo/src/parse/parse.resolver.ts rename packages/tempo/src/support/{tempo.default.ts => support.default.ts} (98%) rename packages/tempo/src/support/{tempo.enum.ts => support.enum.ts} (99%) rename packages/tempo/src/support/{tempo.init.ts => support.init.ts} (97%) rename packages/tempo/src/support/{tempo.intl.ts => support.intl.ts} (100%) rename packages/tempo/src/support/{tempo.register.ts => support.register.ts} (94%) rename packages/tempo/src/support/{tempo.runtime.ts => support.runtime.ts} (99%) rename packages/tempo/src/support/{tempo.symbol.ts => support.symbol.ts} (59%) rename packages/tempo/src/support/{tempo.util.ts => support.util.ts} (96%) create mode 100644 packages/tempo/test/engine/pattern_compiler_optimization.test.ts diff --git a/doc/main_branch_protection.md b/doc/main_branch_protection.md index abb6c19..98949e2 100644 --- a/doc/main_branch_protection.md +++ b/doc/main_branch_protection.md @@ -2,10 +2,11 @@ To prevent accidental writes (commits and pushes) to the `main` branch on your local workstation, you can use **Git Hooks**. -## Summary of Changes -I have already set up local hooks for the `magma` repository: -1. **`pre-commit`**: Blocks direct commits to the `main` branch. -2. **`pre-push`**: Blocks pushing changes to the remote `main` branch. + +## Example: Local Hooks for Main Branch Protection +Set up local hooks to: +1. **`pre-commit`**: Prevent direct commits to the `main` branch. +2. **`pre-push`**: Prevent pushing changes to the remote `main` branch. ### How to Override If you genuinely need to write to `main` (e.g., for an urgent fix), you have two options: @@ -137,10 +138,10 @@ git branch feature/my-feature # 2. Make sure your local reference to 'origin/main' is up-to-date git fetch origin # 3. Reset your local 'main' back to where it should be (the remote version) -git reset --hard origin/main # (Fetching first ensures you don't accidentally reset to a stale origin/main reference) +git reset --hard origin/main -# 3. Switch to your new branch to continue working +# 4. Switch to your new branch to continue working git checkout feature/my-feature ``` diff --git a/packages/tempo/doc/tempo.layout.md b/packages/tempo/doc/tempo.layout.md index 7a865df..39e565d 100644 --- a/packages/tempo/doc/tempo.layout.md +++ b/packages/tempo/doc/tempo.layout.md @@ -81,6 +81,18 @@ When crafting raw regex, the following capture groups are used by the engine: - `per`: Period string offset - `unt`: Relative unit (e.g., `days`, `weeks`) +## Prototyping with `Tempo.regexp()` + +You can use the static `Tempo.regexp()` method to "preview" how a layout string will be compiled by the engine. This is useful for testing custom regex logic before applying it to your configuration. + +```typescript +// Expands {dd}, {sep}, {mm}, etc. into a final anchored RegExp +const regex = Tempo.regexp('{dd}{sep}{mm}{sep}{yy}'); + +console.log(regex.source); +// Output: ^(?
...)(?:...)(?...)(?:...)(?...)$ +``` + --- ## Professional Services diff --git a/packages/tempo/package.json b/packages/tempo/package.json index bf436df..0447ede 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -54,8 +54,8 @@ "default": "./dist/core.index.js" }, "#tempo/enums": { - "development": "./src/support/tempo.enum.ts", - "default": "./dist/support/tempo.enum.js" + "development": "./src/support/support.enum.ts", + "default": "./dist/support/support.enum.js" }, "#tempo/duration": { "development": "./src/module/module.duration.ts", @@ -132,8 +132,8 @@ "import": "./dist/tempo.index.js" }, "./enums": { - "types": "./dist/support/tempo.enum.d.ts", - "import": "./dist/support/tempo.enum.js" + "types": "./dist/support/support.enum.d.ts", + "import": "./dist/support/support.enum.js" }, "./extend/*": { "types": "./dist/plugin/extend/extend.*.d.ts", diff --git a/packages/tempo/src/discrete/discrete.parse.ts b/packages/tempo/src/discrete/discrete.parse.ts index 7745ff3..7f8ef9f 100644 --- a/packages/tempo/src/discrete/discrete.parse.ts +++ b/packages/tempo/src/discrete/discrete.parse.ts @@ -16,8 +16,8 @@ import { defineInterpreterModule } from '../plugin/plugin.util.js'; import type { Range, ResolvedRange } from '../plugin/plugin.type.js'; import { sym, isTempo, TermError, getRuntime, Match } from '../support/support.index.js'; import { markConfig, setPatterns, init, extendState } from '../support/support.index.js'; -import { setProperty } from '#tempo/support/tempo.util.js'; -import enums from '../support/tempo.enum.js'; +import { setProperty } from '#tempo/support/support.util.js'; +import enums from '../support/support.enum.js'; import * as t from '../tempo.type.js'; import type { Tempo } from '../tempo.class.js'; diff --git a/packages/tempo/src/engine/engine.layout.ts b/packages/tempo/src/engine/engine.layout.ts index a07293d..a70db66 100644 --- a/packages/tempo/src/engine/engine.layout.ts +++ b/packages/tempo/src/engine/engine.layout.ts @@ -1,5 +1,6 @@ import { ownEntries } from '#library/primitive.library.js'; -import { Token } from '#tempo/support/tempo.symbol.js'; +import { Token } from '#tempo/support/support.symbol.js'; +import { resolveLayoutOrderPure } from './engine.resolver.js'; import type * as t from '../tempo.type.js'; export type LayoutEntry = [symbol, string]; @@ -94,22 +95,8 @@ export function resolveLayoutOrder({ layout, monthDayLayouts, isMonthDay, layout classification ?? DEFAULT_LAYOUT_CLASS, ); - const layouts = ownEntries(ordered) as LayoutEntry[]; - let changed = false; - - monthDayLayouts.forEach(([dmy, mdy]) => { - const idx1 = layouts.findIndex(([key]) => key.description === dmy); - const idx2 = layouts.findIndex(([key]) => key.description === mdy); - - if (idx1 === -1 || idx2 === -1) return; - - const swap1 = idx1 < idx2 && isMonthDay; - const swap2 = idx1 > idx2 && !isMonthDay; - if (swap1 || swap2) { - [layouts[idx1], layouts[idx2]] = [layouts[idx2], layouts[idx1]]; - changed = true; - } - }); + const layouts = resolveLayoutOrderPure(ordered, monthDayLayouts, isMonthDay); + const changed = layouts.some((entry, idx) => entry[0] !== (ownEntries(ordered)[idx] as LayoutEntry)[0]); if (changed) return Object.fromEntries(layouts) as Record; return ordered; diff --git a/packages/tempo/src/engine/engine.pattern.ts b/packages/tempo/src/engine/engine.pattern.ts index fe1cf92..3246ead 100644 --- a/packages/tempo/src/engine/engine.pattern.ts +++ b/packages/tempo/src/engine/engine.pattern.ts @@ -4,12 +4,14 @@ import { isRegExp, isNullish, isEmpty, isString } from '#library/assertion.library.js'; import { ownEntries, ownKeys } from '#library/primitive.library.js'; -import { Match, Snippet, Layout } from '../support/tempo.default.js'; -import { getSymbol, hasOwn, logWarn, logError } from '../support/tempo.util.js'; -import { Token } from '../support/tempo.symbol.js'; -import enums from '../support/tempo.enum.js'; +import { Match, Snippet, Layout } from '../support/support.default.js'; +import { getSymbol, hasOwn, logWarn, logError } from '../support/support.util.js'; +import { Token } from '../support/support.symbol.js'; +import enums from '../support/support.enum.js'; import type * as t from '../tempo.type.js'; +const BRACES_REGEX = new RegExp(Match.braces, 'g'); + export interface PatternCompilerOptions { state: t.Internal.State; } @@ -50,21 +52,21 @@ export class PatternCompiler { } if (source.startsWith('/') && source.endsWith('/')) - source = source.substring(1, source.length - 1); // remove the leading/trailing "/" + source = source.substring(1, source.length - 1); // remove the leading/trailing "/" if (source.startsWith('^') && source.endsWith('$')) - source = source.substring(1, source.length - 1); // remove the leading/trailing anchors (^ $) + source = source.substring(1, source.length - 1); // remove the leading/trailing anchors (^ $) - return source.replace(new RegExp(Match.braces, 'g'), (match, name) => { // iterate over "{}" pairs in the source string - const token = getSymbol(name); // get the symbol for this {name} + return source.replace(BRACES_REGEX, (match, name) => {// iterate over "{}" pairs in the source string + const token = getSymbol(name); // get the symbol for this {name} const customs = snippet?.[token as keyof Snippet]?.source ?? snippet?.[name as keyof Snippet]?.source; const globals = state.parse.snippet[token as keyof Snippet]?.source ?? state.parse.snippet[name as keyof Snippet]?.source; const stateLayout = state.parse.layout[token as keyof Layout] ?? state.parse.layout[name as keyof Layout]; - const defaultLayout = Layout[token as keyof Layout]; // get resolution source (layout) + const defaultLayout = Layout[token as keyof Layout];// get resolution source (layout) - let res = customs ?? globals ?? stateLayout ?? defaultLayout; // get the snippet/layout source + let res = customs ?? globals ?? stateLayout ?? defaultLayout; // get the snippet/layout source - if (isNullish(res) && name.includes('.')) { // if no definition found, try fallback - const prefix = name.split('.')[0]; // get the base token name + if (isNullish(res) && name.includes('.')) { // if no definition found, try fallback + const prefix = name.split('.')[0]; // get the base token name const pToken = getSymbol(prefix); res = snippet?.[pToken as keyof Snippet]?.source ?? snippet?.[prefix as keyof Snippet]?.source ?? state.parse.snippet[pToken as keyof Snippet]?.source ?? state.parse.snippet[prefix as keyof Snippet]?.source @@ -72,15 +74,15 @@ export class PatternCompiler { ?? Layout[pToken as keyof Layout]; } - if (res && name.includes('.')) { // wrap dotted extensions for identification + if (res && name.includes('.')) { // wrap dotted extensions for identification const safeName = name.replace(/\./g, '_'); if (!res.startsWith(`(?<${safeName}>`)) res = `(?<${safeName}>${res})`; } - return (isNullish(res) || res === match) // if no definition found, - ? match // return the original match - : matcher(res, d + 1); // else recurse to see if snippet contains embedded "{}" pairs + return (isNullish(res) || res === match) // if no definition found, + ? match // return the original match + : matcher(res, d + 1); // else recurse to see if snippet contains embedded "{}" pairs }); }; @@ -91,7 +93,7 @@ export class PatternCompiler { return compiled; } catch (e: any) { // Use the computed source for fallback, do not cache fallback, and log error - logError({ context: 'pattern compile failed', pattern: source }, e); + logError(this.#state.config, { context: 'pattern compile failed', pattern: source }, e); return new RegExp(`^${Match.escape(source)}$`, 'i'); } } diff --git a/packages/tempo/src/engine/engine.layout.resolver.ts b/packages/tempo/src/engine/engine.resolver.ts similarity index 100% rename from packages/tempo/src/engine/engine.layout.resolver.ts rename to packages/tempo/src/engine/engine.resolver.ts diff --git a/packages/tempo/src/parse/parse.layout.ts b/packages/tempo/src/parse/parse.layout.ts index ae4da41..08b034d 100644 --- a/packages/tempo/src/parse/parse.layout.ts +++ b/packages/tempo/src/parse/parse.layout.ts @@ -1,5 +1,6 @@ import { ownEntries } from '#library/primitive.library.js'; -import { Token } from '#tempo/support/tempo.symbol.js'; +import { Token } from '#tempo/support/support.symbol.js'; +import { resolveLayoutOrderPure } from '../engine/engine.resolver.js'; import type * as t from '../tempo.type.js'; export type LayoutEntry = [symbol, string]; @@ -77,22 +78,8 @@ export function resolveLayoutOrder({ layout, monthDayLayouts, isMonthDay, layout classification ?? DEFAULT_LAYOUT_CLASS, ); - const layouts = ownEntries(ordered) as LayoutEntry[]; - let changed = false; - - monthDayLayouts.forEach(([dmy, mdy]) => { - const idx1 = layouts.findIndex(([key]) => key.description === dmy); - const idx2 = layouts.findIndex(([key]) => key.description === mdy); - - if (idx1 === -1 || idx2 === -1) return; - - const swap1 = idx1 < idx2 && isMonthDay; - const swap2 = idx1 > idx2 && !isMonthDay; - if (swap1 || swap2) { - [layouts[idx1], layouts[idx2]] = [layouts[idx2], layouts[idx1]]; - changed = true; - } - }); + const layouts = resolveLayoutOrderPure(ordered, monthDayLayouts, isMonthDay); + const changed = layouts.some((entry, idx) => entry[0] !== (ownEntries(ordered)[idx] as LayoutEntry)[0]); if (changed) return Object.fromEntries(layouts) as Record; return ordered; diff --git a/packages/tempo/src/parse/parse.resolver.ts b/packages/tempo/src/parse/parse.resolver.ts deleted file mode 100644 index a03bceb..0000000 --- a/packages/tempo/src/parse/parse.resolver.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ownEntries } from '#library/primitive.library.js'; -import type * as t from '../tempo.type.js'; - -export type LayoutEntry = [symbol, string]; - -export function resolveLayoutOrderPure( - layout: Record, - monthDayLayouts: t.LayoutPair[] | readonly t.LayoutPair[], - isMonthDay: boolean -): LayoutEntry[] { - const layouts = ownEntries(layout) as LayoutEntry[]; - let changed = false; - - monthDayLayouts.forEach(([dmy, mdy]) => { - const idx1 = layouts.findIndex(([key]) => key.description === dmy); - const idx2 = layouts.findIndex(([key]) => key.description === mdy); - if (idx1 === -1 || idx2 === -1) return; - const swap1 = idx1 < idx2 && isMonthDay; - const swap2 = idx1 > idx2 && !isMonthDay; - if (swap1 || swap2) { - [layouts[idx1], layouts[idx2]] = [layouts[idx2], layouts[idx1]]; - changed = true; - } - }); - - return layouts; -} diff --git a/packages/tempo/src/plugin/plugin.util.ts b/packages/tempo/src/plugin/plugin.util.ts index 8302393..d974a06 100644 --- a/packages/tempo/src/plugin/plugin.util.ts +++ b/packages/tempo/src/plugin/plugin.util.ts @@ -2,7 +2,7 @@ import { isFunction, isString, isUndefined, isClass, isObject, isDefined } from import { secureRef } from '#library/proxy.library.js'; import { sym, getRuntime, isTempo } from '#tempo/support'; -import { hasOwn } from '#tempo/support/tempo.util.js'; +import { hasOwn } from '#tempo/support/support.util.js'; import type { Tempo } from '../tempo.class.js'; import type { Plugin } from './plugin.type.js'; diff --git a/packages/tempo/src/plugin/term/term.quarter.ts b/packages/tempo/src/plugin/term/term.quarter.ts index 51a4857..b1fec7c 100644 --- a/packages/tempo/src/plugin/term/term.quarter.ts +++ b/packages/tempo/src/plugin/term/term.quarter.ts @@ -1,5 +1,5 @@ import { defineTerm, getTermRange, defineRange, resolveCycleWindow } from '../term.util.js'; -import { COMPASS } from '../../support/tempo.enum.js'; +import { COMPASS } from '../../support/support.enum.js'; import { isNumber } from '#library/assertion.library.js'; import { asArray } from '#library'; import type { Tempo } from '../../tempo.class.js'; diff --git a/packages/tempo/src/plugin/term/term.season.ts b/packages/tempo/src/plugin/term/term.season.ts index 49024ce..02321db 100644 --- a/packages/tempo/src/plugin/term/term.season.ts +++ b/packages/tempo/src/plugin/term/term.season.ts @@ -1,5 +1,5 @@ import { getTermRange, defineTerm, defineRange, resolveCycleWindow } from '../term.util.js'; -import { COMPASS } from '../../support/tempo.enum.js'; +import { COMPASS } from '../../support/support.enum.js'; import type { Tempo } from '../../tempo.class.js'; /** definition of meteorological season ranges */ diff --git a/packages/tempo/src/support/tempo.default.ts b/packages/tempo/src/support/support.default.ts similarity index 98% rename from packages/tempo/src/support/tempo.default.ts rename to packages/tempo/src/support/support.default.ts index e76aa74..ce55d52 100644 --- a/packages/tempo/src/support/tempo.default.ts +++ b/packages/tempo/src/support/support.default.ts @@ -2,9 +2,9 @@ import { looseIndex } from '#library/object.library.js'; import { secure, proxify } from '#library/proxy.library.js'; import { getDateTimeFormat } from '#library/international.library.js'; -import { NUMBER, MODE, MONTH_DAY } from './tempo.enum.js'; -import { Token } from './tempo.symbol.js'; -import { IntlDefault } from './tempo.intl.js'; +import { NUMBER, MODE, MONTH_DAY } from './support.enum.js'; +import { Token } from './support.symbol.js'; +import { IntlDefault } from './support.intl.js'; import type { Options } from '../tempo.type.js'; import type { Tempo } from '../tempo.class.js'; diff --git a/packages/tempo/src/support/tempo.enum.ts b/packages/tempo/src/support/support.enum.ts similarity index 99% rename from packages/tempo/src/support/tempo.enum.ts rename to packages/tempo/src/support/support.enum.ts index d91e577..3f43a22 100644 --- a/packages/tempo/src/support/tempo.enum.ts +++ b/packages/tempo/src/support/support.enum.ts @@ -1,4 +1,4 @@ -import { sym } from './tempo.symbol.js'; +import { sym } from './support.symbol.js'; import { enumify, Enum } from '#library/enumerate.library.js'; import { proxify } from '#library/proxy.library.js'; import { allDescriptors } from '#library/reflection.library.js'; diff --git a/packages/tempo/src/support/support.index.ts b/packages/tempo/src/support/support.index.ts index 8a6f6af..0b68965 100644 --- a/packages/tempo/src/support/support.index.ts +++ b/packages/tempo/src/support/support.index.ts @@ -24,14 +24,14 @@ export { PARSE, MONTH_DAY, NumericPattern -} from './tempo.enum.js'; +} from './support.enum.js'; export { markConfig } from '#library/symbol.library.js'; -export { sym, isTempo, Token, TermError, type TempoBrand } from './tempo.symbol.js'; -export { $Tempo, $Register, $Interpreter, $logError, $logDebug, $dbg, $guard, $errored, $Internal, $Bridge, $RuntimeBrand, $Descriptor, $setConfig, $setDiscovery, $setEvents, $setPeriods, $setAliases, $buildGuard, $IsBase, $Identity, $Logify, $Discover, $ImmutableSkip } from './tempo.symbol.js'; -export { registryUpdate, registryReset, onRegistryReset } from './tempo.register.js'; -export { getRuntime, TempoRuntime } from './tempo.runtime.js'; -export { Match, Snippet, Layout, Event, Period, Ignore, Guard, Default } from './tempo.default.js'; -export { SCHEMA, getLargestUnit, logError, logWarn, logDebug } from './tempo.util.js'; +export { sym, isTempo, Token, TermError, type TempoBrand } from './support.symbol.js'; +export { $Tempo, $Register, $Interpreter, $logError, $logDebug, $dbg, $guard, $errored, $Internal, $Bridge, $RuntimeBrand, $Descriptor, $setConfig, $setDiscovery, $setEvents, $setPeriods, $setAliases, $buildGuard, $IsBase, $Identity, $Logify, $Discover, $ImmutableSkip } from './support.symbol.js'; +export { registryUpdate, registryReset, onRegistryReset } from './support.register.js'; +export { getRuntime, TempoRuntime } from './support.runtime.js'; +export { Match, Snippet, Layout, Event, Period, Ignore, Guard, Default } from './support.default.js'; +export { SCHEMA, getLargestUnit, logError, logWarn, logDebug } from './support.util.js'; export { setPatterns } from '../engine/engine.pattern.js'; -export { init, extendState } from './tempo.init.js'; \ No newline at end of file +export { init, extendState } from './support.init.js'; \ No newline at end of file diff --git a/packages/tempo/src/support/tempo.init.ts b/packages/tempo/src/support/support.init.ts similarity index 97% rename from packages/tempo/src/support/tempo.init.ts rename to packages/tempo/src/support/support.init.ts index e2cd4a7..206cee9 100644 --- a/packages/tempo/src/support/tempo.init.ts +++ b/packages/tempo/src/support/support.init.ts @@ -8,11 +8,11 @@ import { asType } from '#library/type.library.js'; import { isString, isObject, isUndefined, isDefined, isRegExp } from '#library/assertion.library.js'; import { ownEntries } from '#library/primitive.library.js'; -import { getRuntime } from './tempo.runtime.js'; -import { setProperty, setProperties, hasOwn, create, collect, normalizeLayoutOrder, resolveMonthDay, logError } from './tempo.util.js'; -import { sym, Token } from './tempo.symbol.js'; -import { Match, Snippet, Layout, Event, Period, Ignore, Default } from './tempo.default.js'; -import enums, { STATE } from './tempo.enum.js'; +import { getRuntime } from './support.runtime.js'; +import { setProperty, setProperties, hasOwn, create, collect, normalizeLayoutOrder, resolveMonthDay, logError } from './support.util.js'; +import { sym, Token } from './support.symbol.js'; +import { Match, Snippet, Layout, Event, Period, Ignore, Default } from './support.default.js'; +import enums, { STATE } from './support.enum.js'; import * as t from '../tempo.type.js'; /** @internal Initialise a Tempo state */ diff --git a/packages/tempo/src/support/tempo.intl.ts b/packages/tempo/src/support/support.intl.ts similarity index 100% rename from packages/tempo/src/support/tempo.intl.ts rename to packages/tempo/src/support/support.intl.ts diff --git a/packages/tempo/src/support/tempo.register.ts b/packages/tempo/src/support/support.register.ts similarity index 94% rename from packages/tempo/src/support/tempo.register.ts rename to packages/tempo/src/support/support.register.ts index 1bf410d..2859f9e 100644 --- a/packages/tempo/src/support/tempo.register.ts +++ b/packages/tempo/src/support/support.register.ts @@ -3,14 +3,14 @@ import { isEqual } from '#library/object.library.js'; import { isDefined, isObject, isSymbol, isUndefined } from '#library/assertion.library.js'; import { ownKeys } from '#library/primitive.library.js'; import { unwrap } from '#library/primitive.library.js'; -import { sym } from './tempo.symbol.js'; +import { sym } from './support.symbol.js'; import type { Property } from '#library/type.library.js'; -import { getRuntime } from './tempo.runtime.js'; -import { hasOwn, setProperty } from './tempo.util.js'; +import { getRuntime } from './support.runtime.js'; +import { hasOwn, setProperty } from './support.util.js'; // Import the live enums and their mutable state from the enum module -import { STATE, REGISTRIES, DEFAULTS } from './tempo.enum.js'; +import { STATE, REGISTRIES, DEFAULTS } from './support.enum.js'; const rt = getRuntime(); diff --git a/packages/tempo/src/support/tempo.runtime.ts b/packages/tempo/src/support/support.runtime.ts similarity index 99% rename from packages/tempo/src/support/tempo.runtime.ts rename to packages/tempo/src/support/support.runtime.ts index 3e439c6..df2fbcc 100644 --- a/packages/tempo/src/support/tempo.runtime.ts +++ b/packages/tempo/src/support/support.runtime.ts @@ -1,4 +1,4 @@ -import { sym } from './tempo.symbol.js'; +import { sym } from './support.symbol.js'; import type { TermPlugin, Extension, Plugin } from '../plugin/plugin.type.js'; import type { Internal } from '../tempo.type.js'; diff --git a/packages/tempo/src/support/tempo.symbol.ts b/packages/tempo/src/support/support.symbol.ts similarity index 59% rename from packages/tempo/src/support/tempo.symbol.ts rename to packages/tempo/src/support/support.symbol.ts index 8ef15d7..ea0d522 100644 --- a/packages/tempo/src/support/tempo.symbol.ts +++ b/packages/tempo/src/support/support.symbol.ts @@ -13,26 +13,26 @@ export const isTempo = (tempo?: any): tempo is TempoBrand => Boolean(tempo?.[sym export const TermError: unique symbol = Symbol.for('magmacomputing/tempo/termError') as any; /** @internal unique symbols for critical internal accessors */ -/** key for Global Discovery of Tempo configuration */ export const $Tempo: unique symbol = Symbol.for('$Tempo') as any; -/** key for Reactive Plugin Registration */ export const $Register: unique symbol = Symbol.for('magmacomputing/tempo/register') as any; -/** key for Internal Interpreter Service */ export const $Interpreter: unique symbol = Symbol.for('magmacomputing/tempo/interpreter') as any; -/** key for contextual Error Logging */ export const $logError: unique symbol = Symbol.for('magmacomputing/tempo/logError') as any; -/** key for contextual Debug Logging */ export const $logDebug: unique symbol = Symbol.for('magmacomputing/tempo/logDebug') as any; -/** key for contextual Debugger */ export const $dbg: unique symbol = Symbol.for('magmacomputing/tempo/dbg') as any; -/** key for Master Guard */ export const $guard: unique symbol = Symbol.for('magmacomputing/tempo/guard') as any; -/** internal key for signaling pre-errored state */ export const $errored: unique symbol = Symbol.for('magmacomputing/tempo/errored') as any; -/** internal key for accessing private instance state */ export const $Internal: unique symbol = Symbol.for('magmacomputing/tempo/internal') as any; -/** hardened globalThis bridge key for the TempoRuntime */export const $Bridge: unique symbol = Symbol.for('magmacomputing/tempo/runtime') as any; -/** cross-bundle brand check for TempoRuntime */ export const $RuntimeBrand: unique symbol = Symbol.for('magmacomputing/tempo/runtime/brand') as any; -/** branding for explicit PropertyDescriptors */ export const $Descriptor: unique symbol = Symbol.for('magmacomputing/tempo/descriptor') as any; +/** key for Global Discovery of Tempo configuration */ export const $Tempo: unique symbol = Symbol.for('$Tempo') as any; +/** key for Reactive Plugin Registration */ export const $Register: unique symbol = Symbol.for('magmacomputing/tempo/register') as any; +/** key for Internal Interpreter Service */ export const $Interpreter: unique symbol = Symbol.for('magmacomputing/tempo/interpreter') as any; +/** key for contextual Error Logging */ export const $logError: unique symbol = Symbol.for('magmacomputing/tempo/logError') as any; +/** key for contextual Debug Logging */ export const $logDebug: unique symbol = Symbol.for('magmacomputing/tempo/logDebug') as any; +/** key for contextual Debugger */ export const $dbg: unique symbol = Symbol.for('magmacomputing/tempo/dbg') as any; +/** key for Master Guard */ export const $guard: unique symbol = Symbol.for('magmacomputing/tempo/guard') as any; +/** internal key for signaling pre-errored state */ export const $errored: unique symbol = Symbol.for('magmacomputing/tempo/errored') as any; +/** internal key for accessing private instance state */ export const $Internal: unique symbol = Symbol.for('magmacomputing/tempo/internal') as any; +/** hardened globalThis bridge key for the TempoRuntime */ export const $Bridge: unique symbol = Symbol.for('magmacomputing/tempo/runtime') as any; +/** cross-bundle brand check for TempoRuntime */ export const $RuntimeBrand: unique symbol = Symbol.for('magmacomputing/tempo/runtime/brand') as any; +/** branding for explicit PropertyDescriptors */ export const $Descriptor: unique symbol = Symbol.for('magmacomputing/tempo/descriptor') as any; -/** internal static config helper */ export const $setConfig: unique symbol = Symbol.for('magmacomputing/tempo/setConfig') as any; -/** internal static discovery helper */ export const $setDiscovery: unique symbol = Symbol.for('magmacomputing/tempo/setDiscovery') as any; -/** internal static event builder */ export const $setEvents: unique symbol = Symbol.for('magmacomputing/tempo/setEvents') as any; -/** internal static period builder */ export const $setPeriods: unique symbol = Symbol.for('magmacomputing/tempo/setPeriods') as any; -/** internal static alias builder */ export const $setAliases: unique symbol = Symbol.for('magmacomputing/tempo/setAliases') as any; -/** internal static guard builder */ export const $buildGuard: unique symbol = Symbol.for('magmacomputing/tempo/buildGuard') as any; -/** internal static base class marker */ export const $IsBase: unique symbol = Symbol.for('magmacomputing/tempo/isBase') as any; +/** internal static config helper */ export const $setConfig: unique symbol = Symbol.for('magmacomputing/tempo/setConfig') as any; +/** internal static discovery helper */ export const $setDiscovery: unique symbol = Symbol.for('magmacomputing/tempo/setDiscovery') as any; +/** internal static event builder */ export const $setEvents: unique symbol = Symbol.for('magmacomputing/tempo/setEvents') as any; +/** internal static period builder */ export const $setPeriods: unique symbol = Symbol.for('magmacomputing/tempo/setPeriods') as any; +/** internal static alias builder */ export const $setAliases: unique symbol = Symbol.for('magmacomputing/tempo/setAliases') as any; +/** internal static guard builder */ export const $buildGuard: unique symbol = Symbol.for('magmacomputing/tempo/buildGuard') as any; +/** internal static base class marker */ export const $IsBase: unique symbol = Symbol.for('magmacomputing/tempo/isBase') as any; /** @internal Tempo Symbol Registry (Local Keys) */ const local = { diff --git a/packages/tempo/src/support/tempo.util.ts b/packages/tempo/src/support/support.util.ts similarity index 96% rename from packages/tempo/src/support/tempo.util.ts rename to packages/tempo/src/support/support.util.ts index 4de9b99..f282c65 100644 --- a/packages/tempo/src/support/tempo.util.ts +++ b/packages/tempo/src/support/support.util.ts @@ -1,13 +1,13 @@ import { isBoolean } from '#library/assertion.library.js'; -import { sym, Token } from './tempo.symbol.js'; +import { sym, Token } from './support.symbol.js'; import { asType } from '#library/type.library.js'; import { asArray } from '#library/coercion.library.js'; import { isSymbol, isUndefined, isDefined, isString, isRegExp, isNullish, isObject, isEmpty } from '#library/assertion.library.js'; import { ownEntries, ownKeys, unwrap } from '#library/primitive.library.js'; -import { getRuntime } from './tempo.runtime.js'; -import { Match, Snippet, Layout } from './tempo.default.js'; -import enums from './tempo.enum.js'; +import { getRuntime } from './support.runtime.js'; +import { Match, Snippet, Layout } from './support.default.js'; +import enums from './support.enum.js'; import type * as t from '../tempo.type.js'; /** @internal normalize layout-order options into a clean string array */ diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index f243c38..6d064b2 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -23,9 +23,9 @@ import type { TermPlugin, Plugin } from './plugin/plugin.type.js'; import { AliasEngine } from './engine/engine.alias.js'; import { PatternCompiler } from './engine/engine.pattern.js'; -import { resolveMonthDay, setProperty, proto, hasOwn, normalizeLayoutOrder } from './support/tempo.util.js'; +import { resolveMonthDay, setProperty, proto, hasOwn, normalizeLayoutOrder } from './support/support.util.js'; import { DEFAULT_LAYOUT_CLASS, resolveLayoutOrder, getLayoutOrder } from './parse/parse.layout.js'; -import { datePattern } from './support/tempo.default.js'; +import { datePattern } from './support/support.default.js'; import { sym, markConfig, TermError, getRuntime, init, extendState, setPatterns, isTempo, registryUpdate, registryReset, onRegistryReset, Match, Token, Snippet, Layout, Event, Period, Ignore, Default, Guard, enums, STATE, DISCOVERY, $Internal, $setConfig, $logError, $logDebug, $Identity, $setEvents, $setPeriods, $setAliases, $buildGuard, $IsBase, $Tempo, $Register, $Logify, $errored, $dbg, $guard, $Discover, $setDiscovery } from '#tempo/support'; import * as t from './tempo.type.js'; // namespaced types (Tempo.*) @@ -885,7 +885,7 @@ export class Tempo { return Token[key as keyof typeof Token] ?? Symbol.for(`$Tempo.${key}`); } - /** @internal translates {layout} into an anchored, case-insensitive RegExp. */ + /** translates {layout} into an anchored, case-insensitive RegExp. */ static regexp(layout: string | RegExp, snippet?: Snippet) { const state = (this as any)[$Internal](); diff --git a/packages/tempo/src/tempo.type.ts b/packages/tempo/src/tempo.type.ts index dc18935..c8a7ef8 100644 --- a/packages/tempo/src/tempo.type.ts +++ b/packages/tempo/src/tempo.type.ts @@ -7,13 +7,13 @@ * Inside `tempo.class.ts` these are accessed via `import * as t`. */ -import { sym, type TempoBrand } from '#tempo/support/tempo.symbol.js'; -import * as enums from '#tempo/support/tempo.enum.js'; +import { sym, type TempoBrand } from '#tempo/support/support.symbol.js'; +import * as enums from '#tempo/support/support.enum.js'; import type { Logify } from '#library/logify.class.js'; -import type { Snippet, Layout, Event, Period, Ignore } from '#tempo/support/tempo.default.js'; +import type { Snippet, Layout, Event, Period, Ignore } from '#tempo/support/support.default.js'; import type { IntRange, NonOptional, Property, Plural, Prettify, TemporalObject, TypeValue, RegistryOption } from '#library/type.library.js'; import type { TermPlugin, Plugin } from '#tempo/plugin/plugin.type.js'; -import type { Token } from '#tempo/support/tempo.symbol.js'; +import type { Token } from '#tempo/support/support.symbol.js'; import type { Tempo } from '#tempo/tempo.class.js'; import { AliasEngine } from './engine/engine.alias.js'; import type { PatternCompiler } from './engine/engine.pattern.js'; diff --git a/packages/tempo/test/engine/parse.layout.test.ts b/packages/tempo/test/engine/parse.layout.test.ts index a9a8779..1f17582 100644 --- a/packages/tempo/test/engine/parse.layout.test.ts +++ b/packages/tempo/test/engine/parse.layout.test.ts @@ -11,7 +11,7 @@ const makeLayout = (names: string[]) => const orderOf = (layout: Record) => Reflect.ownKeys(layout).map(key => (key as symbol).description); -describe('engine.layout resolver', () => { +describe('engine.resolver', () => { test('no-op when no swap pair matches', () => { const layout = makeLayout(['x', 'y', 'z']); const resolved = resolveLayoutOrder({ diff --git a/packages/tempo/test/engine/pattern_compiler_optimization.test.ts b/packages/tempo/test/engine/pattern_compiler_optimization.test.ts new file mode 100644 index 0000000..e02b1d1 --- /dev/null +++ b/packages/tempo/test/engine/pattern_compiler_optimization.test.ts @@ -0,0 +1,44 @@ +import { Tempo } from '#tempo'; +import { $Internal } from '#tempo/support'; +import { PatternCompiler } from '../../src/engine/engine.pattern.js'; +import { getSymbol } from '../../src/support/support.util.js'; + +describe('PatternCompiler Optimization and Safety', () => { + test('should expand nested placeholders correctly using the hoisted BRACES_REGEX', () => { + const state = (Tempo as any)[$Internal](); + const compiler = new PatternCompiler({ state }); + + // Define some snippets in the state + state.parse.snippet[getSymbol('a')] = { source: 'A{b}' }; + state.parse.snippet[getSymbol('b')] = { source: 'B{c}' }; + state.parse.snippet[getSymbol('c')] = { source: 'C' }; + + const result = compiler.compileRegExp('{a}'); + expect(result.source).toBe('^(ABC)$'); + }); + + test('should handle circular references by hitting recursion limit', () => { + const state = (Tempo as any)[$Internal](); + const compiler = new PatternCompiler({ state }); + + // Circular reference: a -> b -> a + state.parse.snippet[getSymbol('a')] = { source: 'A{b}' }; + state.parse.snippet[getSymbol('b')] = { source: 'B{a}' }; + + // This should hit the recursion limit (d > 10) and return the partially expanded string + const result = compiler.compileRegExp('{a}'); + // Expanding {a} -> A{b} -> AB{a} -> ABA{b} ... + // It should stop and return the source at depth 11 + expect(result.source).toContain('ABABAB'); + }); + + test('should handle multiple occurrences of placeholders in a single string', () => { + const state = (Tempo as any)[$Internal](); + const compiler = new PatternCompiler({ state }); + + state.parse.snippet[getSymbol('x')] = { source: 'X' }; + + const result = compiler.compileRegExp('{x}-{x}-{x}'); + expect(result.source).toBe('^(X-X-X)$'); + }); +}); diff --git a/packages/tempo/test/plugins/plugin_registration.test.ts b/packages/tempo/test/plugins/plugin_registration.test.ts index 631c81c..b2314bb 100644 --- a/packages/tempo/test/plugins/plugin_registration.test.ts +++ b/packages/tempo/test/plugins/plugin_registration.test.ts @@ -1,5 +1,5 @@ import { Tempo } from '#tempo'; -import { getRuntime } from '#tempo/support/tempo.runtime.js'; +import { getRuntime } from '#tempo/support/support.runtime.js'; import { TickerModule } from '#tempo/ticker'; describe('Ticker Registration / Initialization', () => { From 6d4f30f5a28b866e90454d745ff59f5de405f37a Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Thu, 7 May 2026 13:20:36 +1000 Subject: [PATCH 07/16] rm parse/ --- packages/tempo/doc/tempo.layout.md | 2 +- packages/tempo/package.json | 6 -- packages/tempo/src/engine/engine.layout.ts | 19 +++- packages/tempo/src/engine/engine.pattern.ts | 2 +- packages/tempo/src/parse/parse.layout.ts | 91 ------------------- packages/tempo/src/support/support.util.ts | 8 +- packages/tempo/src/tempo.class.ts | 2 +- packages/tempo/src/tsconfig.json | 1 - .../tempo/test/engine/parse.layout.test.ts | 2 +- .../pattern_compiler_optimization.test.ts | 23 +++-- packages/tempo/vitest.config.ts | 2 - 11 files changed, 37 insertions(+), 121 deletions(-) delete mode 100644 packages/tempo/src/parse/parse.layout.ts diff --git a/packages/tempo/doc/tempo.layout.md b/packages/tempo/doc/tempo.layout.md index 39e565d..d1fecea 100644 --- a/packages/tempo/doc/tempo.layout.md +++ b/packages/tempo/doc/tempo.layout.md @@ -90,7 +90,7 @@ You can use the static `Tempo.regexp()` method to "preview" how a layout string const regex = Tempo.regexp('{dd}{sep}{mm}{sep}{yy}'); console.log(regex.source); -// Output: ^(?
...)(?:...)(?...)(?:...)(?...)$ +// Output (illustrative): ^((?
...)(?...)(?...)(?...)(?...))$ ``` --- diff --git a/packages/tempo/package.json b/packages/tempo/package.json index 0447ede..5785a3c 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -30,8 +30,6 @@ "**/plugin/extend/extend.*.ts", "dist/engine/engine.*.js", "src/engine/engine.*.ts", - "dist/parse/parse.*.js", - "src/parse/parse.*.ts", "dist/module/module.*.js", "src/module/module.*.ts", "dist/discrete/discrete.*.js", @@ -109,10 +107,6 @@ "development": "./src/discrete/discrete.parse.ts", "default": "./dist/discrete/discrete.parse.js" }, - "#tempo/parse/*.js": { - "development": "./src/parse/*.ts", - "default": "./dist/parse/*.js" - }, "#tempo/format": { "development": "./src/discrete/discrete.format.ts", "default": "./dist/discrete/discrete.format.js" diff --git a/packages/tempo/src/engine/engine.layout.ts b/packages/tempo/src/engine/engine.layout.ts index a70db66..97702f1 100644 --- a/packages/tempo/src/engine/engine.layout.ts +++ b/packages/tempo/src/engine/engine.layout.ts @@ -4,7 +4,7 @@ import { resolveLayoutOrderPure } from './engine.resolver.js'; import type * as t from '../tempo.type.js'; export type LayoutEntry = [symbol, string]; -export type LayoutController = Record; +export type LayoutController = Record; const TOKEN_ALIAS = new Map( (ownEntries(Token, true) as [string, symbol][]).map(([name, key]) => [key, name]) @@ -63,8 +63,15 @@ export function resolveLayoutClassificationOrder(layout: Record, const seen = new Set(); preferred.forEach(name => { - const resolvedName = TOKEN_DESCRIPTION_BY_NAME.get(name) ?? name; - const entry = byName.get(resolvedName) ?? byName.get(name); + const isSym = typeof name === 'symbol'; + const description = isSym ? (name.description ?? '') : ''; + const alias = isSym ? TOKEN_ALIAS.get(name) : undefined; + + const resolvedName = !isSym ? (TOKEN_DESCRIPTION_BY_NAME.get(name) ?? name) : undefined; + const entry = isSym + ? (byName.get(description) ?? (alias ? byName.get(alias) : undefined)) + : (byName.get(resolvedName!) ?? byName.get(name)); + if (!entry) return; if (seen.has(entry[0])) return; seen.add(entry[0]); @@ -95,8 +102,12 @@ export function resolveLayoutOrder({ layout, monthDayLayouts, isMonthDay, layout classification ?? DEFAULT_LAYOUT_CLASS, ); + const entries = ownEntries(ordered) as LayoutEntry[]; const layouts = resolveLayoutOrderPure(ordered, monthDayLayouts, isMonthDay); - const changed = layouts.some((entry, idx) => entry[0] !== (ownEntries(ordered)[idx] as LayoutEntry)[0]); + + if (layouts.length !== entries.length) return ordered; + + const changed = layouts.some((entry, idx) => entry[0] !== entries[idx][0]); if (changed) return Object.fromEntries(layouts) as Record; return ordered; diff --git a/packages/tempo/src/engine/engine.pattern.ts b/packages/tempo/src/engine/engine.pattern.ts index 3246ead..9411edf 100644 --- a/packages/tempo/src/engine/engine.pattern.ts +++ b/packages/tempo/src/engine/engine.pattern.ts @@ -47,7 +47,7 @@ export class PatternCompiler { const matcher = (source: string, d = 0): string => { if (d > 10) { // Emit a diagnostic if recursion limit is hit (likely circular placeholder) - logWarn?.(this.#state?.config, `[PatternCompiler] Recursion limit exceeded in matcher (d > 10) for src:`, source, `depth:`, d); + logWarn(this.#state.config, `[PatternCompiler] Recursion limit exceeded in matcher (d > 10) for src:`, source, `depth:`, d); return source; } diff --git a/packages/tempo/src/parse/parse.layout.ts b/packages/tempo/src/parse/parse.layout.ts deleted file mode 100644 index 08b034d..0000000 --- a/packages/tempo/src/parse/parse.layout.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { ownEntries } from '#library/primitive.library.js'; -import { Token } from '#tempo/support/support.symbol.js'; -import { resolveLayoutOrderPure } from '../engine/engine.resolver.js'; -import type * as t from '../tempo.type.js'; - -export type LayoutEntry = [symbol, string]; -export type LayoutController = Record; - -const TOKEN_ALIAS = new Map( - (ownEntries(Token, true) as [string, symbol][]).map(([name, key]) => [key, name]) -); - -const TOKEN_DESCRIPTION_BY_NAME = new Map( - (ownEntries(Token, true) as [string, symbol][]) - .map(([name, key]) => [name, key.description ?? ''] as const) - .filter(([, description]) => description.length > 0) -); - -export const DEFAULT_LAYOUT_CLASS: unique symbol = Symbol('default'); - -export interface ResolveLayoutOrderArgs { - layout: Record; - monthDayLayouts: t.LayoutPair[] | readonly t.LayoutPair[]; - isMonthDay: boolean; - layoutController?: LayoutController; - classification?: PropertyKey; -} - -export function createLayoutController(layout: Record): LayoutController { - return { - [DEFAULT_LAYOUT_CLASS]: getLayoutOrder(layout), - } -} - -export function resolveLayoutClassificationOrder(layout: Record, controller: LayoutController, classification: PropertyKey = DEFAULT_LAYOUT_CLASS): Record { - const preferred = controller[classification] ?? []; - if (preferred.length === 0) return layout; - - const entries = ownEntries(layout) as LayoutEntry[]; - const byName = new Map(); - entries.forEach(([key, value]) => { - const description = key.description ?? ''; - if (description) byName.set(description, [key, value]); - const alias = TOKEN_ALIAS.get(key); - if (alias) byName.set(alias, [key, value]); - }); - const next: LayoutEntry[] = []; - const seen = new Set(); - - preferred.forEach(name => { - const isSym = typeof name === 'symbol'; - const description = isSym ? (name.description ?? '') : ''; - const alias = isSym ? TOKEN_ALIAS.get(name) : undefined; - - const resolvedName = !isSym ? (TOKEN_DESCRIPTION_BY_NAME.get(name) ?? name) : undefined; - const entry = isSym - ? (byName.get(description) ?? (alias ? byName.get(alias) : undefined)) - : (byName.get(resolvedName!) ?? byName.get(name)); - - if (!entry) return; - if (seen.has(entry[0])) return; - seen.add(entry[0]); - next.push(entry); - }); - - entries.forEach(entry => { - if (!seen.has(entry[0])) next.push(entry); - }); - - const changed = next.length === entries.length && next.some((entry, idx) => entry[0] !== entries[idx][0]); - return changed ? Object.fromEntries(next) as Record : layout; -} - -export function resolveLayoutOrder({ layout, monthDayLayouts, isMonthDay, layoutController, classification }: ResolveLayoutOrderArgs): Record { - const ordered = resolveLayoutClassificationOrder( - layout, - layoutController ?? createLayoutController(layout), - classification ?? DEFAULT_LAYOUT_CLASS, - ); - - const layouts = resolveLayoutOrderPure(ordered, monthDayLayouts, isMonthDay); - const changed = layouts.some((entry, idx) => entry[0] !== (ownEntries(ordered)[idx] as LayoutEntry)[0]); - - if (changed) return Object.fromEntries(layouts) as Record; - return ordered; -} - -export function getLayoutOrder(layout: Record): string[] { - return (ownEntries(layout) as LayoutEntry[]) - .map(([key]) => key.description ?? String(key)); -} diff --git a/packages/tempo/src/support/support.util.ts b/packages/tempo/src/support/support.util.ts index f282c65..859bfc3 100644 --- a/packages/tempo/src/support/support.util.ts +++ b/packages/tempo/src/support/support.util.ts @@ -62,7 +62,13 @@ export const proto = (obj: any): any => Object.getPrototypeOf(unwrap(obj)); /** @internal create a new shadowed object from a prototype */ export function create(obj: any, name: string): T { - const entry = proto(obj)[name]; + const prototype = proto(obj); + if (!isObject(prototype)) { + logError(null, `[Tempo#create] Failed to create shadowed object for '${name}'. Proto(obj) is null or not an object.`); + return {} as T; + } + + const entry = prototype[name]; if (!isObject(entry)) { logError(null, `[Tempo#create] Failed to create shadowed object for '${name}'. The prototype entry from proto(obj) is missing or not an object (received: ${typeof entry}).`); return {} as T; diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index 6d064b2..3004eb3 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -24,7 +24,7 @@ import type { TermPlugin, Plugin } from './plugin/plugin.type.js'; import { AliasEngine } from './engine/engine.alias.js'; import { PatternCompiler } from './engine/engine.pattern.js'; import { resolveMonthDay, setProperty, proto, hasOwn, normalizeLayoutOrder } from './support/support.util.js'; -import { DEFAULT_LAYOUT_CLASS, resolveLayoutOrder, getLayoutOrder } from './parse/parse.layout.js'; +import { DEFAULT_LAYOUT_CLASS, resolveLayoutOrder, getLayoutOrder } from './engine/engine.layout.js'; import { datePattern } from './support/support.default.js'; import { sym, markConfig, TermError, getRuntime, init, extendState, setPatterns, isTempo, registryUpdate, registryReset, onRegistryReset, Match, Token, Snippet, Layout, Event, Period, Ignore, Default, Guard, enums, STATE, DISCOVERY, $Internal, $setConfig, $logError, $logDebug, $Identity, $setEvents, $setPeriods, $setAliases, $buildGuard, $IsBase, $Tempo, $Register, $Logify, $errored, $dbg, $guard, $Discover, $setDiscovery } from '#tempo/support'; import * as t from './tempo.type.js'; // namespaced types (Tempo.*) diff --git a/packages/tempo/src/tsconfig.json b/packages/tempo/src/tsconfig.json index 24f4d1b..cdef1a9 100644 --- a/packages/tempo/src/tsconfig.json +++ b/packages/tempo/src/tsconfig.json @@ -22,7 +22,6 @@ "#tempo/ticker": ["./plugin/extend/extend.ticker.ts"], "#tempo/engine/*.js": ["./engine/*.ts"], "#tempo/module/*.js": ["./module/*.ts"], - "#tempo/parse/*.js": ["./parse/*.ts"], "#tempo/plugin/extend/*.js": ["./plugin/extend/*.ts"], "#tempo/plugin/term/*.js": ["./plugin/term/*.ts"], "#tempo/term/*": ["./plugin/term/term.*.ts"], diff --git a/packages/tempo/test/engine/parse.layout.test.ts b/packages/tempo/test/engine/parse.layout.test.ts index 1f17582..8af58ab 100644 --- a/packages/tempo/test/engine/parse.layout.test.ts +++ b/packages/tempo/test/engine/parse.layout.test.ts @@ -3,7 +3,7 @@ import { createLayoutController, resolveLayoutClassificationOrder, resolveLayoutOrder, -} from '#tempo/parse/parse.layout.js'; +} from '#tempo/engine/engine.layout.js'; const makeLayout = (names: string[]) => Object.fromEntries(names.map(name => [Symbol(name), name])) as Record; diff --git a/packages/tempo/test/engine/pattern_compiler_optimization.test.ts b/packages/tempo/test/engine/pattern_compiler_optimization.test.ts index e02b1d1..72de435 100644 --- a/packages/tempo/test/engine/pattern_compiler_optimization.test.ts +++ b/packages/tempo/test/engine/pattern_compiler_optimization.test.ts @@ -1,13 +1,19 @@ -import { Tempo } from '#tempo'; -import { $Internal } from '#tempo/support'; +import { $Internal, TempoRuntime, init } from '#tempo/support'; import { PatternCompiler } from '../../src/engine/engine.pattern.js'; import { getSymbol } from '../../src/support/support.util.js'; describe('PatternCompiler Optimization and Safety', () => { - test('should expand nested placeholders correctly using the hoisted BRACES_REGEX', () => { - const state = (Tempo as any)[$Internal](); - const compiler = new PatternCompiler({ state }); + let state: any; + let compiler: PatternCompiler; + + beforeEach(() => { + const runtime = TempoRuntime.createScoped(); + state = init({}, false); + runtime.state = state; + compiler = new PatternCompiler({ state }); + }); + test('should expand nested placeholders correctly using the hoisted BRACES_REGEX', () => { // Define some snippets in the state state.parse.snippet[getSymbol('a')] = { source: 'A{b}' }; state.parse.snippet[getSymbol('b')] = { source: 'B{c}' }; @@ -18,9 +24,6 @@ describe('PatternCompiler Optimization and Safety', () => { }); test('should handle circular references by hitting recursion limit', () => { - const state = (Tempo as any)[$Internal](); - const compiler = new PatternCompiler({ state }); - // Circular reference: a -> b -> a state.parse.snippet[getSymbol('a')] = { source: 'A{b}' }; state.parse.snippet[getSymbol('b')] = { source: 'B{a}' }; @@ -28,14 +31,10 @@ describe('PatternCompiler Optimization and Safety', () => { // This should hit the recursion limit (d > 10) and return the partially expanded string const result = compiler.compileRegExp('{a}'); // Expanding {a} -> A{b} -> AB{a} -> ABA{b} ... - // It should stop and return the source at depth 11 expect(result.source).toContain('ABABAB'); }); test('should handle multiple occurrences of placeholders in a single string', () => { - const state = (Tempo as any)[$Internal](); - const compiler = new PatternCompiler({ state }); - state.parse.snippet[getSymbol('x')] = { source: 'X' }; const result = compiler.compileRegExp('{x}-{x}-{x}'); diff --git a/packages/tempo/vitest.config.ts b/packages/tempo/vitest.config.ts index 3605aec..ee931ee 100644 --- a/packages/tempo/vitest.config.ts +++ b/packages/tempo/vitest.config.ts @@ -44,7 +44,6 @@ export default defineConfig({ { find: /^#tempo\/plugin\/plugin\.(.*)\.js$/, replacement: resolve(__dirname, './dist/plugin/plugin.$1.js') }, { find: /^#tempo\/plugin\/extend\/(.*)\.js$/, replacement: resolve(__dirname, './dist/plugin/extend/$1.js') }, { find: /^#tempo\/engine\/(.*)\.js$/, replacement: resolve(__dirname, './dist/engine/$1.js') }, - { find: /^#tempo\/parse\/(.*)\.js$/, replacement: resolve(__dirname, './dist/parse/$1.js') }, { find: /^#tempo\/module\/(.*)\.js$/, replacement: resolve(__dirname, './dist/module/$1.js') }, { find: /^#tempo\/plugin\/term\/(.*)\.js$/, replacement: resolve(__dirname, './dist/plugin/term/$1.js') }, { find: /^#tempo\/support$/, replacement: resolve(__dirname, './dist/support/support.index.js') }, @@ -65,7 +64,6 @@ export default defineConfig({ { find: /^#tempo\/plugin\/plugin\.(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/plugin.$1.ts') }, { find: /^#tempo\/plugin\/extend\/(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/extend/$1.ts') }, { find: /^#tempo\/engine\/(.*)\.js$/, replacement: resolve(__dirname, './src/engine/$1.ts') }, - { find: /^#tempo\/parse\/(.*)\.js$/, replacement: resolve(__dirname, './src/parse/$1.ts') }, { find: /^#tempo\/module\/(.*)\.js$/, replacement: resolve(__dirname, './src/module/$1.ts') }, { find: /^#tempo\/plugin\/term\/(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/term/$1.ts') }, { find: /^#tempo\/support$/, replacement: resolve(__dirname, './src/support/support.index.ts') }, From 9f74183cfb0c64abacbc7fa9b70f648f0737ac10 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Thu, 7 May 2026 13:43:21 +1000 Subject: [PATCH 08/16] PR 4th review --- packages/tempo/doc/installation.md | 16 ++++++++++++++-- packages/tempo/src/engine/engine.layout.ts | 11 ++++++----- packages/tempo/src/engine/engine.pattern.ts | 4 ++-- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/tempo/doc/installation.md b/packages/tempo/doc/installation.md index 4c6c498..05b4d7b 100644 --- a/packages/tempo/doc/installation.md +++ b/packages/tempo/doc/installation.md @@ -8,7 +8,11 @@ `Temporal` is now at Stage 4 and is expected to land broadly in runtimes soon. To avoid needlessly inflating package size with a dependency that will increasingly become unnecessary, `Tempo` does not bundle a `Temporal` polyfill by default. -As of 13 January 2026, Chrome 144 has shipped `Temporal`, and Firefox 139 also includes native `Temporal` support, while Node.js still does not provide built-in `Temporal` globally. Please verify support in your actual target runtime(s) and add a polyfill only when needed. +As of 13 January 2026, Chrome 144 has shipped `Temporal`, and Firefox 139 also includes native `Temporal` support. + +While Node.js does not yet enable `Temporal` by default, recent versions (Node 20+) support it via the `--harmony-temporal` flag. This allows you to use `Tempo` without an external polyfill package. + +Please verify support in your actual target runtime(s) and add a polyfill only when needed. You can check at runtime with a simple guard: @@ -39,7 +43,15 @@ import { Tempo } from '@magmacomputing/tempo'; const t = new Tempo('next Friday'); ``` -### Node.js quick-start (if Temporal is not available) +### Node.js (with Native Temporal) + +If you are using Node.js 20+, you can enable native `Temporal` support without installing a polyfill: + +```bash +node --harmony-temporal my-app.js +``` + +### Node.js (with Polyfill) The polyfill import shown here is conditional guidance, not required for all environments. diff --git a/packages/tempo/src/engine/engine.layout.ts b/packages/tempo/src/engine/engine.layout.ts index 97702f1..4ca00e4 100644 --- a/packages/tempo/src/engine/engine.layout.ts +++ b/packages/tempo/src/engine/engine.layout.ts @@ -52,12 +52,13 @@ export function resolveLayoutClassificationOrder(layout: Record, if (preferred.length === 0) return layout; const entries = ownEntries(layout) as LayoutEntry[]; - const byName = new Map(); + const lookup = new Map(); entries.forEach(([key, value]) => { + lookup.set(key, [key, value]); const description = key.description ?? ''; - if (description) byName.set(description, [key, value]); + if (description) lookup.set(description, [key, value]); const alias = TOKEN_ALIAS.get(key); - if (alias) byName.set(alias, [key, value]); + if (alias) lookup.set(alias, [key, value]); }); const next: LayoutEntry[] = []; const seen = new Set(); @@ -69,8 +70,8 @@ export function resolveLayoutClassificationOrder(layout: Record, const resolvedName = !isSym ? (TOKEN_DESCRIPTION_BY_NAME.get(name) ?? name) : undefined; const entry = isSym - ? (byName.get(description) ?? (alias ? byName.get(alias) : undefined)) - : (byName.get(resolvedName!) ?? byName.get(name)); + ? (lookup.get(name) ?? lookup.get(description) ?? (alias ? lookup.get(alias) : undefined)) + : (lookup.get(resolvedName!) ?? lookup.get(name)); if (!entry) return; if (seen.has(entry[0])) return; diff --git a/packages/tempo/src/engine/engine.pattern.ts b/packages/tempo/src/engine/engine.pattern.ts index 9411edf..aca9bb0 100644 --- a/packages/tempo/src/engine/engine.pattern.ts +++ b/packages/tempo/src/engine/engine.pattern.ts @@ -160,8 +160,8 @@ export class PatternCompiler { */ export function setPatterns(state: t.Internal.State) { // 🛡️ Critical fix: ensure we use an OWN PatternCompiler for each state to avoid cross-pollution - if (!hasOwn(state, 'patternCompiler')) + if (!hasOwn(state, 'patternCompiler') || !state.patternCompiler) state.patternCompiler = new PatternCompiler({ state }); - state.patternCompiler!.setPatterns(); + state.patternCompiler.setPatterns(); } From 0bec2ad72b1fe4e872692289dd7e18c47e0865ee Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Thu, 7 May 2026 14:49:41 +1000 Subject: [PATCH 09/16] 1st pass Guard Builder --- package.json | 4 +- packages/library/package.json | 2 +- packages/tempo/CHANGELOG.md | 20 ++++++ packages/tempo/package.json | 4 +- packages/tempo/plan/RELEASE-D.md | 11 +-- packages/tempo/src/engine/engine.guard.ts | 82 +++++++++++++++++++++++ packages/tempo/src/tempo.class.ts | 67 +----------------- 7 files changed, 116 insertions(+), 74 deletions(-) create mode 100644 packages/tempo/src/engine/engine.guard.ts diff --git a/package.json b/package.json index 0e2644a..7767a05 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tempo-monorepo", - "version": "2.9.1", + "version": "2.9.2", "private": true, "description": "Magma Computing Monorepo", "repository": { @@ -41,4 +41,4 @@ "overrides": { "esbuild": "^0.25.0" } -} +} \ No newline at end of file diff --git a/packages/library/package.json b/packages/library/package.json index 789c165..3cc5edf 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/library", - "version": "2.9.1", + "version": "2.9.2", "description": "Shared utility library for Tempo", "author": "Magma Computing Solutions", "license": "MIT", diff --git a/packages/tempo/CHANGELOG.md b/packages/tempo/CHANGELOG.md index 10d538a..3a18a65 100644 --- a/packages/tempo/CHANGELOG.md +++ b/packages/tempo/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.9.2] - 2026-05-07 + +### Added +- **Identity-Based Layout Resolution**: Hardened `resolveLayoutClassificationOrder` to support identity-based symbol lookups. This ensures that tokens without descriptions or aliases (such as raw symbols) can be correctly prioritized in preferred layout ordering. +- **Named Capture for Separators**: Updated the default `{sep}` snippet to use a named capture group `(?...)`, improving the inspectability of generated regex patterns. + +### Changed +- **Modular Decompression**: Removed the redundant `parse.layout.ts` re-export module and consolidated all layout resolution logic into `engine.layout.ts`. Updated internal Specifiers and test-aliases to point to the new canonical home. +- **Node.js Harmony Support**: Updated documentation to highlight native `Temporal` support in Node.js 20+ via the `--harmony-temporal` flag, reducing the need for external polyfills in modern server-side environments. + +### Fixed +- **Utility Security Hardening**: Refactored the `create` and `setPatterns` utilities with robust prototype-shadowing guards. These improvements prevent `TypeError` crashes when interacting with null-prototype objects and guarantee `PatternCompiler` state isolation across concurrent Tempo instances. +- **RegExp Preview Accuracy**: Corrected the documentation example for `Tempo.regexp()` to accurately reflect the anchored outer capture group and named snippet expansions produced by the engine. + +## [2.9.1] - 2026-05-07 + +### Fixed +- **Support Utility Consolidation**: Completed the rename and migration of internal support utilities to the `@packages/tempo/src/support/` directory. +- **Pattern Compiler isolated test state**: Fixed state-leakage in `pattern_compiler_optimization.test.ts` by implementing `TempoRuntime.createScoped()` and `init({}, false)` within `beforeEach` hooks. + ## [2.9.0] - 2026-05-06 ### Added diff --git a/packages/tempo/package.json b/packages/tempo/package.json index 5785a3c..1ac7707 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/tempo", - "version": "2.9.1", + "version": "2.9.2", "description": "The Tempo core library", "author": "Magma Computing Solutions", "license": "MIT", @@ -224,7 +224,7 @@ }, "devDependencies": { "@js-temporal/polyfill": "^0.5.1", - "@magmacomputing/library": "2.9.1", + "@magmacomputing/library": "2.9.2", "@rollup/plugin-alias": "^6.0.0", "cross-env": "^7.0.3", "magic-string": "^0.30.21", diff --git a/packages/tempo/plan/RELEASE-D.md b/packages/tempo/plan/RELEASE-D.md index 54e8017..4567304 100644 --- a/packages/tempo/plan/RELEASE-D.md +++ b/packages/tempo/plan/RELEASE-D.md @@ -21,11 +21,12 @@ This release focuses on modularizing and refactoring the parsing and pattern-mat - [x] Add/expand unit tests for alias/collision - [x] Update documentation and references -### Guard Builder Extraction (Assessment) -- [ ] Identify all guard-building/token-ingestion logic -- [ ] Assess complexity/reuse for extraction -- [ ] Outline module boundaries if justified -- [ ] Document reasons if not extracted +### Guard Builder Extraction +- [x] Identify all guard-building/token-ingestion logic +- [x] Assess complexity/reuse for extraction +- [x] Outline module boundaries if justified +- [x] Move "Scan-and-Consume" logic to `engine.guard.ts` +- [x] Refactor `tempo.class.ts` to use `createMasterGuard` ### Parse Result Normalizer Extraction (Assessment) - [ ] Identify all result normalization/trace logic diff --git a/packages/tempo/src/engine/engine.guard.ts b/packages/tempo/src/engine/engine.guard.ts new file mode 100644 index 0000000..19697b8 --- /dev/null +++ b/packages/tempo/src/engine/engine.guard.ts @@ -0,0 +1,82 @@ +import { isString, isSymbol } from '#library/assertion.library.js'; +import { Match } from '#tempo/support/support.default.js'; + +/** + * Interface for the Master Guard scanner. + */ +export interface MasterGuard { + test(input: string): boolean; +} + +/** + * Create a new Master Guard scanner based on a list of allowed tokens. + * + * @param words - List of strings or symbols that are valid in the current context. + * @returns An object with a .test() method that performs a greedy scan-and-consume validation. + */ +export function createMasterGuard(words: (string | symbol)[]): MasterGuard { + const wordsList = words + .filter(w => isString(w) || isSymbol(w)) + .map(w => (isSymbol(w) ? w.description : (w as string))!.toLowerCase()) + .filter(Boolean); + + const allowedTokens = new Set(wordsList); + + let maxT = 0; + for (const w of wordsList) if (w.length > maxT) maxT = w.length; + const maxTokenLength = maxT; + + return { + test(input: string): boolean { + if (!input || typeof input !== 'string') return false; + + let i = 0; + const len = input.length; + + while (i < len) { + const char = input[i]; + + // 1. Skip spaces + if (char === ' ' || char === '\n' || char === '\t' || char === '\r') { + i++; + continue; + } + + // 2. Try Bracket match (starts with [) + if (char === '[') { + const sub = input.substring(i); + const match = sub.match(Match.bracket); + if (match && match.index === 0) { + i += match[0].length; + continue; + } + } + + // 3. Try Longest Token match from Set + let matched = false; + const searchLen = Math.min(maxTokenLength, len - i); + const slice = input.substring(i, i + searchLen).toLowerCase(); + + for (let l = searchLen; l > 0; l--) { + const candidate = slice.substring(0, l); + if (allowedTokens.has(candidate)) { + i += l; + matched = true; + break; + } + } + if (matched) continue; + + // 4. Try Fallback char (Match.guard) + if (Match.guard.test(char)) { + i++; + continue; + } + + return false; // No valid match at current position + } + + return true; + } + }; +} diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index 3004eb3..67ae44b 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -23,6 +23,7 @@ import type { TermPlugin, Plugin } from './plugin/plugin.type.js'; import { AliasEngine } from './engine/engine.alias.js'; import { PatternCompiler } from './engine/engine.pattern.js'; +import { createMasterGuard } from './engine/engine.guard.js'; import { resolveMonthDay, setProperty, proto, hasOwn, normalizeLayoutOrder } from './support/support.util.js'; import { DEFAULT_LAYOUT_CLASS, resolveLayoutOrder, getLayoutOrder } from './engine/engine.layout.js'; import { datePattern } from './support/support.default.js'; @@ -93,7 +94,6 @@ export class Tempo { /** mapping of terms to their resolved values */ static #termMap: Map = new Map(); /** flag to prevent recursion during init */ static #lifecycle = { bootstrap: true, initialising: false, extendDepth: 0, ready: false }; /** Master Guard predicate (implements RegExp-like interface) */static #guard: { test(str: string): boolean } = { test: () => true }; - /** Set of allowed lowercased tokens for the Master Guard */ static #allowedTokens: Set = new Set(); static [$IsBase] = true; @@ -430,70 +430,9 @@ export class Tempo { ...Tempo.#terms.map(t => t.key), ...Tempo.#terms.map(t => t.scope), ...Guard - ].filter(w => isString(w) || isSymbol(w)) - .map(w => (isSymbol(w) ? w.description : (w as string))!.toLowerCase()) - .filter(Boolean); + ]; - Tempo.#allowedTokens = new Set(wordsList); - - let maxT = 0; - for (const w of wordsList) if (w.length > maxT) maxT = w.length; - const maxTokenLength = maxT; - - // Define the custom guard logic (Scan-and-Consume) - Tempo.#guard = { - test(input: string): boolean { - if (!input || typeof input !== 'string') return false; - - let i = 0; - const len = input.length; - - while (i < len) { - const char = input[i]; - - // 1. Skip spaces - if (char === ' ' || char === '\n' || char === '\t' || char === '\r') { - i++; - continue; - } - - // 2. Try Bracket match (starts with [) - if (char === '[') { - const sub = input.substring(i); - const match = sub.match(Match.bracket); - if (match && match.index === 0) { - i += match[0].length; - continue; - } - } - - // 3. Try Longest Token match from Set - let matched = false; - const searchLen = Math.min(maxTokenLength, len - i); - const slice = input.substring(i, i + searchLen).toLowerCase(); - - for (let l = searchLen; l > 0; l--) { - const candidate = slice.substring(0, l); - if (Tempo.#allowedTokens.has(candidate)) { - i += l; - matched = true; - break; - } - } - if (matched) continue; - - // 4. Try Fallback char (Match.guard) - if (Match.guard.test(char)) { - i++; - continue; - } - - return false; // No valid match at current position - } - - return true; - } - } + Tempo.#guard = createMasterGuard(wordsList); if ((this as any)[$Internal]() === Tempo.#global) { setPatterns((this as any)[$Internal]()); From f2f2a86e434e56585c13485423f56379b62db4fa Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Thu, 7 May 2026 15:03:41 +1000 Subject: [PATCH 10/16] Guard Builder --- packages/tempo/doc/architecture.md | 8 +-- packages/tempo/src/engine/engine.guard.ts | 6 +- .../tempo/test/engine/engine.guard.test.ts | 62 +++++++++++++++++++ 3 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 packages/tempo/test/engine/engine.guard.test.ts diff --git a/packages/tempo/doc/architecture.md b/packages/tempo/doc/architecture.md index ff377c6..9495c03 100644 --- a/packages/tempo/doc/architecture.md +++ b/packages/tempo/doc/architecture.md @@ -124,13 +124,13 @@ For more implementation details, see [Soft Freeze Strategy](./soft_freeze_strate ## ⚡ 3. Master Guard (Guarded-Lazy Strategy) Used for: `new Tempo(string | number)` -The **Guarded-Lazy** strategy ensures that even with hundreds of custom plugins, the entry point remains nearly instantaneous. In **v2.0.1**, this was refined for 100% matching reliability. +The **Guarded-Lazy** strategy ensures that even with hundreds of custom plugins, the entry point remains nearly instantaneous. As of **v2.9.2**, this logic is decoupled into a dedicated `engine.guard.ts` module. ### How it works: -1. **Longest-Token Matching**: To prevent partial matching (e.g., matching `qtr` inside `quarter`), the guard uses a "Scan-and-Consume" loop that prioritizes the longest available token. +1. **Longest-Token Matching**: To prevent partial matching (e.g., matching `qtr` inside `quarter`), the guard uses a greedy "Scan-and-Consume" loop that prioritizes the longest available token. 2. **Unified Wordlist**: The guard automatically ingests all registered Terms, Timezones, Month names, and Custom Events into a single high-speed lookup Set. -3. **High-Speed Gatekeeper**: By avoiding complex backtracking regexes, the gatekeeper provides predictable $O(1)$ performance even as the plugin list grows. -4. **Versioned Registry (v2.9.0)**: To avoid redundant wordlist rebuilding, the Guard now monitors a `#version` counter on the alias registry. The wordlist is only rebuilt when a mutation actually occurs. +3. **High-Speed Gatekeeper**: By avoiding complex backtracking regexes, the gatekeeper provides predictable $O(1)$ performance regardless of how many plugins are registered. +4. **Versioned Registry**: To avoid redundant wordlist rebuilding, the Guard monitors a version counter on the alias registry. The wordlist is only rebuilt when a mutation actually occurs. 5. **Auto-Lazy**: Valid inputs that pass the guard automatically switch the instance to `mode: 'defer'`, deferring the full $O(N)$ parse work until a property is actually read. --- diff --git a/packages/tempo/src/engine/engine.guard.ts b/packages/tempo/src/engine/engine.guard.ts index 19697b8..8fd5317 100644 --- a/packages/tempo/src/engine/engine.guard.ts +++ b/packages/tempo/src/engine/engine.guard.ts @@ -28,7 +28,7 @@ export function createMasterGuard(words: (string | symbol)[]): MasterGuard { return { test(input: string): boolean { - if (!input || typeof input !== 'string') return false; + if (!input || !isString(input)) return false; let i = 0; const len = input.length; @@ -73,10 +73,10 @@ export function createMasterGuard(words: (string | symbol)[]): MasterGuard { continue; } - return false; // No valid match at current position + return false; // No valid match at current position } return true; } - }; + } } diff --git a/packages/tempo/test/engine/engine.guard.test.ts b/packages/tempo/test/engine/engine.guard.test.ts new file mode 100644 index 0000000..73297a7 --- /dev/null +++ b/packages/tempo/test/engine/engine.guard.test.ts @@ -0,0 +1,62 @@ +import { createMasterGuard } from '#tempo/engine/engine.guard.js'; + +describe('engine.guard (Master Guard)', () => { + it('should permit tokens from the provided word list', () => { + const guard = createMasterGuard(['apple', 'banana', 'cherry']); + expect(guard.test('apple')).toBe(true); + expect(guard.test('banana')).toBe(true); + expect(guard.test('cherry')).toBe(true); + expect(guard.test('apple banana')).toBe(true); + }); + + it('should reject unrecognized tokens', () => { + const guard = createMasterGuard(['apple', 'banana']); + expect(guard.test('apple grape')).toBe(false); + expect(guard.test('date')).toBe(false); + }); + + it('should handle greedy longest-token matching', () => { + const guard = createMasterGuard(['jan', 'january']); + // Should consume 'january' as one token, not 'jan' + 'uary' (which would fail) + expect(guard.test('january')).toBe(true); + expect(guard.test('jan')).toBe(true); + }); + + it('should skip valid bracketed content', () => { + const guard = createMasterGuard(['apple']); + // [any content] should be skipped by the bracket matcher + expect(guard.test('apple [random text]')).toBe(true); + expect(guard.test('[2026] apple')).toBe(true); + }); + + it('should permit fallback characters (digits and punctuation)', () => { + const guard = createMasterGuard(['utc']); + // Digits, hyphens, colons, and dots are usually allowed by Match.guard + expect(guard.test('2026-05-07 utc 13:00')).toBe(true); + }); + + it('should be case-insensitive', () => { + const guard = createMasterGuard(['Monday']); + expect(guard.test('monday')).toBe(true); + expect(guard.test('MONDAY')).toBe(true); + expect(guard.test('MonDay')).toBe(true); + }); + + it('should ignore various whitespace characters', () => { + const guard = createMasterGuard(['token']); + expect(guard.test('token\n \t\r token')).toBe(true); + }); + + it('should handle symbols in the word list', () => { + const sym = Symbol.for('test.token'); + const guard = createMasterGuard(['apple', sym]); + expect(guard.test('apple test.token')).toBe(true); + }); + + it('should fail on empty or non-string input', () => { + const guard = createMasterGuard(['apple']); + expect(guard.test('')).toBe(false); + expect(guard.test(null as any)).toBe(false); + expect(guard.test(123 as any)).toBe(false); + }); +}); From 8099d824115e20369a34e18ad713d6a5c8548e9b Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Thu, 7 May 2026 17:24:38 +1000 Subject: [PATCH 11/16] Guard PR review #2 --- doc/main_branch_protection.md | 10 +- .../library/src/common/assertion.library.ts | 8 +- .../library/src/common/temporal.library.ts | 37 +++- packages/tempo/doc/tempo.layout.md | 2 +- packages/tempo/index.md | 7 +- packages/tempo/plan/RELEASE-D.md | 10 +- packages/tempo/src/discrete/discrete.parse.ts | 203 +++-------------- packages/tempo/src/engine/engine.alias.ts | 14 +- packages/tempo/src/engine/engine.composer.ts | 11 +- packages/tempo/src/engine/engine.guard.ts | 12 +- .../tempo/src/engine/engine.normalizer.ts | 205 ++++++++++++++++++ packages/tempo/src/engine/engine.pattern.ts | 2 + packages/tempo/src/engine/engine.term.ts | 5 +- packages/tempo/src/module/module.duration.ts | 14 +- packages/tempo/src/support/support.default.ts | 1 + packages/tempo/src/tempo.class.ts | 14 +- packages/tempo/test/core/tempo_guard.test.ts | 7 +- .../tempo/test/engine/engine.guard.test.ts | 1 + .../tempo/test/support/setup.console-spy.ts | 10 +- 19 files changed, 338 insertions(+), 235 deletions(-) create mode 100644 packages/tempo/src/engine/engine.normalizer.ts diff --git a/doc/main_branch_protection.md b/doc/main_branch_protection.md index 98949e2..6c96048 100644 --- a/doc/main_branch_protection.md +++ b/doc/main_branch_protection.md @@ -95,7 +95,9 @@ done ## 🆘 I'm on 'main' and have changes, what do I do? -If you've already made changes on `main` and the hook blocks your commit, **don't panic and don't drop your stash!** You can easily move your work to a new branch. +If you've already made changes on `main` and the hook blocks your commit, **don't panic and don't drop your stash!** +1. **Stage all changes**: Run `git add .` or `git add -A` to ensure all changes, including new or untracked files, are staged for commit. (Note: `git commit -am` only stages modified files, so explicit staging is recommended to avoid losing new work). +2. **Commit your work**: Run `git commit -m "Your descriptive message"` to save your staged changes to your local branch history. ### The "Magic" Command: Just Create a New Branch Git allows you to create and switch to a new branch while keeping your uncommitted changes. @@ -125,7 +127,8 @@ git checkout -b feature/my-cool-feature git stash pop # 4. Commit -git commit -am "My feature changes" +git add . +git commit -m "My feature changes" ``` ### "I accidentally committed before I added the hook!" @@ -139,6 +142,9 @@ git branch feature/my-feature git fetch origin # 3. Reset your local 'main' back to where it should be (the remote version) # (Fetching first ensures you don't accidentally reset to a stale origin/main reference) +# SAFETY CHECK: Verify that your working tree is clean (e.g. `git status`). +# Stash or commit any uncommitted changes before proceeding! +# ⚠️ This reset will DISCARD ALL UNCOMMITTED LOCAL CHANGES. git reset --hard origin/main # 4. Switch to your new branch to continue working diff --git a/packages/library/src/common/assertion.library.ts b/packages/library/src/common/assertion.library.ts index ac23489..aa3fbe7 100644 --- a/packages/library/src/common/assertion.library.ts +++ b/packages/library/src/common/assertion.library.ts @@ -65,11 +65,11 @@ export const isTemporal = (obj: T): obj is Extract => protoType (obj as any) instanceof (globalThis as any).Temporal.PlainMonthDay )); -export const isInstant = (obj: T): obj is Extract => 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 = (obj: T): obj is Extract => 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 = (obj: T): obj is Extract => 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 isInstant = (obj: T): obj is Extract => 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) && isUndefined((obj as any).timeZone)); +export const isZonedDateTime = (obj: T): obj is Extract => 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) || isDefined((obj as any).timeZone))); +export const isPlainDate = (obj: T): obj is Extract => 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) && isUndefined((obj as any).timeZone) && 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 = (obj: T): obj is Extract => 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 = (obj: T): obj is Extract => 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 isPlainDateTime = (obj: T): obj is Extract => 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) && isUndefined((obj as any).timeZone) && (isDefined((obj as any).hour) || isDefined((obj as any).minute) || isDefined((obj as any).second) || isDefined((obj as any).nanosecond))); export const isDuration = (obj: T): obj is Extract => 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 = (obj: T): obj is Extract => isString(obj) || isDuration(obj) || (isObject(obj) && ( 'years' in obj || 'months' in obj || 'weeks' in obj || 'days' in obj || diff --git a/packages/library/src/common/temporal.library.ts b/packages/library/src/common/temporal.library.ts index 5a7acdd..450a060 100644 --- a/packages/library/src/common/temporal.library.ts +++ b/packages/library/src/common/temporal.library.ts @@ -4,7 +4,7 @@ */ import '#library/temporal.polyfill.js'; // ensure Temporal is available -import { isNumber, isString } from '#library/assertion.library.js'; +import { isNumber, isObject, isString, isUndefined, isZonedDateTime } from '#library/assertion.library.js'; /** return the current Temporal.Now.instant */ export function instant() { @@ -81,9 +81,10 @@ export function normaliseFractionalDurations(payload: Record) { /** * ## toZonedDateTime * Create a `Temporal.ZonedDateTime` from a - * property-bag (year, month, day, …, timeZone, calendar). + * property-bag or ISO string. */ -export function toZonedDateTime(bag: Temporal.ZonedDateTimeLike & { timeZone: Temporal.TimeZoneLike, calendar?: Temporal.CalendarLike }): Temporal.ZonedDateTime { +export function toZonedDateTime(bag: Temporal.ZonedDateTimeLike | string, tz: Temporal.TimeZoneLike = 'UTC'): Temporal.ZonedDateTime { + if (isString(bag)) return Temporal.ZonedDateTime.from(`${bag}[${tz}]`); return Temporal.ZonedDateTime.from(bag); } @@ -108,15 +109,33 @@ export function toInstant(epochNanoseconds: bigint): Temporal.Instant { /** * ## getTemporalIds * Normalize TimeZone and Calendar inputs into a [timeZoneId, calendarId] tuple. + * Accepts either (tz, cal) strings or a single ZonedDateTime-like object. + * Supports both spec-final (flat) and V8 harmony (nested) structures. */ -export function getTemporalIds(tz: any, cal: any): [string, string] { - const rawTz = isString(tz) ? tz : ((tz as any)?.timeZoneId ?? (tz as any)?.id); - const rawCal = isString(cal) ? cal : ((cal as any)?.calendarId ?? (cal as any)?.id); +export function getTemporalIds(tzOrZdt: any, cal?: any): [string, string] { const fallbackTz = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; - const tzId = (isString(rawTz) && rawTz.trim().length > 0) ? rawTz : fallbackTz; - const calId = (isString(rawCal) && rawCal.trim().length > 0) ? rawCal : 'iso8601'; + const bag = (isZonedDateTime(tzOrZdt) && isUndefined(cal)) + ? tzOrZdt + : { timeZone: tzOrZdt, calendar: cal }; + + const rawTz = bag.timeZoneId ?? bag.timeZone?.id ?? bag.timeZone; + const rawCal = bag.calendarId ?? bag.calendar?.id ?? bag.calendar; + + // Helper to extract string ID from potential objects (TimeZone, Calendar, or ZonedDateTime) + const toId = (v: any): string => { + if (isString(v)) return v; + if (isZonedDateTime(v)) return toId((v as any).timeZoneId ?? (v as any).timeZone?.id ?? (v as any).timeZone); + if (isObject(v)) return String((v as any).id ?? (v as any).timeZoneId ?? (v as any).calendarId ?? ''); + return String(v ?? ''); + } + + const tzStr = toId(rawTz); + const calStr = toId(rawCal); + + const tzId = (tzStr.trim().length > 0 && tzStr !== '[object Object]' && tzStr !== 'undefined') ? tzStr : fallbackTz; + const calId = (calStr.trim().length > 0 && calStr !== '[object Object]' && calStr !== 'undefined') ? calStr : 'iso8601'; - return [tzId || 'UTC', calId || 'iso8601']; + return [tzId, calId]; } /** diff --git a/packages/tempo/doc/tempo.layout.md b/packages/tempo/doc/tempo.layout.md index d1fecea..1f2fbbb 100644 --- a/packages/tempo/doc/tempo.layout.md +++ b/packages/tempo/doc/tempo.layout.md @@ -90,7 +90,7 @@ You can use the static `Tempo.regexp()` method to "preview" how a layout string const regex = Tempo.regexp('{dd}{sep}{mm}{sep}{yy}'); console.log(regex.source); -// Output (illustrative): ^((?
...)(?...)(?...)(?...)(?...))$ +// Output (illustrative): ^((?
...)(?...)(?...)(?...)(?...))$ ``` --- diff --git a/packages/tempo/index.md b/packages/tempo/index.md index 83be48f..3ec7e2c 100644 --- a/packages/tempo/index.md +++ b/packages/tempo/index.md @@ -556,12 +556,11 @@ function focusActiveCard() { } .tempo-btn-brand { - background-color: #3498db; - color: white; + background-color: var(--vp-c-brand-1); + color: var(--vp-c-white); } .tempo-btn-brand:hover { - background-color: #2980b9; - color: white; + background-color: var(--vp-c-brand-2); } .tempo-btn-alt { diff --git a/packages/tempo/plan/RELEASE-D.md b/packages/tempo/plan/RELEASE-D.md index 4567304..e280143 100644 --- a/packages/tempo/plan/RELEASE-D.md +++ b/packages/tempo/plan/RELEASE-D.md @@ -28,11 +28,11 @@ This release focuses on modularizing and refactoring the parsing and pattern-mat - [x] Move "Scan-and-Consume" logic to `engine.guard.ts` - [x] Refactor `tempo.class.ts` to use `createMasterGuard` -### Parse Result Normalizer Extraction (Assessment) -- [ ] Identify all result normalization/trace logic -- [ ] Assess complexity/reuse for extraction -- [ ] Outline module boundaries if justified -- [ ] Document reasons if not extracted +### Parse Result Normalizer Extraction +- [x] Identify all result normalization/trace logic +- [x] Assess complexity/reuse for extraction +- [x] Outline module boundaries if justified +- [x] Extract normalization logic to `engine.normalizer.ts` ## Expected Improvements and Risks diff --git a/packages/tempo/src/discrete/discrete.parse.ts b/packages/tempo/src/discrete/discrete.parse.ts index 7f8ef9f..d634ed7 100644 --- a/packages/tempo/src/discrete/discrete.parse.ts +++ b/packages/tempo/src/discrete/discrete.parse.ts @@ -6,10 +6,11 @@ import { isNumeric } from '#library/assertion.library.js'; import { instant, getTemporalIds } from '#library/temporal.library.js'; import { ownKeys, ownEntries } from '#library/primitive.library.js'; import type { TypeValue } from '#library/type.library.js'; -import { resolveTermMutation, resolveTermValue } from '../engine/engine.term.js'; + +import { resolveTermValue } from '../engine/engine.term.js'; import { selectLayoutPatterns } from '../engine/engine.planner.js'; -import { prefix, parseWeekday, parseDate, parseTime, parseZone } from '../engine/engine.lexer.js'; import { compose } from '../engine/engine.composer.js'; +import { normalizeMatch, accumulateResult } from '../engine/engine.normalizer.js'; import { getRange, getTermRange } from '../plugin/term.util.js'; import { defineInterpreterModule } from '../plugin/plugin.util.js'; @@ -17,37 +18,9 @@ import type { Range, ResolvedRange } from '../plugin/plugin.type.js'; import { sym, isTempo, TermError, getRuntime, Match } from '../support/support.index.js'; import { markConfig, setPatterns, init, extendState } from '../support/support.index.js'; import { setProperty } from '#tempo/support/support.util.js'; -import enums from '../support/support.enum.js'; import * as t from '../tempo.type.js'; import type { Tempo } from '../tempo.class.js'; -/** - * Provide a lightweight host context that mimics a Tempo instance for functional alias handlers. - * @internal - */ -function getResolutionContext(state: any, dateTime: Temporal.ZonedDateTime, resolvingKeys: Set) { - const TempoClass = getRuntime().modules['Tempo']; - return { - add: (val: any) => dateTime.add(val), - subtract: (val: any) => dateTime.subtract(val), - with: (val: any) => dateTime.with(val), - set: (val: any, opt?: any) => { - const res = _ParseEngine.conform(state, val, dateTime, true, resolvingKeys); - return (TempoClass as any)?.from(isZonedDateTime(res.value) ? res.value : dateTime, { ...state.config, ...opt }); - }, - toNow: () => instant().toZonedDateTimeISO(state.config.timeZone).withCalendar(state.config.calendar), - toDateTime: () => dateTime, - get hh() { return dateTime.hour }, - get mi() { return dateTime.minute }, - get ss() { return dateTime.second }, - get yy() { return dateTime.year }, - get mm() { return dateTime.month }, - get dd() { return dateTime.day }, - [sym.$Identity]: true, - config: state.config - }; -} - /** * Internal Parse Engine Implementation */ @@ -78,19 +51,19 @@ const _ParseEngine = { try { const { config } = state; + const TempoClass = getRuntime().modules['Tempo']; + const terms = getRuntime().pluginsDb.terms; + const val = dateTime ?? state.anchor ?? state.config.anchor ?? (isTempo(tempo) ? (tempo as any).toDateTime() : (isZonedDateTime(tempo) ? tempo : (isInstant(tempo) ? tempo.toZonedDateTimeISO(config.timeZone) : undefined))); - const basis = isTempo(val) ? (val as any).toDateTime() : (isDefined(val) ? val : instant().toZonedDateTimeISO(config.timeZone)); + const [tz, cal] = getTemporalIds(config.timeZone, config.calendar); + const basis = isTempo(val) ? (val as any).toDateTime() : (isDefined(val) ? val : instant().toZonedDateTimeISO(tz).withCalendar(cal)); const isAnchored = isDefined(val); if (isRoot) { state.parse.anchor = basis; state.parse.isAnchored = isAnchored; } - const [tz, cal] = isTempo(basis) ? [(basis as any).tz, (basis as any).cal] : getTemporalIds(basis ?? config.timeZone, basis ?? config.calendar); - today = isZonedDateTime(basis) ? basis : (isTempo(basis) ? (basis as any).toDateTime() : (isZonedDateTime(val) ? val : instant().toZonedDateTimeISO(tz).withCalendar(cal))); - - const TempoClass = getRuntime().modules['Tempo']; - const terms = getRuntime().pluginsDb.terms; + today = basis; if (term) { const ident = term.startsWith('#') ? term.slice(1) : term; @@ -158,7 +131,7 @@ const _ParseEngine = { const { timeZone: tz2, calendar: cal2 } = state.config; const [targetTz, targetCal] = getTemporalIds(tz2, cal2); - const { dateTime: dt, timeZone } = compose(res, today, tz, targetTz, targetCal, (m) => _ParseEngine.result(state, m), state.config.timeStamp, state.config); + const { dateTime: dt, timeZone } = compose(res, today, tz, targetTz, targetCal, (m) => accumulateResult(state, m), state.config.timeStamp, state.config); dateTime = dt; if (timeZone && state) state.config.timeZone = timeZone; @@ -198,13 +171,10 @@ const _ParseEngine = { } if (!isEmpty(options)) zdt = zdt.with(options as Temporal.ZonedDateTimeLikeObject); + if (timeZone) zdt = zdt.withTimeZone(timeZone); + if (calendar) zdt = zdt.withCalendar(calendar); - if (timeZone) - if (isZonedDateTime(zdt)) zdt = zdt.withTimeZone(timeZone); - if (calendar) - zdt = zdt.withCalendar(calendar); - - _ParseEngine.result(state, { type: 'Temporal.ZonedDateTimeLike', value: zdt, match: 'Temporal.ZonedDateTimeLike' }); + accumulateResult(state, { type: 'Temporal.ZonedDateTimeLike', value: zdt, match: 'Temporal.ZonedDateTimeLike' }); return Object.assign(arg, { type: 'Temporal.ZonedDateTime', @@ -214,8 +184,9 @@ const _ParseEngine = { if (isTempo(value)) { const res = (value as any).toDateTime(); - state.config.timeZone = res.timeZoneId; - state.config.calendar = res.calendarId; + const [tz, cal] = getTemporalIds(res); + state.config.timeZone = tz; + state.config.calendar = cal; return Object.assign(arg, { type: 'Temporal.ZonedDateTime', value: res }); } @@ -273,11 +244,11 @@ const _ParseEngine = { if (type === 'String') { if (isEmpty(trim)) { - _ParseEngine.result(state, { type: 'Empty', value: trim, match: 'Empty' }); + accumulateResult(state, { type: 'Empty', value: trim, match: 'Empty' }); return Object.assign(arg, { type: 'Empty' }); } if (isIntegerLike(trim)) { - _ParseEngine.result(state, { type: 'BigInt', value: asInteger(trim), match: 'BigInt' }); + accumulateResult(state, { type: 'BigInt', value: asInteger(trim), match: 'BigInt' }); return Object.assign(arg, { type: 'BigInt', value: asInteger(trim) }); } } @@ -320,15 +291,15 @@ const _ParseEngine = { const hasTime = Object.keys(groups) .some(key => ['hh', 'mi', 'ss', 'ms', 'us', 'ns', 'ff', 'mer'].includes(key) || Match.period.test(key) || (Match.named.test(key) && key.endsWith('tm'))) || Object.values(groups).includes('now'); - _ParseEngine.result(state, { match: symKey.description, value: trim, groups: { ...groups } }); - - dateTime = parseZone(groups, dateTime, state.config); - dateTime = _ParseEngine.parseGroups(state, groups, dateTime, isAnchored, resolvingKeys); - if (state.errored) return arg; + accumulateResult(state, { match: symKey.description, value: trim, groups: { ...groups } }); - dateTime = parseWeekday(groups, dateTime, state.config); - dateTime = parseDate(groups, dateTime, state.config, state.parse["pivot"]); - dateTime = parseTime(groups, dateTime); + dateTime = normalizeMatch(groups, dateTime, { + state, + isAnchored, + resolvingKeys, + subParse: (v, dt, rk) => _ParseEngine.parseLayout(state, v, dt, true, rk), + conform: (v, dt, rk) => _ParseEngine.conform(state, v, dt, true, rk) + }); const isChanged = !dateTime.toPlainTime().equals(anchorTime); if (!isAnchored && !hasTime && !isChanged) @@ -353,110 +324,6 @@ const _ParseEngine = { return groups as t.Groups; }, - /** resolve {event} | {period} to their date | time values (mutates groups) */ - parseGroups(state: t.Internal.State, groups: t.Groups, dateTime: Temporal.ZonedDateTime, isAnchored: boolean, resolvingKeys: Set): Temporal.ZonedDateTime { - const prevAnchor = state.anchor; - const prevZdt = state.zdt; - - state.anchor = dateTime; - state.zdt = dateTime; - - state.parseDepth = (state.parseDepth ?? 0) + 1; - const isRoot = state.parseDepth === 1; - if (isRoot) state.matches = []; - - const TempoClass = getRuntime().modules['Tempo']; - const aliasEngine = state.aliasEngine ?? (TempoClass as any)?.[sym.$Internal]?.().aliasEngine; - - try { - for (const key of ownKeys(groups)) { - if (key === 'slk') { - const slk = groups[key]; - const result = resolveTermMutation(TempoClass, state as any, 'set', slk, undefined, dateTime); - - if (result === null) { - state.errored = true; - delete groups[key]; - break; - } - - dateTime = result; - delete groups[key]; - continue; - } - - if (Match.named.test(key)) { // remove structural markers - delete groups[key]; - continue; - } - - const register = aliasEngine?.getAlias(key); - if (!register) continue; - - const aliasKey = register.name; - if (resolvingKeys.size > 50 || resolvingKeys.has(aliasKey)) { - const msg = `Infinite recursion detected in Tempo resolution for: ${aliasKey}`; - state.errored = true; - if (TempoClass) (TempoClass as any)[sym.$logError](state.config, new RangeError(msg)); - delete groups[key]; - continue; - } - - resolvingKeys.add(aliasKey); - - const host = getResolutionContext(state, dateTime, resolvingKeys); - const res = aliasEngine?.resolveAlias(key as any, host); - if (!res) continue; - - try { - const mapped = ({ - evt: { type: 'Event', pat: 'dt' }, - per: { type: 'Period', pat: 'tm' } - } as const)[res.type as 'evt' | 'per']; - - if (!mapped) - throw new Error(`[ParseEngine] Unexpected AliasType: ${res.type}`); - - const { type, pat } = mapped; - - _ParseEngine.result(state, { type, value: res.key as any, match: pat, source: res.source, groups: { [key]: res.value } }); - - // If it resolved to a new string, we re-parse it - if (!isEmpty(res.value) && res.value !== String(groups[key])) { - const resolving = new Set(resolvingKeys); - resolving.add(res.key); - // Explicitly propagate anchor for recursive parse - const prevAnchor: any = state.anchor; - state.anchor = dateTime; - const resMatch = _ParseEngine.parseLayout(state, res.value, dateTime, true, resolving); - state.anchor = prevAnchor; - - if (resMatch.type === 'Temporal.ZonedDateTime') - dateTime = resMatch.value; - } - } finally { - state.zdt = dateTime; - delete groups[key]; - } - } - } finally { - if (isDefined(prevAnchor)) state.anchor = prevAnchor; - else delete state.anchor; - if (isDefined(prevZdt)) state.zdt = prevZdt; - else delete state.zdt; - state.parseDepth--; - if (state.parseDepth === 0) delete state.matches; - } - - if (isDefined(groups["mm"]) && !isNumeric(groups["mm"])) { - const mm = prefix(groups["mm"] as t.MONTH); - if (TempoClass) groups["mm"] = (TempoClass as any).MONTH[mm as t.MONTH]!.toString().padStart(2, '0'); - else if (enums.MONTH[mm as t.MONTH]) groups["mm"] = enums.MONTH[mm as t.MONTH]!.toString().padStart(2, '0'); - } - - return dateTime; - }, - /** check if we've been given a ZonedDateTimeLike object */ isZonedDateTimeLike(state: any, tempo: t.DateTime | t.Options | undefined): tempo is Temporal.ZonedDateTimeLike & { value?: any } { if (!isObject(tempo) || isEmpty(tempo) || (tempo.constructor !== Object && tempo.constructor !== undefined)) @@ -470,21 +337,6 @@ const _ParseEngine = { .filter(isString) .some((key: string) => state.ZONED_DATE_TIME.has(key) && !state.CONFIG.has(key)) }, - - /** accumulate match results */ - result(state: any, ...rest: Partial[]) { - const match = Object.assign({}, ...rest) as t.Internal.Match; - - if (isDefined(state.parse.anchor)) - match.anchor = state.parse.anchor; - - if (!isDefined(match.isAnchored) && isDefined(state.parse.isAnchored)) - match.isAnchored = state.parse.isAnchored; - - const res = state.parse.result; - if (isDefined(res) && !Object.isFrozen(res)) - if (!res.includes(match)) res.push(match); - } } const withState = (fn: (state: t.Internal.State, ...args: A) => R) => { @@ -507,9 +359,8 @@ export const ParseEngine = { conform: withState(_ParseEngine.conform), parseLayout: withState(_ParseEngine.parseLayout), parseMatch: withState(_ParseEngine.parseMatch), - parseGroups: withState(_ParseEngine.parseGroups), isZonedDateTimeLike: withState(_ParseEngine.isZonedDateTimeLike), - result: withState(_ParseEngine.result) + result: withState(accumulateResult) }; /** diff --git a/packages/tempo/src/engine/engine.alias.ts b/packages/tempo/src/engine/engine.alias.ts index 5b1561f..21b29e8 100644 --- a/packages/tempo/src/engine/engine.alias.ts +++ b/packages/tempo/src/engine/engine.alias.ts @@ -81,19 +81,21 @@ export class AliasEngine { getWords() { return this.#words } constructor(options = {} as AliasEngineOptions) { - this.#parent = options.parent ?? null; + const parent = options.parent; this.#logger = options.logger; this.#config = options.config; this.#id = AliasEngine.#idCounter++; - if (this.#parent instanceof AliasEngine) { - this.#depth = this.#parent.#depth + 1; - this.#state = Object.create(this.#parent.#state); // create a new state object that inherits from the parent engine's state - this.#words = Object.create(this.#parent.#words); // create a new words object that inherits from the parent engine's words for collision detection + if (parent instanceof AliasEngine) { + this.#parent = parent; + this.#depth = parent.#depth + 1; + this.#state = Object.create(parent.#state); // create a new state object that inherits from the parent engine's state + this.#words = Object.create(parent.#words); // create a new words object that inherits from the parent engine's words for collision detection } else { - if (this.#parent) + if (parent) this.#logger?.error(this.#config, "Parent engine must be an instance of AliasEngine"); + this.#parent = null; this.#depth = 0; this.#state = Object.create(null); // initialize an empty state for the root engine (no parent) this.#words = Object.create(null); // initialize an empty words object for the root engine (no parent) diff --git a/packages/tempo/src/engine/engine.composer.ts b/packages/tempo/src/engine/engine.composer.ts index 9289d01..50375ed 100644 --- a/packages/tempo/src/engine/engine.composer.ts +++ b/packages/tempo/src/engine/engine.composer.ts @@ -1,5 +1,6 @@ -import { isTempo, logError } from '#tempo/support'; +import { getTemporalIds } from '#library/temporal.library.js'; import { isNumeric, isInstant, isZonedDateTime, isPlainDate, isPlainDateTime } from '#library/assertion.library.js'; +import { isTempo, logError } from '#tempo/support'; import type { TemporalObject, TypeValue } from '#library/type.library.js'; import type { Tempo } from '#tempo/tempo.class.js'; import * as t from '../tempo.type.js'; @@ -20,7 +21,7 @@ export function compose( ): { dateTime: Temporal.ZonedDateTime, timeZone?: string | undefined } { let temporal: TemporalObject | Tempo = today; let timeZone: string | undefined; - let dateTime: Temporal.ZonedDateTime; + let dateTime: Temporal.ZonedDateTime = today; switch (type) { case 'Void': @@ -33,8 +34,8 @@ export function compose( case 'String': try { const str = value.replace(/Z$/, ''); - const zdt = Temporal.ZonedDateTime.from(`${str}[${tz}]`); - timeZone = zdt.timeZoneId; + const zdt = Temporal.ZonedDateTime.from(str.includes('[') ? str : `${str}[${tz}]`); + timeZone = getTemporalIds(zdt)[0]; temporal = zdt; onResult?.({ type, value: str, match: 'iso8601' }); } catch (err) { @@ -152,5 +153,5 @@ export function compose( } } - return { dateTime, timeZone }; + return { dateTime: dateTime ?? today, timeZone }; } diff --git a/packages/tempo/src/engine/engine.guard.ts b/packages/tempo/src/engine/engine.guard.ts index 8fd5317..33a02b9 100644 --- a/packages/tempo/src/engine/engine.guard.ts +++ b/packages/tempo/src/engine/engine.guard.ts @@ -16,8 +16,8 @@ export interface MasterGuard { */ export function createMasterGuard(words: (string | symbol)[]): MasterGuard { const wordsList = words - .filter(w => isString(w) || isSymbol(w)) - .map(w => (isSymbol(w) ? w.description : (w as string))!.toLowerCase()) + .filter(w => isString(w) || (isSymbol(w) && !!w.description)) + .map(w => (isSymbol(w) ? w.description! : (w as string)).toLowerCase()) .filter(Boolean); const allowedTokens = new Set(wordsList); @@ -32,6 +32,7 @@ export function createMasterGuard(words: (string | symbol)[]): MasterGuard { let i = 0; const len = input.length; + let matchedAny = false; while (i < len) { const char = input[i]; @@ -48,6 +49,7 @@ export function createMasterGuard(words: (string | symbol)[]): MasterGuard { const match = sub.match(Match.bracket); if (match && match.index === 0) { i += match[0].length; + matchedAny = true; continue; } } @@ -62,6 +64,7 @@ export function createMasterGuard(words: (string | symbol)[]): MasterGuard { if (allowedTokens.has(candidate)) { i += l; matched = true; + matchedAny = true; break; } } @@ -70,13 +73,14 @@ export function createMasterGuard(words: (string | symbol)[]): MasterGuard { // 4. Try Fallback char (Match.guard) if (Match.guard.test(char)) { i++; + matchedAny = true; continue; } return false; // No valid match at current position } - return true; + return matchedAny; } - } + }; } diff --git a/packages/tempo/src/engine/engine.normalizer.ts b/packages/tempo/src/engine/engine.normalizer.ts new file mode 100644 index 0000000..028a619 --- /dev/null +++ b/packages/tempo/src/engine/engine.normalizer.ts @@ -0,0 +1,205 @@ +import { isDefined, isEmpty, isZonedDateTime, isNumeric } from '#library/assertion.library.js'; +import type { TypeValue } from '#library/type.library.js'; +import { ownKeys } from '#library/primitive.library.js'; +import { getRuntime, sym, Match } from '#tempo/support'; +import { getTemporalIds, instant } from '#library/temporal.library.js'; +import { prefix, parseWeekday, parseDate, parseTime, parseZone } from './engine.lexer.js'; +import { resolveTermMutation } from './engine.term.js'; +import enums from '#tempo/support/support.enum.js'; +import * as t from '../tempo.type.js'; + +/** + * Context provided to the normalizer to handle recursion and state management. + */ +export interface NormalizerContext { + state: t.Internal.State; + isAnchored: boolean; + resolvingKeys: Set; + subParse: (value: string, dateTime: Temporal.ZonedDateTime, resolvingKeys: Set) => TypeValue; + conform: (value: any, dateTime: Temporal.ZonedDateTime, resolvingKeys: Set) => TypeValue; +} + +/** + * Provide a lightweight host context that mimics a Tempo instance for functional alias handlers. + */ +export function getResolutionContext(ctx: NormalizerContext, dateTime: Temporal.ZonedDateTime) { + const { state, resolvingKeys, conform } = ctx; + const TempoClass = getRuntime().modules['Tempo']; + return { + add: (val: any) => dateTime.add(val), + subtract: (val: any) => dateTime.subtract(val), + with: (val: any) => dateTime.with(val), + set: (val: any, opt?: any) => { + const res = conform(val, dateTime, resolvingKeys); + return (TempoClass as any)?.from(isZonedDateTime(res.value) ? res.value : dateTime, { ...state.config, ...opt }); + }, + toNow: () => { + const [tz, cal] = getTemporalIds(state.config.timeZone, state.config.calendar); + return instant().toZonedDateTimeISO(tz).withCalendar(cal); + }, + get tz() { return getTemporalIds(state.config.timeZone)[0] }, + get cal() { return getTemporalIds(state.config.timeZone, state.config.calendar)[1] }, + toDateTime: () => dateTime, + get hh() { return dateTime.hour }, + get mi() { return dateTime.minute }, + get ss() { return dateTime.second }, + get yy() { return dateTime.year }, + get mm() { return dateTime.month }, + get dd() { return dateTime.day }, + [sym.$Identity]: true, + config: state.config + }; +} + +/** + * Normalize a set of regex groups into a Temporal.ZonedDateTime. + */ +export function normalizeMatch( + groups: t.Groups, + dateTime: Temporal.ZonedDateTime, + ctx: NormalizerContext +): Temporal.ZonedDateTime { + const { state, isAnchored } = ctx; + + // 1. Zone + dateTime = parseZone(groups, dateTime, state.config); + + // 2. Aliases & Groups + dateTime = resolveAliases(groups, dateTime, ctx); + if (state.errored) return dateTime; + + // 3. Weekday, Date, Time + dateTime = parseWeekday(groups, dateTime, state.config); + dateTime = parseDate(groups, dateTime, state.config, state.parse["pivot"]); + dateTime = parseTime(groups, dateTime); + + return dateTime; +} + +/** + * Resolve {event} | {period} aliases found in the matched groups. + */ +export function resolveAliases( + groups: t.Groups, + dateTime: Temporal.ZonedDateTime, + ctx: NormalizerContext +): Temporal.ZonedDateTime { + const { state, resolvingKeys, subParse } = ctx; + const prevAnchor = state.anchor; + const prevZdt = state.zdt; + + state.anchor = dateTime; + state.zdt = dateTime; + + state.parseDepth = (state.parseDepth ?? 0) + 1; + const isRoot = state.parseDepth === 1; + if (isRoot) state.matches = []; + + const TempoClass = getRuntime().modules['Tempo']; + const aliasEngine = state.aliasEngine ?? (TempoClass as any)?.[sym.$Internal]?.().aliasEngine; + + try { + for (const key of ownKeys(groups)) { + if (key === 'slk') { + const slk = groups[key]; + const result = resolveTermMutation(TempoClass, state as any, 'set', slk, undefined, dateTime); + + if (result === null) { + state.errored = true; + delete groups[key]; + break; + } + + dateTime = result; + delete groups[key]; + continue; + } + + if (Match.named.test(key)) { + delete groups[key]; + continue; + } + + const register = aliasEngine?.getAlias(key); + if (!register) continue; + + const aliasKey = register.name; + if (resolvingKeys.size > 50 || resolvingKeys.has(aliasKey)) { + const msg = `Infinite recursion detected in Tempo resolution for: ${aliasKey}`; + state.errored = true; + if (TempoClass) (TempoClass as any)[sym.$logError](state.config, new RangeError(msg)); + delete groups[key]; + continue; + } + + resolvingKeys.add(aliasKey); + + const host = getResolutionContext(ctx, dateTime); + const res = aliasEngine?.resolveAlias(key as any, host); + if (!res) continue; + + try { + const mapped = ({ + evt: { type: 'Event', pat: 'dt' }, + per: { type: 'Period', pat: 'tm' } + } as const)[res.type as 'evt' | 'per']; + + if (!mapped) + throw new Error(`[ParseEngine] Unexpected AliasType: ${res.type}`); + + const { type, pat } = mapped; + + accumulateResult(state, { type, value: res.key as any, match: pat, source: res.source, groups: { [key]: res.value } }); + + if (!isEmpty(res.value) && res.value !== String(groups[key])) { + const resolving = new Set(resolvingKeys); + resolving.add(res.key); + + const prevAnchor: any = state.anchor; + state.anchor = dateTime; + const resMatch = subParse(res.value, dateTime, resolving); + state.anchor = prevAnchor; + + if (resMatch.type === 'Temporal.ZonedDateTime') + dateTime = resMatch.value; + } + } finally { + state.zdt = dateTime; + delete groups[key]; + } + } + } finally { + if (isDefined(prevAnchor)) state.anchor = prevAnchor; + else delete state.anchor; + if (isDefined(prevZdt)) state.zdt = prevZdt; + else delete state.zdt; + state.parseDepth--; + if (state.parseDepth === 0) delete state.matches; + } + + if (isDefined(groups["mm"]) && !isNumeric(groups["mm"])) { + const mm = prefix(groups["mm"] as t.MONTH); + if (TempoClass) groups["mm"] = (TempoClass as any).MONTH[mm as t.MONTH]!.toString().padStart(2, '0'); + else if (enums.MONTH[mm as t.MONTH]) groups["mm"] = enums.MONTH[mm as t.MONTH]!.toString().padStart(2, '0'); + } + + return dateTime; +} + +/** + * Accumulate match results for diagnostic tracing. + */ +export function accumulateResult(state: t.Internal.State, ...rest: Partial[]) { + const match = Object.assign({}, ...rest) as t.Internal.Match; + + if (isDefined(state.parse.anchor)) + match.anchor = state.parse.anchor; + + if (!isDefined(match.isAnchored) && isDefined(state.parse.isAnchored)) + match.isAnchored = state.parse.isAnchored; + + const res = state.parse.result; + if (isDefined(res) && !Object.isFrozen(res)) { + if (!res.includes(match)) res.push(match); + } +} diff --git a/packages/tempo/src/engine/engine.pattern.ts b/packages/tempo/src/engine/engine.pattern.ts index aca9bb0..cd73003 100644 --- a/packages/tempo/src/engine/engine.pattern.ts +++ b/packages/tempo/src/engine/engine.pattern.ts @@ -25,6 +25,8 @@ export class PatternCompiler { this.#state = options.state; } + get state() { return this.#state; } + /** * Translates {layout} into an anchored, case-insensitive RegExp. * Includes recursive expansion of placeholders using snippet registries. diff --git a/packages/tempo/src/engine/engine.term.ts b/packages/tempo/src/engine/engine.term.ts index cce7c84..c9b7930 100644 --- a/packages/tempo/src/engine/engine.term.ts +++ b/packages/tempo/src/engine/engine.term.ts @@ -1,4 +1,4 @@ -import { toZonedDateTime, toInstant } from '#library/temporal.library.js'; +import { toZonedDateTime, toInstant, getTemporalIds } from '#library/temporal.library.js'; import { isDefined, isString, isZonedDateTime, isNumeric } from '#library/assertion.library.js'; import { asArray } from '#library/coercion.library.js'; @@ -40,8 +40,7 @@ export function resolveTermMutation(Tempo: TempoType, instance: Tempo, mutate: s return null; } - const tz = zdt.timeZoneId; - const cal = zdt.calendarId; + const [tz, cal] = getTemporalIds(zdt); // Slick Shorthand Parsing (e.g. #qtr.>2, #zodiac.<) let mod: string | undefined; diff --git a/packages/tempo/src/module/module.duration.ts b/packages/tempo/src/module/module.duration.ts index bf2456f..6d73004 100644 --- a/packages/tempo/src/module/module.duration.ts +++ b/packages/tempo/src/module/module.duration.ts @@ -1,4 +1,5 @@ -import { isString, isObject, isDefined, isUndefined, isZonedDateTime } from '#library/assertion.library.js'; +import { getTemporalIds } from '#library/temporal.library.js'; +import { isString, isObject, isDefined, isUndefined } from '#library/assertion.library.js'; import { singular } from '#library/string.library.js'; import { getAccessors } from '#library/reflection.library.js'; import { ifDefined } from '#library/object.library.js'; @@ -94,8 +95,11 @@ function duration(this: Tempo, type: 'until' | 'since', arg?: any, until?: any) const offset = new (this.constructor as any)(value, { ...opts, anchor: this, mode: enums.MODE.Strict }); const offsetZdt = offset.toDateTime(); - const diffZone = selfZdt.timeZoneId !== offsetZdt.timeZoneId; - const dur = selfZdt.until(offsetZdt.withCalendar(selfZdt.calendarId), { largestUnit: diffZone ? 'hours' : (unit ?? 'years') }); + const [selfTz, selfCal] = getTemporalIds(selfZdt); + const [offsetTz] = getTemporalIds(offsetZdt); + + const diffZone = selfTz !== offsetTz; + const dur = selfZdt.until(offsetZdt.withCalendar(selfCal), { largestUnit: diffZone ? 'hours' : (unit ?? 'years') }); if (isDefined(unit)) unit = `${singular(unit)}s`; @@ -117,8 +121,8 @@ function duration(this: Tempo, type: 'until' | 'since', arg?: any, until?: any) const locale = (this as any).config['locale']; const rtConfig = (this as any).config.intl?.relativeTime; const rtOptions = opts['relativeTime']; - - const rtf = (typeof rtOptions === 'function' ? rtOptions : rtOptions?.format) + + const rtf = (typeof rtOptions === 'function' ? rtOptions : rtOptions?.format) || (typeof rtConfig === 'function' ? rtConfig : rtConfig?.format) || opts['rtfFormat'] || (this as any).config['rtfFormat']; diff --git a/packages/tempo/src/support/support.default.ts b/packages/tempo/src/support/support.default.ts index ce55d52..6ab791f 100644 --- a/packages/tempo/src/support/support.default.ts +++ b/packages/tempo/src/support/support.default.ts @@ -1,6 +1,7 @@ import { looseIndex } from '#library/object.library.js'; import { secure, proxify } from '#library/proxy.library.js'; import { getDateTimeFormat } from '#library/international.library.js'; +import { getTemporalIds } from '#library/temporal.library.js'; import { NUMBER, MODE, MONTH_DAY } from './support.enum.js'; import { Token } from './support.symbol.js'; diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index 67ae44b..8785632 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -13,7 +13,7 @@ import { pad, trimAll } from '#library/string.library.js'; import { getType } from '#library/type.library.js'; import { clone } from '#library/serialize.library.js'; import { isEmpty, isDefined, isUndefined, isString, isObject, isSymbol, isFunction, isClass, isZonedDateTime, isDurationLike } from '#library/assertion.library.js'; -import { instant } from '#library/temporal.library.js'; +import { instant, getTemporalIds } from '#library/temporal.library.js'; import { getDateTimeFormat, getHemisphere, canonicalLocale } from '#library/international.library.js'; import type { Property, Secure } from '#library/type.library.js'; @@ -828,7 +828,8 @@ export class Tempo { static regexp(layout: string | RegExp, snippet?: Snippet) { const state = (this as any)[$Internal](); - state.patternCompiler ??= new PatternCompiler({ state }); + if (!state.patternCompiler || state.patternCompiler.state !== state) + state.patternCompiler = new PatternCompiler({ state }); return state.patternCompiler.compileRegExp(layout, snippet as any); } @@ -1283,8 +1284,8 @@ export class Tempo { /** Microseconds of the millisecond (0-999) */ get us() { return this.toDateTime().microsecond as t.us } /** Nanoseconds of the microsecond (0-999) */ get ns() { return this.toDateTime().nanosecond as t.ns } /** Fractional seconds (e.g., 0.123456789) */ get ff() { return +(`0.${pad(this.ms, 3)}${pad(this.us, 3)}${pad(this.ns, 3)}`) } - /** IANA Time Zone ID (e.g., 'Australia/Sydney') */ get tz() { return this.toDateTime().timeZoneId } - /** Temporal Calendar ID (e.g., 'iso8601' | 'gregory') */ get cal() { return this.toDateTime().calendarId } + /** IANA Time Zone ID (e.g., 'Australia/Sydney') */ get tz() { return getTemporalIds(this.toDateTime())[0] } + /** Temporal Calendar ID (e.g., 'iso8601' | 'gregory') */ get cal() { return getTemporalIds(this.toDateTime())[1] } /** Unix timestamp (defaults to milliseconds) */ get ts() { return this.epoch[this.#local.config.timeStamp] } /** Short month name (e.g., 'Jan') */ get mmm() { return Tempo.MONTH.keyOf(this.toDateTime().month as t.Month) } /** Full month name (e.g., 'January') */ get mon() { return Tempo.MONTHS.keyOf(this.toDateTime().month as t.Month) } @@ -1396,7 +1397,10 @@ export class Tempo { /** returns a Temporal.PlainDateTime representation */ toPlainDateTime() { return this.toDateTime().toPlainDateTime() } /** returns the underlying Temporal.Instant */ toInstant() { return this.toDateTime().toInstant() } - /** the current system time localized to this instance. */toNow() { return instant().toZonedDateTimeISO(this.tz).withCalendar(this.cal) } + /** the current system time localized to this instance. */toNow() { + const [tz, cal] = getTemporalIds(this.#local.config.timeZone, this.#local.config.calendar); + return instant().toZonedDateTimeISO(tz).withCalendar(cal); + } /** the date-time as a standard `Date` object. */ toDate() { return new Date(this.toDateTime().round({ smallestUnit: enums.ELEMENT.ms }).epochMilliseconds) } /** Custom JSON serialization for `JSON.stringify`. */ toJSON() { return { ...this.#local.config, value: this.toString() } } /** iso8601 string representation of the date-time. */ diff --git a/packages/tempo/test/core/tempo_guard.test.ts b/packages/tempo/test/core/tempo_guard.test.ts index 76ad331..e832eb9 100644 --- a/packages/tempo/test/core/tempo_guard.test.ts +++ b/packages/tempo/test/core/tempo_guard.test.ts @@ -38,7 +38,12 @@ describe('Master Guard Extension', () => { // 3. '@@@banana@@@' now passes guard const t = new Tempo('@@@banana@@@'); - // expect(t.parse.lazy).toBe(true); + }); + + it('should permit numeric inputs which bypass the guard', () => { + const t = new Tempo(20260507); expect(t).toBeInstanceOf(Tempo); + // Accessing a property triggers the parse + expect(t.yy).toBe(2026); }); }); diff --git a/packages/tempo/test/engine/engine.guard.test.ts b/packages/tempo/test/engine/engine.guard.test.ts index 73297a7..93d2abe 100644 --- a/packages/tempo/test/engine/engine.guard.test.ts +++ b/packages/tempo/test/engine/engine.guard.test.ts @@ -56,6 +56,7 @@ describe('engine.guard (Master Guard)', () => { it('should fail on empty or non-string input', () => { const guard = createMasterGuard(['apple']); expect(guard.test('')).toBe(false); + expect(guard.test(' ')).toBe(false); expect(guard.test(null as any)).toBe(false); expect(guard.test(123 as any)).toBe(false); }); diff --git a/packages/tempo/test/support/setup.console-spy.ts b/packages/tempo/test/support/setup.console-spy.ts index 712f735..64d843c 100644 --- a/packages/tempo/test/support/setup.console-spy.ts +++ b/packages/tempo/test/support/setup.console-spy.ts @@ -2,11 +2,11 @@ import { vi, afterAll, beforeEach } from 'vitest'; // Named spies for each console method export const spies = { - error: vi.spyOn(console, 'error').mockImplementation(() => {}), - warn: vi.spyOn(console, 'warn').mockImplementation(() => {}), - debug: vi.spyOn(console, 'debug').mockImplementation(() => {}), - log: vi.spyOn(console, 'log').mockImplementation(() => {}), - info: vi.spyOn(console, 'info').mockImplementation(() => {}), + error: vi.spyOn(console, 'error').mockImplementation(() => { }), + warn: vi.spyOn(console, 'warn').mockImplementation(() => { }), + debug: vi.spyOn(console, 'debug').mockImplementation(() => { }), + log: vi.spyOn(console, 'log').mockImplementation(() => { }), + info: vi.spyOn(console, 'info').mockImplementation(() => { }), } beforeEach(() => { From 87ab725c5405783d222eb4c2f59ca5c306029dae Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Thu, 7 May 2026 17:33:02 +1000 Subject: [PATCH 12/16] CHANGELOG --- packages/tempo/CHANGELOG.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/tempo/CHANGELOG.md b/packages/tempo/CHANGELOG.md index 3a18a65..fa957aa 100644 --- a/packages/tempo/CHANGELOG.md +++ b/packages/tempo/CHANGELOG.md @@ -5,19 +5,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [2.9.2] - 2026-05-07 +## [2.9.2] - 2026-05-08 ### Added +- **Resilient ID Extraction**: Unified `timeZoneId` and `calendarId` extraction into a single spec-resilient helper `getTemporalIds`. This ensures 100% compatibility across both spec-final and Node.js V8 harmony environments by resolving nested property drift (`timeZone.id` vs `timeZoneId`). - **Identity-Based Layout Resolution**: Hardened `resolveLayoutClassificationOrder` to support identity-based symbol lookups. This ensures that tokens without descriptions or aliases (such as raw symbols) can be correctly prioritized in preferred layout ordering. - **Named Capture for Separators**: Updated the default `{sep}` snippet to use a named capture group `(?...)`, improving the inspectability of generated regex patterns. ### Changed +- **Non-Recursive Bootstrap**: Hardened the `toNow()` lifecycle and `today` alias to safely access local configuration without triggering circular parsing dependencies. - **Modular Decompression**: Removed the redundant `parse.layout.ts` re-export module and consolidated all layout resolution logic into `engine.layout.ts`. Updated internal Specifiers and test-aliases to point to the new canonical home. - **Node.js Harmony Support**: Updated documentation to highlight native `Temporal` support in Node.js 20+ via the `--harmony-temporal` flag, reducing the need for external polyfills in modern server-side environments. ### Fixed +- **MasterGuard Validation**: Improved the `MasterGuard` scanner to correctly identify and reject whitespace-only strings by implementing explicit match tracking. +- **Symbol Mapping Safety**: Fixed a potential `TypeError` in `AliasEngine` when mapping Symbols without descriptions by hardening the `wordsList` creation logic. - **Utility Security Hardening**: Refactored the `create` and `setPatterns` utilities with robust prototype-shadowing guards. These improvements prevent `TypeError` crashes when interacting with null-prototype objects and guarantee `PatternCompiler` state isolation across concurrent Tempo instances. -- **RegExp Preview Accuracy**: Corrected the documentation example for `Tempo.regexp()` to accurately reflect the anchored outer capture group and named snippet expansions produced by the engine. +- **PatternCompiler Isolation**: Refactored `Tempo.regexp()` to guarantee `PatternCompiler` isolation per-state, preventing unintended cache leakage across inherited registries. +- **UI Accessibility**: Updated documentation button styles to use theme variables, ensuring WCAG 2.1 contrast compliance (4.5:1) for all brand elements. +- **RegExp Preview Accuracy**: Corrected the documentation example for `Tempo.regexp()` to accurately reflect the anchored outer capture group and unique named snippet expansions (`sep`, `sep_1`) produced by the engine. ## [2.9.1] - 2026-05-07 From b75a2092a8f3cdee6e158ff23ddbc8c8352a6c14 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Thu, 7 May 2026 17:51:19 +1000 Subject: [PATCH 13/16] Guard PR review #3 --- .../library/src/common/temporal.library.ts | 25 ++++++--- .../test/common/temporal_library.test.ts | 33 +++++++++++ packages/tempo/plan/RELEASE-D.md | 56 ++++++++----------- packages/tempo/src/engine/engine.composer.ts | 2 +- .../tempo/src/engine/engine.normalizer.ts | 20 +++++-- packages/tempo/src/engine/engine.pattern.ts | 3 +- packages/tempo/src/tempo.class.ts | 18 ++++-- 7 files changed, 104 insertions(+), 53 deletions(-) create mode 100644 packages/library/test/common/temporal_library.test.ts diff --git a/packages/library/src/common/temporal.library.ts b/packages/library/src/common/temporal.library.ts index 450a060..3a63cc6 100644 --- a/packages/library/src/common/temporal.library.ts +++ b/packages/library/src/common/temporal.library.ts @@ -4,7 +4,7 @@ */ import '#library/temporal.polyfill.js'; // ensure Temporal is available -import { isNumber, isObject, isString, isUndefined, isZonedDateTime } from '#library/assertion.library.js'; +import { isNumber, isObject, isString, isUndefined, isDefined, isZonedDateTime } from '#library/assertion.library.js'; /** return the current Temporal.Now.instant */ export function instant() { @@ -84,7 +84,11 @@ export function normaliseFractionalDurations(payload: Record) { * property-bag or ISO string. */ export function toZonedDateTime(bag: Temporal.ZonedDateTimeLike | string, tz: Temporal.TimeZoneLike = 'UTC'): Temporal.ZonedDateTime { - if (isString(bag)) return Temporal.ZonedDateTime.from(`${bag}[${tz}]`); + if (isString(bag)) { + // Detect existing zone designator: bracketed IANA zone ([...]) or numeric offset (±HH:MM, Z) + const hasZone = /\[[^\]]+\]|([+-]\d{2}(:?\d{2})?|Z)$/.test(bag); + return Temporal.ZonedDateTime.from(hasZone ? bag : `${bag}[${tz}]`); + } return Temporal.ZonedDateTime.from(bag); } @@ -112,14 +116,21 @@ export function toInstant(epochNanoseconds: bigint): Temporal.Instant { * Accepts either (tz, cal) strings or a single ZonedDateTime-like object. * Supports both spec-final (flat) and V8 harmony (nested) structures. */ +export function getTemporalIds(zdt: Temporal.ZonedDateTime, cal?: Temporal.CalendarLike): [string, string]; +export function getTemporalIds(tz: Temporal.TimeZoneLike, cal?: Temporal.CalendarLike): [string, string]; export function getTemporalIds(tzOrZdt: any, cal?: any): [string, string] { const fallbackTz = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; - const bag = (isZonedDateTime(tzOrZdt) && isUndefined(cal)) - ? tzOrZdt - : { timeZone: tzOrZdt, calendar: cal }; - const rawTz = bag.timeZoneId ?? bag.timeZone?.id ?? bag.timeZone; - const rawCal = bag.calendarId ?? bag.calendar?.id ?? bag.calendar; + let rawTz: any, rawCal: any; + if (isZonedDateTime(tzOrZdt)) { + // If first arg is ZonedDateTime, use its IDs as source + rawTz = tzOrZdt.timeZoneId ?? tzOrZdt.timeZone?.id ?? tzOrZdt.timeZone; + // If a second argument is provided, it explicitly overrides the ZonedDateTime's calendar + rawCal = isDefined(cal) ? cal : (tzOrZdt.calendarId ?? tzOrZdt.calendar?.id ?? tzOrZdt.calendar); + } else { + rawTz = tzOrZdt; + rawCal = cal; + } // Helper to extract string ID from potential objects (TimeZone, Calendar, or ZonedDateTime) const toId = (v: any): string => { diff --git a/packages/library/test/common/temporal_library.test.ts b/packages/library/test/common/temporal_library.test.ts new file mode 100644 index 0000000..7b92c78 --- /dev/null +++ b/packages/library/test/common/temporal_library.test.ts @@ -0,0 +1,33 @@ +import { toZonedDateTime } from '#library/temporal.library.js'; + +describe('Temporal Library Helpers', () => { + describe('toZonedDateTime', () => { + it('should append [tz] to plain ISO strings', () => { + const zdt = toZonedDateTime('2024-01-01T12:00:00', 'Australia/Sydney'); + expect(zdt.timeZoneId).toBe('Australia/Sydney'); + expect(zdt.toString()).toContain('[Australia/Sydney]'); + }); + + it('should NOT append [tz] to strings that already have a bracketed zone', () => { + const zdt = toZonedDateTime('2024-01-01T12:00:00[UTC]', 'Australia/Sydney'); + expect(zdt.timeZoneId).toBe('UTC'); + expect(zdt.toString()).not.toContain('[Australia/Sydney]'); + }); + + it('should NOT append [tz] to strings that have both an offset and a bracket', () => { + const input = '2024-01-01T12:00:00+11:00[Australia/Sydney]'; + const zdt = toZonedDateTime(input, 'UTC'); + expect(zdt.timeZoneId).toBe('Australia/Sydney'); + expect(zdt.toString()).not.toContain('[UTC]'); + }); + + it('should handle "Z" as a zone designator and pass it through (even if ZonedDateTime.from throws without a bracket)', () => { + const bag = '2024-01-01T12:00:00Z'; + try { + toZonedDateTime(bag, 'Australia/Sydney'); + } catch (e) { + expect((e as Error).message).toContain('requires a time zone ID in brackets'); + } + }); + }); +}); diff --git a/packages/tempo/plan/RELEASE-D.md b/packages/tempo/plan/RELEASE-D.md index e280143..904d393 100644 --- a/packages/tempo/plan/RELEASE-D.md +++ b/packages/tempo/plan/RELEASE-D.md @@ -126,48 +126,36 @@ Modularize all logic related to event/period alias resolution, collision policy, - Easier to extend and maintain event/period handling. - Improved testability and reliability of alias resolution. -### Guard Builder Extraction — Assessment Outline +### Guard Builder Extraction (`engine.guard.ts`) **Purpose:** -Evaluate the value and feasibility of extracting all logic related to token ingestion and fast-fail guard rebuild lifecycle into a dedicated module. +Provides a high-performance scanner for fast-fail validation of layout strings before full regex parsing, ensuring that only plausible input enters the expensive parsing loop. **Boundaries & Responsibilities:** -- Would own the process of ingesting tokens and rebuilding fast-fail guards for parsing. -- Would expose APIs for guard construction, update, and validation. -- Should integrate with the main engine’s parse pipeline and pattern system. +- **Token Ingestion**: Owns the logic for ingesting registered aliases and symbols to build a master word list. +- **Scanning Logic**: Implements a greedy "consume and continue" scanner that handles whitespace, bracketed content (literals), and longest-token matching. +- **Lifecycle Integration**: Synchronizes with `Tempo.init()` and registry reset events to rebuild the guard state dynamically. -**Assessment Steps:** -1. Identify all guard-building and token-ingestion logic in `tempo.class.ts` and helpers. -2. Determine if the logic is sufficiently complex or reused to justify extraction. -3. If justified, outline module boundaries and migration steps similar to previous extractions. -4. If not, document reasons for keeping logic inline. +**Public API:** +- `createMasterGuard(words: (string | symbol)[]): MasterGuard`: Factory for creating a scanner instance from a list of allowed words. +- `MasterGuard.test(input: string): boolean`: Predicate that returns `true` if the input is a valid combination of allowed tokens/characters. -**Risks & Mitigations:** -- Risk: Over-extraction of simple logic. Mitigation: Only extract if complexity or reuse warrants. -- Risk: Integration issues with parse pipeline. Mitigation: Careful interface design and incremental refactor. - -**Expected Improvements (if extracted):** -- Cleaner separation of guard logic. -- Easier to test and update guard-building behavior. +**Implementation Notes:** +- Replaces the legacy inline regex-based guard with a dedicated scanner that correctly handles greedy longest-match priorities (e.g., matching "january" as a single token rather than "jan" + "uary"). +- Hardened to reject whitespace-only or empty strings via internal `matchedAny` tracking. -### Parse Result Normalizer Extraction — Assessment Outline +### Parse Result Normalizer Extraction (`engine.normalizer.ts`) **Purpose:** -Evaluate the value and feasibility of extracting all logic related to match accumulation and parse-result shaping/trace output into a dedicated module. +Centralizes the logic for accumulating raw regex matches and normalizing them into structured `MatchResult` objects, decoupling match-shaping from the core parsing loop. **Boundaries & Responsibilities:** -- Would own the process of normalizing parse results and shaping trace/debug output. -- Would expose APIs for result normalization and trace formatting. -- Should integrate with the main engine’s parse and debug systems. +- **Match Normalization**: Maps raw `RegExpExecArray` capture groups back to their original alias keys and values. +- **Result Accumulation**: Manages the persistent list of results, ensuring that duplicate or overlapping segments are handled consistently. +- **Contextual Resolution**: Provides a hardened "shadow" context for resolving function-based aliases (e.g. `today`) without triggering infinite recursion. -**Assessment Steps:** -1. Identify all result normalization and trace output logic in `tempo.class.ts` and helpers. -2. Determine if the logic is sufficiently complex or reused to justify extraction. -3. If justified, outline module boundaries and migration steps similar to previous extractions. -4. If not, document reasons for keeping logic inline. - -**Risks & Mitigations:** -- Risk: Over-extraction of simple logic. Mitigation: Only extract if complexity or reuse warrants. -- Risk: Integration issues with parse/trace systems. Mitigation: Careful interface design and incremental refactor. +**Public API:** +- `normalizeMatch(match: RegExpExecArray, anchor: ZonedDateTime, context: NormalizerContext): MatchResult`: Transforms a raw regex match into a shaped result object. +- `accumulateResult(result: MatchResult, registry: MatchResult[]): void`: Utility for merging new results into the existing parsing state. -**Expected Improvements (if extracted):** -- Cleaner separation of result normalization logic. -- Easier to test and update parse-result shaping and trace output. +**Implementation Notes:** +- Includes a spec-resilient `toNow()` implementation that handles V8 harmony property drift. +- Decouples alias resolution from the main `Tempo` class state, allowing the normalizer to work safely during the bootstrap and parsing phases. diff --git a/packages/tempo/src/engine/engine.composer.ts b/packages/tempo/src/engine/engine.composer.ts index 50375ed..4f39216 100644 --- a/packages/tempo/src/engine/engine.composer.ts +++ b/packages/tempo/src/engine/engine.composer.ts @@ -21,7 +21,7 @@ export function compose( ): { dateTime: Temporal.ZonedDateTime, timeZone?: string | undefined } { let temporal: TemporalObject | Tempo = today; let timeZone: string | undefined; - let dateTime: Temporal.ZonedDateTime = today; + let dateTime: Temporal.ZonedDateTime | undefined; switch (type) { case 'Void': diff --git a/packages/tempo/src/engine/engine.normalizer.ts b/packages/tempo/src/engine/engine.normalizer.ts index 028a619..065701d 100644 --- a/packages/tempo/src/engine/engine.normalizer.ts +++ b/packages/tempo/src/engine/engine.normalizer.ts @@ -8,6 +8,13 @@ import { resolveTermMutation } from './engine.term.js'; import enums from '#tempo/support/support.enum.js'; import * as t from '../tempo.type.js'; +/** + * Maximum depth for recursive alias resolution. + * This ceiling (50) is generous to accommodate complex alias chains while remaining well above + * the PatternCompiler.matcher depth limit (~10), preventing stack overflows during normalization. + */ +const MAX_TEMPO_RESOLVE_DEPTH = 50; + /** * Context provided to the normalizer to handle recursion and state management. */ @@ -124,7 +131,7 @@ export function resolveAliases( if (!register) continue; const aliasKey = register.name; - if (resolvingKeys.size > 50 || resolvingKeys.has(aliasKey)) { + if (resolvingKeys.size > MAX_TEMPO_RESOLVE_DEPTH || resolvingKeys.has(aliasKey)) { const msg = `Infinite recursion detected in Tempo resolution for: ${aliasKey}`; state.errored = true; if (TempoClass) (TempoClass as any)[sym.$logError](state.config, new RangeError(msg)); @@ -155,10 +162,10 @@ export function resolveAliases( const resolving = new Set(resolvingKeys); resolving.add(res.key); - const prevAnchor: any = state.anchor; + const subAnchor: any = state.anchor; state.anchor = dateTime; const resMatch = subParse(res.value, dateTime, resolving); - state.anchor = prevAnchor; + state.anchor = subAnchor; if (resMatch.type === 'Temporal.ZonedDateTime') dateTime = resMatch.value; @@ -200,6 +207,11 @@ export function accumulateResult(state: t.Internal.State, ...rest: Partial + existing.match === match.match && + existing.source === match.source && + String(existing.anchor ?? '') === String(match.anchor ?? '') + ); + if (!isDuplicate) res.push(match); } } diff --git a/packages/tempo/src/engine/engine.pattern.ts b/packages/tempo/src/engine/engine.pattern.ts index cd73003..9c0105d 100644 --- a/packages/tempo/src/engine/engine.pattern.ts +++ b/packages/tempo/src/engine/engine.pattern.ts @@ -77,7 +77,8 @@ export class PatternCompiler { } if (res && name.includes('.')) { // wrap dotted extensions for identification - const safeName = name.replace(/\./g, '_'); + let safeName = name.trim().replace(/[^A-Za-z0-9_$]/g, '_'); + if (!/^[A-Za-z_$]/.test(safeName)) safeName = `_${safeName}`; if (!res.startsWith(`(?<${safeName}>`)) res = `(?<${safeName}>${res})`; } diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index c6fdc98..0ed3704 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -828,12 +828,8 @@ export class Tempo { static regexp(layout: string | RegExp, snippet?: Snippet) { const state = (this as any)[$Internal](); -<<<<<<< feature/tempo-pattern-compiler if (!state.patternCompiler || state.patternCompiler.state !== state) state.patternCompiler = new PatternCompiler({ state }); -======= - state.patternCompiler ??= new PatternCompiler({ state }); ->>>>>>> main return state.patternCompiler.compileRegExp(layout, snippet as any); } @@ -985,6 +981,8 @@ export class Tempo { /** constructor options */ #options = {} as t.Options; /** instantiation Temporal Instant */ #now: Temporal.Instant; /** underlying Temporal ZonedDateTime */ #zdt!: Temporal.ZonedDateTime; + /** memoized TimeZone ID */ #tz?: string; + /** memoized Calendar ID */ #cal?: string; /** indicator that the instance failed to parse */ #errored = false; /** temporary anchor used during parsing */ #anchor: Temporal.ZonedDateTime | undefined; /** prebuilt formats, for convenience */ #fmt!: any; @@ -1131,6 +1129,14 @@ export class Tempo { return (this.constructor as typeof Tempo).hasModule(name); } + /** returns a [timezone, calendar] tuple derived from the underlying date-time. */ + #temporalIds(): [string, string] { + if (!this.#tz || !this.#cal) { + [this.#tz, this.#cal] = getTemporalIds(this.toDateTime()); + } + return [this.#tz, this.#cal]; + } + /** Resolve the instance to a Temporal.ZonedDateTime (with optional callback) */ #resolve(cb?: (zdt: Temporal.ZonedDateTime) => T): T | Temporal.ZonedDateTime { if (!this.#zdt) { @@ -1288,8 +1294,8 @@ export class Tempo { /** Microseconds of the millisecond (0-999) */ get us() { return this.toDateTime().microsecond as t.us } /** Nanoseconds of the microsecond (0-999) */ get ns() { return this.toDateTime().nanosecond as t.ns } /** Fractional seconds (e.g., 0.123456789) */ get ff() { return +(`0.${pad(this.ms, 3)}${pad(this.us, 3)}${pad(this.ns, 3)}`) } - /** IANA Time Zone ID (e.g., 'Australia/Sydney') */ get tz() { return getTemporalIds(this.toDateTime())[0] } - /** Temporal Calendar ID (e.g., 'iso8601' | 'gregory') */ get cal() { return getTemporalIds(this.toDateTime())[1] } + /** IANA Time Zone ID (e.g., 'Australia/Sydney') */ get tz() { return this.#temporalIds()[0] } + /** Temporal Calendar ID (e.g., 'iso8601' | 'gregory') */ get cal() { return this.#temporalIds()[1] } /** Unix timestamp (defaults to milliseconds) */ get ts() { return this.epoch[this.#local.config.timeStamp] } /** Short month name (e.g., 'Jan') */ get mmm() { return Tempo.MONTH.keyOf(this.toDateTime().month as t.Month) } /** Full month name (e.g., 'January') */ get mon() { return Tempo.MONTHS.keyOf(this.toDateTime().month as t.Month) } From ed0ae0084300793bd70c5930a0014763e509d45e Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Fri, 8 May 2026 10:08:49 +1000 Subject: [PATCH 14/16] Guard PR review #4 --- .../tempo/plan => doc}/release-process.md | 0 package-lock.json | 731 ++++++------------ package.json | 4 +- .../library/src/common/temporal.library.ts | 4 +- .../library/src/common/temporal.polyfill.ts | 13 + packages/tempo/doc/installation.md | 9 +- packages/tempo/doc/tempo.config.md | 33 + packages/tempo/package.json | 30 +- packages/tempo/plan/.WISHLIST.md | 294 ------- packages/tempo/plan/RELEASE-D.md | 161 ---- packages/tempo/plan/alias-migration-phase2.md | 51 -- .../tempo/plan/alias-precedence-strategy.md | 57 -- packages/tempo/plan/alias.registration.md | 68 -- packages/tempo/plan/configuration.md | 190 ----- .../tempo/plan/release-c-prefilter-summary.md | 113 --- packages/tempo/src/discrete/discrete.index.ts | 2 - packages/tempo/src/engine/engine.term.ts | 8 +- packages/tempo/src/module/module.duration.ts | 9 +- .../module.format.ts} | 0 packages/tempo/src/module/module.index.ts | 2 + packages/tempo/src/module/module.mutate.ts | 5 +- .../module.parse.ts} | 4 +- .../tempo/src/plugin/extend/extend.ticker.ts | 11 +- packages/tempo/src/plugin/plugin.index.ts | 1 + packages/tempo/src/plugin/plugin.type.ts | 82 +- packages/tempo/src/plugin/plugin.util.ts | 13 +- packages/tempo/src/plugin/term/term.index.ts | 4 +- .../tempo/src/plugin/term/term.quarter.ts | 2 +- packages/tempo/src/plugin/term/term.season.ts | 2 +- .../tempo/src/plugin/term/term.timeline.ts | 2 +- packages/tempo/src/plugin/term/term.type.ts | 71 ++ .../tempo/src/plugin/{ => term}/term.util.ts | 8 +- packages/tempo/src/plugin/term/term.zodiac.ts | 2 +- packages/tempo/src/support/support.runtime.ts | 3 +- packages/tempo/src/tempo.class.ts | 23 +- packages/tempo/src/tempo.index.ts | 3 +- packages/tempo/src/tempo.type.ts | 9 +- packages/tempo/src/tsconfig.json | 6 +- packages/tempo/test/README.md | 2 +- .../parse.prefilter.numeric-safety.test.ts | 2 +- packages/tempo/test/tsconfig.json | 8 +- packages/tempo/vitest.config.ts | 8 +- 42 files changed, 440 insertions(+), 1610 deletions(-) rename {packages/tempo/plan => doc}/release-process.md (100%) delete mode 100644 packages/tempo/plan/.WISHLIST.md delete mode 100644 packages/tempo/plan/RELEASE-D.md delete mode 100644 packages/tempo/plan/alias-migration-phase2.md delete mode 100644 packages/tempo/plan/alias-precedence-strategy.md delete mode 100644 packages/tempo/plan/alias.registration.md delete mode 100644 packages/tempo/plan/configuration.md delete mode 100644 packages/tempo/plan/release-c-prefilter-summary.md delete mode 100644 packages/tempo/src/discrete/discrete.index.ts rename packages/tempo/src/{discrete/discrete.format.ts => module/module.format.ts} (100%) create mode 100644 packages/tempo/src/module/module.index.ts rename packages/tempo/src/{discrete/discrete.parse.ts => module/module.parse.ts} (99%) create mode 100644 packages/tempo/src/plugin/term/term.type.ts rename packages/tempo/src/plugin/{ => term}/term.util.ts (98%) diff --git a/packages/tempo/plan/release-process.md b/doc/release-process.md similarity index 100% rename from packages/tempo/plan/release-process.md rename to doc/release-process.md diff --git a/package-lock.json b/package-lock.json index 6e26a60..6c9e609 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tempo-monorepo", - "version": "2.9.1", + "version": "2.9.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tempo-monorepo", - "version": "2.9.1", + "version": "2.9.2", "workspaces": [ "packages/*" ], @@ -18,7 +18,7 @@ "@types/jquery": "^4.0.0", "@types/node": "^25.5.2", "@vitest/ui": "^2.1.8", - "cross-env": "^7.0.3", + "cross-env": "^10.1.0", "rollup": "^4.60.1", "tslib": "^2.8.1", "tsx": "^4.21.0", @@ -385,6 +385,13 @@ } } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -1316,6 +1323,177 @@ "win32" ] }, + "node_modules/@se-oss/deasync": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@se-oss/deasync/-/deasync-1.0.1.tgz", + "integrity": "sha512-Ha7P/xCNxOuH72BNdLRWs4TT8rsMMrERnHtfKWBeTWu+UFW9OBTrRgfZJOlbAAQFR0l4Q30cpAn8CuR7PXWcPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^4.37.0" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@se-oss/deasync-darwin-arm64": "1.0.1", + "@se-oss/deasync-darwin-x64": "1.0.1", + "@se-oss/deasync-linux-arm64-gnu": "1.0.1", + "@se-oss/deasync-linux-arm64-musl": "1.0.1", + "@se-oss/deasync-linux-x64-gnu": "1.0.1", + "@se-oss/deasync-linux-x64-musl": "1.0.1", + "@se-oss/deasync-win32-arm64-msvc": "1.0.1", + "@se-oss/deasync-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/@se-oss/deasync-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@se-oss/deasync-darwin-arm64/-/deasync-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-0YWmIDEGQfW3GGopmZHhfA6mamsG0HFKZhmBzHVyFiMKkJts8kpQwGbGrWlK8eOAoPCihOsG6tCotYR3p7HZaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@se-oss/deasync-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@se-oss/deasync-darwin-x64/-/deasync-darwin-x64-1.0.1.tgz", + "integrity": "sha512-r3FRTLIXqGqOb1DjTLW3YhO/Dd1vA2qRLP0Ym3Wmk3yMv6c/nm15zg6UVoXbgBu8cjbvcsI/OfbHPdErmjMWsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@se-oss/deasync-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@se-oss/deasync-linux-arm64-gnu/-/deasync-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-657uRew7fZAx663Li03ilLV2lN09Dqb/NxawlDu8kKmboK1BLitHJRS+taiT5oFZqyIDrU45tlQKfCrW0p0sYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@se-oss/deasync-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@se-oss/deasync-linux-arm64-musl/-/deasync-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-IE3fIQPIJtko4lx9sRam+Zz0P4xbpAPJgDCHaz6k9cP1yUvVI179B4IZRnFx0GyjyQpm0KhHoIGHJc4KUmA81Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@se-oss/deasync-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@se-oss/deasync-linux-x64-gnu/-/deasync-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XQl7etZESGIjIraCyxfAey8ZTIJUB4dUFU3rPR/xLVn9bKpZGlJLIms0z3hoHX9mipO+Cqo53vK4IVm6A7U/ww==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@se-oss/deasync-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@se-oss/deasync-linux-x64-musl/-/deasync-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-vWgFAZlqImqMV6jhCWV7C9wcCS1eb1ajhlKduBRPfyUxxkoObe+EqTG2BKJAuafxp3/KS1aUsIMJma9mhwFvow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@se-oss/deasync-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@se-oss/deasync-win32-arm64-msvc/-/deasync-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-yk7lEE7Zd8GX7o6CuUbg3HnnmUhBx4tgfn5ff3eoq05CgBO6Z3ZtL4l+utAe1cxcFaXPhyvcgnHYyA4OF544tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@se-oss/deasync-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@se-oss/deasync-win32-x64-msvc/-/deasync-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-ixizmuLGRPGyAesWUNWVzVOsvuunNb/qMqU8SmjfLR/vVgzdQEkSHFf+fkX9GXPN6FDv+DAz5uskTzhjUyCXFA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@shikijs/core": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", @@ -1985,16 +2163,6 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@xmldom/xmldom": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", - "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.6" - } - }, "node_modules/algoliasearch": { "version": "5.50.2", "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.50.2.tgz", @@ -2021,16 +2189,6 @@ "node": ">= 14.0.0" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2058,13 +2216,6 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, - "license": "ISC" - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -2135,45 +2286,6 @@ "node": ">= 16" } }, - "node_modules/cheerio": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", - "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cheerio-select": "^1.5.0", - "dom-serializer": "^1.3.2", - "domhandler": "^4.2.0", - "htmlparser2": "^6.1.0", - "parse5": "^6.0.1", - "parse5-htmlparser2-tree-adapter": "^6.0.1", - "tslib": "^2.2.0" - }, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/cheerio-select": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.6.0.tgz", - "integrity": "sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "css-select": "^4.3.0", - "css-what": "^6.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.3.1", - "domutils": "^2.8.0" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -2185,16 +2297,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/copy-anything": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", @@ -2212,22 +2314,21 @@ } }, "node_modules/cross-env": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", "dev": true, "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.1" + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" }, "bin": { - "cross-env": "src/bin/cross-env.js", - "cross-env-shell": "src/bin/cross-env-shell.js" + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" }, "engines": { - "node": ">=10.14", - "npm": ">=6", - "yarn": ">=1" + "node": ">=20" } }, "node_modules/cross-spawn": { @@ -2245,36 +2346,6 @@ "node": ">= 8" } }, - "node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2344,75 +2415,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, "node_modules/emoji-regex-xs": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", @@ -2499,16 +2501,6 @@ "node": ">=18" } }, - "node_modules/esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -2677,36 +2669,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -2757,26 +2719,6 @@ "devOptional": true, "license": "Apache-2.0" }, - "node_modules/juice": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/juice/-/juice-8.1.0.tgz", - "integrity": "sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cheerio": "1.0.0-rc.10", - "commander": "^6.1.0", - "mensch": "^0.3.4", - "slick": "^1.12.2", - "web-resource-inliner": "^6.0.1" - }, - "bin": { - "juice": "bin/juice" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", @@ -2836,29 +2778,12 @@ "markdown-it": "bin/markdown-it.mjs" } }, - "node_modules/markdown-it-mathjax3": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/markdown-it-mathjax3/-/markdown-it-mathjax3-4.3.2.tgz", - "integrity": "sha512-TX3GW5NjmupgFtMJGRauioMbbkGsOXAAt1DZ/rzzYmTHqzkO1rNAdiMD4NiruurToPApn2kYy76x02QN26qr2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "juice": "^8.0.0", - "mathjax-full": "^3.2.0" - } - }, - "node_modules/mathjax-full": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.1.tgz", - "integrity": "sha512-aUz9o16MGZdeiIBwZjAfUBTiJb7LRqzZEl1YOZ8zQMGYIyh1/nxRebxKxjDe9L+xcZCr2OHdzoFBMcd6VnLv9Q==", + "node_modules/mathxyjax3": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/mathxyjax3/-/mathxyjax3-0.8.3.tgz", + "integrity": "sha512-eXjFaiyQsTdVOeTFoFaFJ/r1FITpB1f9c5MW4FETfcoVV/+xa5SD9pS05AwugzL/gNuDtWXrTOSmoD2e0Du+UA==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esm": "^3.2.25", - "mhchemparser": "^4.1.0", - "mj-context-menu": "^0.6.1", - "speech-rule-engine": "^4.0.6" - } + "license": "MIT" }, "node_modules/mdast-util-to-hast": { "version": "13.2.1", @@ -2889,20 +2814,6 @@ "dev": true, "license": "MIT" }, - "node_modules/mensch": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", - "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==", - "dev": true, - "license": "MIT" - }, - "node_modules/mhchemparser": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz", - "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/micromark-util-character": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", @@ -2997,19 +2908,6 @@ ], "license": "MIT" }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/minisearch": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", @@ -3024,13 +2922,6 @@ "dev": true, "license": "MIT" }, - "node_modules/mj-context-menu": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", - "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -3067,19 +2958,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, "node_modules/oniguruma-to-es": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", @@ -3092,23 +2970,6 @@ "regex-recursion": "^6.0.2" } }, - "node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse5": "^6.0.1" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3453,16 +3314,6 @@ "node": ">=18" } }, - "node_modules/slick": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", - "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==", - "dev": true, - "license": "MIT (http://mootools.net/license.txt)", - "engines": { - "node": "*" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3494,31 +3345,6 @@ "node": ">=0.10.0" } }, - "node_modules/speech-rule-engine": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.1.4.tgz", - "integrity": "sha512-i/VCLG1fvRc95pMHRqG4aQNscv+9aIsqA2oI7ZQS51sTdUcDHYX6cpT8/tqZ+enjs1tKVwbRBWgxut9SWn+f9g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xmldom/xmldom": "0.9.10", - "commander": "13.1.0", - "wicked-good-xpath": "1.3.0" - }, - "bin": { - "sre": "bin/sre" - } - }, - "node_modules/speech-rule-engine/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -3652,13 +3478,6 @@ "node": ">=6" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, - "license": "MIT" - }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -4163,6 +3982,19 @@ "@esbuild/win32-x64": "0.27.3" } }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typedoc": { "version": "0.28.19", "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.19.tgz", @@ -4350,16 +4182,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/valid-data-url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", - "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -4614,118 +4436,6 @@ } } }, - "node_modules/web-resource-inliner": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", - "integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1", - "escape-goat": "^3.0.0", - "htmlparser2": "^5.0.0", - "mime": "^2.4.6", - "node-fetch": "^2.6.0", - "valid-data-url": "^3.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/web-resource-inliner/node_modules/domhandler": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", - "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.0.1" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/web-resource-inliner/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/web-resource-inliner/node_modules/escape-goat": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", - "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/web-resource-inliner/node_modules/htmlparser2": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", - "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^3.3.0", - "domutils": "^2.4.2", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/fb55/htmlparser2?sponsor=1" - } - }, - "node_modules/web-resource-inliner/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4759,13 +4469,6 @@ "node": ">=8" } }, - "node_modules/wicked-good-xpath": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", - "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==", - "dev": true, - "license": "MIT" - }, "node_modules/yaml": { "version": "2.8.3", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", @@ -4795,7 +4498,7 @@ }, "packages/library": { "name": "@magmacomputing/library", - "version": "2.9.1", + "version": "2.9.2", "license": "MIT", "dependencies": { "tslib": "^2.8.1" @@ -4814,23 +4517,33 @@ }, "packages/tempo": { "name": "@magmacomputing/tempo", - "version": "2.9.1", + "version": "2.9.2", "license": "MIT", "dependencies": { "tslib": "^2.8.1" }, "devDependencies": { "@js-temporal/polyfill": "^0.5.1", - "@magmacomputing/library": "2.9.1", + "@magmacomputing/library": "2.9.2", "@rollup/plugin-alias": "^6.0.0", - "cross-env": "^7.0.3", "magic-string": "^0.30.21", - "markdown-it-mathjax3": "^4.3.2", + "markdown-it-mathjax3": "^5.2.0", "typedoc": "^0.28.19", "typedoc-plugin-markdown": "^4.11.0", "typedoc-vitepress-theme": "^1.1.2", "vitepress": "^1.6.4" } + }, + "packages/tempo/node_modules/markdown-it-mathjax3": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/markdown-it-mathjax3/-/markdown-it-mathjax3-5.2.0.tgz", + "integrity": "sha512-R+XAy5/7vSGuhG9Z0/cJm6zKxOzStcScfSKVwoarh4nBra+v1KClvbALr/xFTEe9iQhwfQM4SJnO68LXL+btMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@se-oss/deasync": "^1.0.1", + "mathxyjax3": "^0.8.3" + } } } } diff --git a/package.json b/package.json index 7767a05..1d233c5 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@types/jquery": "^4.0.0", "@types/node": "^25.5.2", "@vitest/ui": "^2.1.8", - "cross-env": "^7.0.3", + "cross-env": "^10.1.0", "rollup": "^4.60.1", "tslib": "^2.8.1", "tsx": "^4.21.0", @@ -41,4 +41,4 @@ "overrides": { "esbuild": "^0.25.0" } -} \ No newline at end of file +} diff --git a/packages/library/src/common/temporal.library.ts b/packages/library/src/common/temporal.library.ts index 3a63cc6..63e0272 100644 --- a/packages/library/src/common/temporal.library.ts +++ b/packages/library/src/common/temporal.library.ts @@ -4,7 +4,7 @@ */ import '#library/temporal.polyfill.js'; // ensure Temporal is available -import { isNumber, isObject, isString, isUndefined, isDefined, isZonedDateTime } from '#library/assertion.library.js'; +import { isNumber, isObject, isString, isDefined, isZonedDateTime } from '#library/assertion.library.js'; /** return the current Temporal.Now.instant */ export function instant() { @@ -36,7 +36,7 @@ export function getOffsets(timeZone: string, year = 2024) { //** use a fixed ref /** return whether the given (or current) date is in Daylight Savings */ export function isDST(date?: Temporal.ZonedDateTime | string, timeZone: string = Intl.DateTimeFormat().resolvedOptions().timeZone) { - const zdt = (typeof date === 'string') + const zdt = isString(date) ? Temporal.ZonedDateTime.from(date) : (date ?? instant().toZonedDateTimeISO(timeZone)); const { jan, jul } = getOffsets(zdt.timeZoneId, zdt.year); diff --git a/packages/library/src/common/temporal.polyfill.ts b/packages/library/src/common/temporal.polyfill.ts index baa8fb3..f4e4bbf 100644 --- a/packages/library/src/common/temporal.polyfill.ts +++ b/packages/library/src/common/temporal.polyfill.ts @@ -16,4 +16,17 @@ if (typeof globalThis.Temporal === 'undefined') { ); } +// 🛡️ Sane Implementation Check +// Some early native implementations (e.g. Node 22.0.x) are incomplete and crash on basic arithmetic. +// If you encounter "unimplemented code" or V8_Fatal crashes, manually load a polyfill before Tempo. +try { + // Minimal test for a feature known to be a stub in early implementations + if (typeof Temporal.Now.zonedDateTimeISO === 'function') { + const zdt = Temporal.Now.zonedDateTimeISO(); + if (typeof zdt.add !== 'function') throw new Error('Incomplete Temporal implementation'); + } +} catch (err) { + console.warn('Tempo: Native Temporal implementation appears incomplete. Consider loading a polyfill.'); +} + export { } diff --git a/packages/tempo/doc/installation.md b/packages/tempo/doc/installation.md index 05b4d7b..570786f 100644 --- a/packages/tempo/doc/installation.md +++ b/packages/tempo/doc/installation.md @@ -10,7 +10,11 @@ As of 13 January 2026, Chrome 144 has shipped `Temporal`, and Firefox 139 also includes native `Temporal` support. -While Node.js does not yet enable `Temporal` by default, recent versions (Node 20+) support it via the `--harmony-temporal` flag. This allows you to use `Tempo` without an external polyfill package. +While Node.js does not yet enable `Temporal` by default, recent versions (Node 20+) support it via the `--harmony-temporal` flag (or `--js-temporal` in newer builds). This allows you to use `Tempo` without an external polyfill package. + +::: warning +Native implementations in Node.js are currently considered experimental and may be incomplete or contain bugs that cause unexpected crashes (e.g., `V8_Fatal` errors in some builds). For mission-critical stability, we strongly recommend using `@js-temporal/polyfill`. +::: Please verify support in your actual target runtime(s) and add a polyfill only when needed. @@ -51,6 +55,9 @@ If you are using Node.js 20+, you can enable native `Temporal` support without i node --harmony-temporal my-app.js ``` +> [!WARNING] +> Use native support with caution. Some Node.js builds contain incomplete Temporal implementations that can crash on complex arithmetic. See [Temporal Polyfill Note](#temporal-polyfill-note) for details. + ### Node.js (with Polyfill) The polyfill import shown here is conditional guidance, not required for all environments. diff --git a/packages/tempo/doc/tempo.config.md b/packages/tempo/doc/tempo.config.md index 3f19231..d0968aa 100644 --- a/packages/tempo/doc/tempo.config.md +++ b/packages/tempo/doc/tempo.config.md @@ -150,6 +150,9 @@ Tempo.init({ | `mode` | `'auto' \| 'strict' \| 'defer'` | `'auto'` | Controls the hydration strategy (e.g., `defer` for Zero-Cost creation). | | `silent` | `boolean` | `false` | Suppresses console output. Combined with `catch: true` for silent failover. | | `ignore` | `string \| string[]` | `['at']` | List of noise words to be stripped before parsing. | +| `layoutOrder` | `string[]` | Built-in Order | The sequence in which layouts are attempted during parsing. | +| `preFilter` | `boolean` | `false` | Enables the Parse Planner to skip irrelevant layouts based on input classification. | +| `planner` | `PlannerOptions` | `undefined` | Grouped configuration for `layoutOrder` and `preFilter`. | --- @@ -237,6 +240,36 @@ console.log(t.toString()); // Resolved correctly (noise words stripped) ::: +--- + +### 🚀 5.4 Parse Planner & Pre-filtering + +For high-performance applications, you can enable the **Parse Planner** to optimize the pattern-matching loop. + +#### `preFilter` (Boolean) +When enabled, Tempo performs a fast upfront classification of the input string (detecting digits, letters, colons, etc.) and skips layouts that cannot possibly match. + +- **Purely numeric inputs**: Skips `event`, `period`, `wkd`, and `rel` layouts. +- **Alpha-only inputs**: Skips time-heavy layouts like `hms` or `off`. +- **Colon detected**: Prioritizes time-based layouts (`tm`, `dtm`) to find a match faster. + +```javascript +Tempo.init({ preFilter: true }); +``` + +#### `layoutOrder` (Array) +You can manually define the order in which layouts are attempted. This is useful if you know your data primarily uses a specific format (e.g., ISO dates) and want to avoid checking other layouts first. + +```javascript +Tempo.init({ + layoutOrder: ['ymd', 'dt', 'tm', 'rel'] +}); +``` + +::: tip +**Observability**: Set `debug: true` along with `preFilter: true` to see a detailed "Planner summary" in the console, showing how many layouts were skipped for a given input. +::: + --- ## 📊 Summary of Tiers diff --git a/packages/tempo/package.json b/packages/tempo/package.json index 1ac7707..22376e2 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -104,16 +104,16 @@ "default": "./dist/support/*.js" }, "#tempo/parse": { - "development": "./src/discrete/discrete.parse.ts", - "default": "./dist/discrete/discrete.parse.js" + "development": "./src/module/module.parse.ts", + "default": "./dist/module/module.parse.js" }, "#tempo/format": { - "development": "./src/discrete/discrete.format.ts", - "default": "./dist/discrete/discrete.format.js" + "development": "./src/module/module.format.ts", + "default": "./dist/module/module.format.js" }, - "#tempo/discrete": { - "development": "./src/discrete/discrete.index.ts", - "default": "./dist/discrete/discrete.index.js" + "#tempo/module": { + "development": "./src/module/module.index.ts", + "default": "./dist/module/module.index.js" }, "#tempo/*.js": { "development": "./src/*.ts", @@ -161,17 +161,13 @@ "types": "./dist/module/module.mutate.d.ts", "import": "./dist/module/module.mutate.js" }, - "./discrete": { - "types": "./dist/discrete/discrete.index.d.ts", - "import": "./dist/discrete/discrete.index.js" - }, "./format": { - "types": "./dist/discrete/discrete.format.d.ts", - "import": "./dist/discrete/discrete.format.js" + "types": "./dist/module/module.format.d.ts", + "import": "./dist/module/module.format.js" }, "./parse": { - "types": "./dist/discrete/discrete.parse.d.ts", - "import": "./dist/discrete/discrete.parse.js" + "types": "./dist/module/module.parse.d.ts", + "import": "./dist/module/module.parse.js" }, "./library": { "types": "./dist/library.index.d.ts", @@ -201,6 +197,7 @@ "test:ci": "cross-env TZ=America/New_York LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 vitest run", "test:ci:prefilter": "cross-env TZ=America/New_York LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 TEMPO_PREFILTER_CI=true vitest run", "repl": "tsx --conditions=development -i --harmony-temporal --import ./bin/repl.ts", + "safe": "tsx --conditions=development -i --import ./bin/temporal-polyfill.ts --import ./bin/repl.ts", "bare": "tsx --conditions=development -i --harmony-temporal", "core": "cross-env TEMPO_LITE=true tsx --conditions=development -i --harmony-temporal --import ./bin/core.ts", "parse": "cross-env TEMPO_LITE=true tsx --conditions=development -i --harmony-temporal --import ./bin/parse.ts", @@ -226,9 +223,8 @@ "@js-temporal/polyfill": "^0.5.1", "@magmacomputing/library": "2.9.2", "@rollup/plugin-alias": "^6.0.0", - "cross-env": "^7.0.3", "magic-string": "^0.30.21", - "markdown-it-mathjax3": "^4.3.2", + "markdown-it-mathjax3": "^5.2.0", "typedoc": "^0.28.19", "typedoc-plugin-markdown": "^4.11.0", "typedoc-vitepress-theme": "^1.1.2", diff --git a/packages/tempo/plan/.WISHLIST.md b/packages/tempo/plan/.WISHLIST.md deleted file mode 100644 index 24c7d41..0000000 --- a/packages/tempo/plan/.WISHLIST.md +++ /dev/null @@ -1,294 +0,0 @@ -# Tempo v2.0.2 Wishlist (Post-Lockdown) - -## Architecture & Design -- [ ] **Investigate Code-Smells**: Review complex methods (like `#parse`) for high cyclomatic complexity and potential refactoring. - -## Pervasive Hard-Freeze Proxies -We should refactor our core "securing" mechanisms to return our throwing Proxies instead of just freezing the object. This ensures that every "Immutable" object in the Tempo ecosystem—from the main Tempo instance to the tiny ResolvedRange objects—will loudly complain if someone tries to mutate them in the REPL. - -- Pervasive Hard-Freeze Proxies -This plan refactors the library's immutability system to use throwing Proxies instead of native `Object.freeze`. This ensures that all immutable objects provide a consistent "throw-on-mutation" experience, even in non-strict environments like the Node.js REPL. -- User Review Required -> [!IMPORTANT] -> Transitioning from `Object.freeze` to Proxies for all immutable objects is a significant architectural shift. While it provides the best possible UX for developers, it may have minor performance implications and identity-check considerations (e.g., `proxy === target` is false). However, given Tempo's focus on safety and developer experience, this is the recommended path. -- Proposed Changes -- [Component] Library (Utility & Class Decorators) -- [MODIFY] [utility.library.ts](file:///home/michael/Project/magma/packages/library/src/common/utility.library.ts) -- Update `secure()` to wrap the final frozen object in a `proxify()` call. -- This ensures that resolving a term (like `t.term.qtr`) returns a Proxy that throws on mutation. -- [MODIFY] [class.library.ts](file:///home/michael/Project/magma/packages/library/src/common/class.library.ts) -- Update the `@Immutable` decorator to return `proxify(this)` from the constructor. -- This ensures that every `Tempo` instance is a Proxy that throws on mutation. -- [Component] Library (Proxy System) -- [MODIFY] [proxy.library.ts](file:///home/michael/Project/magma/packages/library/src/common/proxy.library.ts) -- Refine error messages to distinguish between "Frozen Objects" (instances) and "Read-Only Delegators" (terms/formats). -- Ensure all mutation traps (including `defineProperty` and `deleteProperty`) are robustly covered. -- Verification Plan -- Manual Verification (REPL) -- Start the REPL: `npm run repl`. -- Verify the following all throw explicit `TypeErrors`: - - `delete t1.term` - - `t1.term = {}` - - `t1.term.qtr = 1` - - `delete t1.term.quarter.month` - -## Parse: `layoutOrder` option — arbitrary layout sequencing - -Today the only reordering supported is the pairwise `mdyLayouts` swap for locale-based `dmy`↔`mdy` flipping. -There is no way for a developer to reorder layouts arbitrarily (e.g. promote `wkd` above `tm`, or demote `rel` entirely). - -Proposed: a `layoutOrder` option that accepts an ordered array of layout names and rebuilds the parse sequence accordingly. - -```ts -Tempo.init({ - layoutOrder: ['wkd', 'dt', 'dtm', 'tmd', 'tm', 'ymd', 'mdy', 'dmy', 'off', 'rel'] -}) -``` - -- The internal layout map is already built from `Object.entries()` so the infrastructure is ready. -- Any name not listed would retain its relative position at the end (non-destructive by default). -- Could be supplied at static-init level or per-instance level (same as `layout`/`snippet`). -- `mdyLayouts` swap would still apply on top, after the custom order is set. - -## Parse: input-class pre-filtering — conditional layout selection - -Today every layout pattern is tested against every input string in order, with no early-outs based on -the nature of the input. Proposed: a fast upfront classification of the raw input that gates which -layout subsets are even tried. - -**Critical constraint — numeric fall-through must be preserved:** -The parse pipeline already has two upstream numeric escapes that happen *before* the layout loop and -must never be disrupted: -- `isIntegerLike(trim)` — matches strings ending in `n` (e.g. `1234567890n`) → treated as BigInt - nanoseconds and returned immediately, bypassing all layouts. -- `type === 'Number'` (i.e. the input was supplied as a JS `number`) — goes through the layout loop - but is interpreted by the composer as epoch milliseconds if no layout matches. - -Any input-class pre-filtering must therefore be applied **only after** those two escapes have already -passed, and must not gate-out purely-numeric inputs from reaching the layout loop — because a numeric -string like `'20260424'` is a valid compact date and must be tried against layout patterns first. -The epoch-millisecond fallback is the *last resort*, not a skip condition. - -Example input classes and their natural consequences: - -| Input class | Layouts to skip | -|---|---| -| Purely numeric | `event`, `period`, `wkd`, `rel` — but NOT the layout loop itself | -| Contains only letters | `hms`, `dmy6`, `mdy6`, `ymd6`, `off` | -| Contains a colon | Bias time layouts first | -| Contains `ago`/`hence` | Jump straight to `rel` | -| Exactly 6 digits | Only try `hms`, `dmy6`, `mdy6`, `ymd6` | -| Exactly 8 digits | Only try compact date layouts | - -Benefits: -- Performance: fewer regex executions per parse call, particularly valuable for hot loops (Ticker, bulk formatting). -- Correctness: eliminates the `{hh}` partial-match latent trap for letter-containing inputs. -- The layout loop remains authoritative — this is transparent pre-filtering, not a replacement. - -Implementation note: input-class detection would run once per parse call using simple -`/^[0-9]+$/`, `/[a-zA-Z]/`, `.length` checks before the layout loop. Could be expressed -as a small decision table or a set of bit-flags (e.g. `hasDigits`, `hasLetters`, `hasColon`, -`hasSeparator`) computed in a single pass over the input string. - -## Further discrete-module candidates (post parse/format/duration/mutation split) - -The parse and format standalone split is already a major win. Additional high-ROI splits still available: - -- Parse Pipeline Planner module: - owns input classification, pre-filter policy, and candidate-layout selection. -- Layout Order Resolver module: - owns base order, locale `mdyLayouts` swaps, and future `layoutOrder` handling. -- Pattern Compiler + Cache module: - owns snippet/layout expansion, regex compilation, and cache invalidation. -- Config Option Mutation Engine module: - owns option-application switchboard (especially parse-related options). -- Alias Resolution Engine module: - owns event/period collision policy and snippet rebinding into layout-aware groups. - -Secondary extractions (lower urgency, still useful): - -- Guard Builder module: - owns token ingestion and fast-fail guard rebuild lifecycle. -- Parse Result Normalizer module: - owns match accumulation and parse-result shaping/trace output. - -## Release-boundary modularization roadmap - -Goal: execute all of the above, but spread refactors to minimize regression risk and preserve API stability. - -### Release A (next immediate release): low-risk, high-leverage foundations - -- [ ] Extract Layout Order Resolver (pure ordering logic only). -- [ ] Add test matrix for ordering determinism: - - base order unchanged - - mdy swap pairs still deterministic - - custom-inserted layouts maintain stable relative order -- [ ] Add telemetry/debug hook for resolved layout order (debug mode only). - -Exit criteria: -- No behavioral change except internally equivalent ordering. -- Existing full test suite green. - -### Release B: planner path and configuration surface - -- [ ] Extract Parse Pipeline Planner (candidate selection interface only). -- [ ] Implement `layoutOrder` option through Layout Order Resolver. -- [ ] Ensure `layoutOrder` composes safely with `mdyLayouts` (order then swap). -- [ ] Add docs and migration notes for ordering customization. - -Exit criteria: -- `layoutOrder` works for both global init and local instance options. -- Legacy behavior preserved when `layoutOrder` is omitted. - -### Release C: conditional pre-filtering optimization - -- [ ] Enable input-class pre-filtering in planner. -- [ ] Preserve numeric safety constraints: - - `isIntegerLike(trim)` remains an early BigInt nanosecond escape. - - Number-input epoch-millisecond fallback remains last-resort behavior. - - Numeric strings still run through layout matching first. -- [ ] Add perf benchmarks before/after for regex-attempt count and constructor parse latency. - -Exit criteria: -- Equal parse correctness against regression suite. -- Measurable reduction in unnecessary pattern checks. - -### Release D: deeper decomposition cleanup - ---- - -#### Parse Result Normalizer Extraction — Assessment Outline - -**Purpose:** -Evaluate the value and feasibility of extracting all logic related to match accumulation and parse-result shaping/trace output into a dedicated module. - -**Boundaries & Responsibilities:** -- Would own the process of normalizing parse results and shaping trace/debug output. -- Would expose APIs for result normalization and trace formatting. -- Should integrate with the main engine’s parse and debug systems. - -**Assessment Steps:** -1. Identify all result normalization and trace output logic in `tempo.class.ts` and helpers. -2. Determine if the logic is sufficiently complex or reused to justify extraction. -3. If justified, outline module boundaries and migration steps similar to previous extractions. -4. If not, document reasons for keeping logic inline. - -**Potential Affected Files:** -- `src/tempo.class.ts` -- `src/support/tempo.util.ts` -- `src/tempo.type.ts` - -**Risks & Mitigations:** -- Risk: Over-extraction of simple logic. Mitigation: Only extract if complexity or reuse warrants. -- Risk: Integration issues with parse/trace systems. Mitigation: Careful interface design and incremental refactor. - -**Expected Improvements (if extracted):** -- Cleaner separation of result normalization logic. -- Easier to test and update parse-result shaping and trace output. - -#### Alias Resolution Engine Extraction — Detailed Outline - -**Purpose:** -Modularize all logic related to event/period alias resolution, collision policy, and snippet rebinding into layout-aware groups for clarity, maintainability, and extensibility. - -**Boundaries & Responsibilities:** -- Accepts event/period definitions and manages alias mapping and collision detection. -- Handles rebinding of snippets into layout-aware groups. -- Exposes clear APIs for resolving aliases and reporting collisions. -- Integrates with the main engine to ensure correct event/period resolution during parsing. - -**Migration Steps:** -1. Identify and extract all alias resolution logic from `tempo.class.ts` and related helpers into a new module (e.g., `engine.alias.ts`). -2. Define clear interfaces for alias registration, lookup, and collision reporting. -3. Refactor the main engine and plugin system to use the new module’s APIs. -4. Add/expand unit tests for alias resolution, collision handling, and rebinding. -5. Document the new module’s API and update internal references. - -**Affected Files:** -- `src/tempo.class.ts` (extraction and refactor) -- `src/support/tempo.util.ts` (if helpers are involved) -- `src/tempo.type.ts` (type updates if needed) - -**Risks & Mitigations:** -- Risk: Incorrect alias resolution or missed collisions. Mitigation: Add focused unit tests and regression tests. -- Risk: Integration issues with plugin/event/period systems. Mitigation: Incremental refactor and thorough testing. - -**Expected Improvements:** -- Cleaner separation of concerns for alias logic. -- Easier to extend and maintain event/period handling. -- Improved testability and reliability of alias resolution. - - -#### Pattern Compiler + Cache Extraction Plan (to be detailed) - -*Purpose*: Modularize all logic related to snippet/layout expansion, regex compilation, and cache invalidation. - -*Outline (to be expanded):* -1. Identify all code responsible for pattern expansion and regex compilation. -2. Define clear module boundaries and interfaces for the compiler and cache. -3. Move related logic from the main engine/class to the new module. -4. Implement cache invalidation and update mechanisms. -5. Add focused unit tests for the new module. -6. Update documentation and internal references. - ---- - -#### Pattern Compiler + Cache Extraction — Detailed Outline - -**Purpose:** -Modularize all logic related to snippet/layout expansion, regex compilation, and pattern cache management for clarity, testability, and maintainability. - -**Boundaries & Responsibilities:** -- Accepts layout/snippet definitions and returns compiled RegExp objects. -- Handles recursive expansion of layout placeholders (e.g., `{yy}`, `{mm}`) using snippet registries. -- Manages a cache of compiled patterns for performance. -- Exposes cache invalidation/refresh methods for dynamic config changes. -- Provides a clear interface for the rest of the Tempo engine to request compiled patterns. - -**Migration Steps:** -1. Extract `compileRegExp`, `setPatterns`, and related helpers from `tempo.util.ts` into a new module (e.g., `pattern.compiler.ts`). -2. Move or wrap memoization/caching logic (from `function.library.ts`) as needed for pattern compilation. -3. Refactor `tempo.class.ts` and other consumers to use the new module’s interface. -4. Ensure all pattern/snippet/layout definitions in `tempo.default.ts` are compatible with the new module. -5. Add/expand unit tests for pattern expansion, compilation, and cache behavior. -6. Document the new module’s API and update internal references. - -**Affected Files:** -- `src/support/tempo.util.ts` (extraction) -- `src/tempo.class.ts` (refactor to use new module) -- `src/support/tempo.default.ts` (ensure compatibility) -- `src/tempo.type.ts` (type updates if needed) -- `library/src/common/function.library.ts` (cache/memoization logic) - -**Risks & Mitigations:** -- Risk: Subtle bugs in recursive expansion or cache invalidation. Mitigation: Add focused unit tests and regression tests. -- Risk: Performance regressions if cache is not used correctly. Mitigation: Benchmark before/after and optimize cache usage. - -**Expected Improvements:** -- Lower cyclomatic complexity in the main engine. -- Easier to test and reason about pattern expansion and compilation. -- Clearer cache management and invalidation. - -### Release D: Recommended Release Strategy - -To balance safety and efficiency: - -- Release the two major extractions (Pattern Compiler + Cache, Alias Resolution Engine) as separate point-releases for focused testing and easier rollback. -- Batch the assessment/documentation steps (Guard Builder, Parse Result Normalizer, affected files/modules, improvements/risks) into a single follow-up release if they are lightweight. - -This approach allows for incremental progress, clear regression points, and manageable review cycles. - -## Next sequence kickoff (start now) - -The recommended execution sequence begins here: - -1. Extract Layout Order Resolver. -2. Extract Parse Pipeline Planner shell (no filtering yet). -3. Wire `layoutOrder` option to resolver. -4. Add input-class pre-filtering behind a guarded internal flag. - -Implementation guardrails: - -- Keep each step behavior-preserving unless explicitly feature-gated. -- Land with dedicated tests in each step before moving to the next. -- Avoid combining planner + filtering + compiler extraction in one release. diff --git a/packages/tempo/plan/RELEASE-D.md b/packages/tempo/plan/RELEASE-D.md deleted file mode 100644 index 904d393..0000000 --- a/packages/tempo/plan/RELEASE-D.md +++ /dev/null @@ -1,161 +0,0 @@ -# Release D: Deeper Decomposition Cleanup - -## Overview -This release focuses on modularizing and refactoring the parsing and pattern-matching internals of Tempo for improved maintainability, testability, and extensibility. The goal is to extract tightly-scoped modules for pattern compilation, alias resolution, guard building, and result normalization, with clear boundaries and robust test coverage. - -## Task Breakdown & Tracking - - -### Pattern Compiler + Cache Extraction -- [x] Extract `compileRegExp`, `setPatterns`, and helpers to new module (PatternCompiler) -- [x] Integrate memoization/caching logic as needed (PatternCompiler cache) -- [x] Refactor engine and consumers to use new PatternCompiler module -- [x] Ensure compatibility with snippet/layout definitions -- [x] Add/expand unit tests for pattern logic and cache -- [x] Update documentation and references - -### Alias Resolution Engine Extraction -- [x] Extract alias resolution logic to new module -- [x] Define interfaces for registration, lookup, collision -- [x] Refactor engine and plugins to use new APIs -- [x] Add/expand unit tests for alias/collision -- [x] Update documentation and references - -### Guard Builder Extraction -- [x] Identify all guard-building/token-ingestion logic -- [x] Assess complexity/reuse for extraction -- [x] Outline module boundaries if justified -- [x] Move "Scan-and-Consume" logic to `engine.guard.ts` -- [x] Refactor `tempo.class.ts` to use `createMasterGuard` - -### Parse Result Normalizer Extraction -- [x] Identify all result normalization/trace logic -- [x] Assess complexity/reuse for extraction -- [x] Outline module boundaries if justified -- [x] Extract normalization logic to `engine.normalizer.ts` - -## Expected Improvements and Risks - -**Expected Improvements:** -- Lower cyclomatic complexity and improved maintainability in the main engine. -- Clearer separation of concerns between parsing, pattern compilation, alias resolution, guard building, and result normalization. -- Easier to test, extend, and debug individual modules. -- More robust and explicit cache management. -- Improved reliability and correctness through focused unit and regression tests. -- Smoother onboarding for new contributors due to modular structure and documentation. - -**Risks:** -- Potential for subtle integration bugs during refactor, especially in recursive expansion, alias resolution, or cache invalidation. -- Temporary performance regressions if cache or pattern compilation is not optimized. -- Over-extraction of simple logic could increase codebase complexity without clear benefit. -- Increased review and testing overhead for each extraction step. - -**Mitigations:** -- Incremental, well-documented releases with dedicated tests at each step. -- Benchmarking and profiling before/after major changes. -- Only extract modules where complexity or reuse justifies it. -- Maintain clear interfaces and documentation for all new modules. - -## Affected Files and Modules - -The following files and modules are likely to be affected by the decomposition and extractions in Release D: - -- `src/tempo.class.ts` — Main engine logic, source of most extraction candidates. -- `src/support/tempo.util.ts` — Utility functions for pattern, guard, and normalization logic. -- `src/support/tempo.default.ts` — Core snippet, layout, and pattern definitions. -- `src/tempo.type.ts` — Type definitions for parse, pattern, and result structures. -- `src/support/tempo.register.ts` — May require updates for cache/registry management. -- `library/src/common/function.library.ts` — Memoization and cache utilities. -- `src/parse/parse.layout.ts` — Layout order and planner logic (if not already modularized). -- Any new modules created for: Pattern Compiler + Cache, Alias Resolution Engine, Guard Builder, Parse Result Normalizer. -- Test files covering parsing, pattern matching, event/period handling, and normalization. - -## Detailed Outlines - -### Pattern Compiler + Cache Extraction — Detailed Outline -**Purpose:** -Modularize all logic related to snippet/layout expansion, regex compilation, and pattern cache management for clarity, testability, and maintainability. - -**Boundaries & Responsibilities:** -- Accepts layout/snippet definitions and returns compiled RegExp objects. -- Handles recursive expansion of layout placeholders (e.g., `{yy}`, `{mm}`) using snippet registries. -- Manages a cache of compiled patterns for performance. -- Exposes cache invalidation/refresh methods for dynamic config changes. -- Provides a clear interface for the rest of the Tempo engine to request compiled patterns. - -**Migration Steps:** -1. Extract `compileRegExp`, `setPatterns`, and related helpers from `tempo.util.ts` into a new module (e.g., `pattern.compiler.ts`). -2. Move or wrap memoization/caching logic (from `function.library.ts`) as needed for pattern compilation. -3. Refactor `tempo.class.ts` and other consumers to use the new module’s interface. -4. Ensure all pattern/snippet/layout definitions in `tempo.default.ts` are compatible with the new module. -5. Add/expand unit tests for pattern expansion, compilation, and cache behavior. -6. Document the new module’s API and update internal references. - -**Risks & Mitigations:** -- Risk: Subtle bugs in recursive expansion or cache invalidation. Mitigation: Add focused unit tests and regression tests. -- Risk: Performance regressions if cache is not used correctly. Mitigation: Benchmark before/after and optimize cache usage. - -**Expected Improvements:** -- Lower cyclomatic complexity in the main engine. -- Easier to test and reason about pattern expansion and compilation. -- Clearer cache management and invalidation. - -### Alias Resolution Engine Extraction — Detailed Outline -**Purpose:** -Modularize all logic related to event/period alias resolution, collision policy, and snippet rebinding into layout-aware groups for clarity, maintainability, and extensibility. - -**Boundaries & Responsibilities:** -- Accepts event/period definitions and manages alias mapping and collision detection. -- Handles rebinding of snippets into layout-aware groups. -- Exposes clear APIs for resolving aliases and reporting collisions. -- Integrates with the main engine to ensure correct event/period resolution during parsing. - -**Migration Steps:** -1. Identify and extract all alias resolution logic from `tempo.class.ts` and related helpers into a new module (e.g., `alias.engine.ts`). -2. Define clear interfaces for alias registration, lookup, and collision reporting. -3. Refactor the main engine and plugin system to use the new module’s APIs. -4. Add/expand unit tests for alias resolution, collision handling, and rebinding. -5. Document the new module’s API and update internal references. - -**Risks & Mitigations:** -- Risk: Incorrect alias resolution or missed collisions. Mitigation: Add focused unit tests and regression tests. -- Risk: Integration issues with plugin/event/period systems. Mitigation: Incremental refactor and thorough testing. - -**Expected Improvements:** -- Cleaner separation of concerns for alias logic. -- Easier to extend and maintain event/period handling. -- Improved testability and reliability of alias resolution. - -### Guard Builder Extraction (`engine.guard.ts`) -**Purpose:** -Provides a high-performance scanner for fast-fail validation of layout strings before full regex parsing, ensuring that only plausible input enters the expensive parsing loop. - -**Boundaries & Responsibilities:** -- **Token Ingestion**: Owns the logic for ingesting registered aliases and symbols to build a master word list. -- **Scanning Logic**: Implements a greedy "consume and continue" scanner that handles whitespace, bracketed content (literals), and longest-token matching. -- **Lifecycle Integration**: Synchronizes with `Tempo.init()` and registry reset events to rebuild the guard state dynamically. - -**Public API:** -- `createMasterGuard(words: (string | symbol)[]): MasterGuard`: Factory for creating a scanner instance from a list of allowed words. -- `MasterGuard.test(input: string): boolean`: Predicate that returns `true` if the input is a valid combination of allowed tokens/characters. - -**Implementation Notes:** -- Replaces the legacy inline regex-based guard with a dedicated scanner that correctly handles greedy longest-match priorities (e.g., matching "january" as a single token rather than "jan" + "uary"). -- Hardened to reject whitespace-only or empty strings via internal `matchedAny` tracking. - -### Parse Result Normalizer Extraction (`engine.normalizer.ts`) -**Purpose:** -Centralizes the logic for accumulating raw regex matches and normalizing them into structured `MatchResult` objects, decoupling match-shaping from the core parsing loop. - -**Boundaries & Responsibilities:** -- **Match Normalization**: Maps raw `RegExpExecArray` capture groups back to their original alias keys and values. -- **Result Accumulation**: Manages the persistent list of results, ensuring that duplicate or overlapping segments are handled consistently. -- **Contextual Resolution**: Provides a hardened "shadow" context for resolving function-based aliases (e.g. `today`) without triggering infinite recursion. - -**Public API:** -- `normalizeMatch(match: RegExpExecArray, anchor: ZonedDateTime, context: NormalizerContext): MatchResult`: Transforms a raw regex match into a shaped result object. -- `accumulateResult(result: MatchResult, registry: MatchResult[]): void`: Utility for merging new results into the existing parsing state. - -**Implementation Notes:** -- Includes a spec-resilient `toNow()` implementation that handles V8 harmony property drift. -- Decouples alias resolution from the main `Tempo` class state, allowing the normalizer to work safely during the bootstrap and parsing phases. diff --git a/packages/tempo/plan/alias-migration-phase2.md b/packages/tempo/plan/alias-migration-phase2.md deleted file mode 100644 index d506b06..0000000 --- a/packages/tempo/plan/alias-migration-phase2.md +++ /dev/null @@ -1,51 +0,0 @@ -# Alias Migration: Phase 2 - Full Resolution Engine - -This document outlines the remaining tasks to complete the migration from legacy alias management to the centralized `AliasEngine` architecture. The goal is to move all interpretation and mutation logic out of the Parser and into the Engine. - -## 1. Consolidate Resolution Context (The "Host" Object) -Currently, `discrete.parse.ts` manually constructs a "pseudo-Tempo" `host` object to pass into functional aliases. This logic should be standardized and moved to a helper. - -- [x] Create a `getResolutionContext(state, dateTime)` helper in `support` or `AliasEngine`. -- [x] Ensure the context provides `add`, `subtract`, `with`, `set`, and time-unit accessors. -- [x] Remove the manual host construction from `discrete.parse.ts`. - -## 2. Hardened Clock Snapping -Aliases that resolve to a time-string (`hh:mm[:ss]`) currently have two different paths depending on whether they are static or functional. - -- [x] **Standardize Paths**: Both static and functional aliases should trigger the "snap" path if they match `Match.clock`. -- [x] **Fix Precision Leak**: Ensure that snapping to a clock-time clears ALL sub-second components (ms, us, ns) from the anchor. -- [x] **Support High-Precision**: Update the snapping logic to support `hh:mm:ss.ffffff` patterns natively. -- [x] **Engine-Level Detection**: Move the `Match.clock` test into `AliasEngine.resolveAlias`. - -## 3. Rich Alias Results -Instead of returning a raw `string | number`, the `AliasEngine` should return a structured result object. - -- [x] Define `AliasResult` interface: - ```typescript - interface AliasResult { - value: string; - key: string; // The original baseName (e.g., 'noon') - type: 'evt' | 'per'; - source: 'global' | 'local'; - isClock: boolean; // True if it matched Match.clock - isFunction: boolean; - } - ``` -- [x] Update `resolveAlias` to return this structure. - -## 4. Parser Cleanup -With the Engine handling the "what" and "how" of resolution, the Parser can focus on the "when". - -- [x] Refactor `parseGroups` in `discrete.parse.ts` to consume the new `AliasResult`. -- [x] Remove manual string-splitting and mutation logic from the Parser. -- [x] Leverage the `source` metadata from the result instead of manually parsing regex group names (like `evt1_0`). -- [x] Extract `host` context construction to a helper. - -## 5. Lifecycle & Monitoring -- [x] Implement `AliasEngine.getVersion()` or similar to allow `Tempo` class to detect registry changes without deep-cloning. -- [x] Audit `tempo.class.ts` for any remaining direct access to `parse.event` or `parse.period`. - ---- - -> [!IMPORTANT] -> **Priority 1**: Hardening the clock-snapping logic and fixing the sub-second precision leak. diff --git a/packages/tempo/plan/alias-precedence-strategy.md b/packages/tempo/plan/alias-precedence-strategy.md deleted file mode 100644 index f0d8a7e..0000000 --- a/packages/tempo/plan/alias-precedence-strategy.md +++ /dev/null @@ -1,57 +0,0 @@ -# Alias Precedence Strategy (Custom Over Built-in) - -## Context -A parser conflict was identified where a user-defined alias (`half-hour`) did not override the built-in pattern (`half[ -]?hour`). - -Implemented behavior now gives user-defined `event` and `period` aliases precedence over existing built-ins by placing incoming aliases first in evaluation order. - -## Risks Of Custom-First Precedence -1. Behavioral change risk - - Existing consumers that relied on built-ins winning may observe changed parse results. - -2. Pattern overlap ambiguity - - Regex-like aliases can overlap in non-obvious ways, making the selected winner surprising. - -3. Global side effects - - A single custom alias can change parsing behavior globally after `Tempo.init()` or plugin registration. - -4. Ordering sensitivity - - If precedence is based only on merge order, results can vary depending on discovery/store/options composition. - -## Why Not Use Symbol Keys For Public Aliases -Using `Symbol` for alias keys is not recommended as a public API: -- Alias definitions are string/regex patterns and must be converted into regex groups. -- Discovery and storage payloads are JSON/string keyed; Symbols are not portable in that flow. -- Symbols reduce inspectability and debuggability for users. - -Recommended: keep string keys for matching, optionally use internal metadata (including Symbols if desired) for identity bookkeeping only. - -## Current Mitigation Implemented -1. Custom aliases are evaluated before existing aliases. -2. Collision warnings are emitted when an incoming alias appears to overlap an existing alias pattern. - -## Recommended Follow-Up Improvements -1. Add explicit priority metadata - - Introduce structured alias records with fields like `source`, `priority`, and `insertionIndex`. - - Suggested default ordering: custom > plugin > built-in. - -2. Add strict conflict mode - - Optional config mode that throws on ambiguous overlaps instead of only warning. - -3. Improve collision diagnostics - - Include winning and losing aliases and origin (`builtin/custom/plugin`) in warning messages. - -4. Add deterministic override tests - - Custom exact override over built-in regex. - - Plugin override over built-in. - - Non-overlapping aliases remain stable. - - Strict mode throws on overlap. - -5. Add canonicalization policy (optional) - - Consider normalizing common punctuation/spacing variants for consistency. - - Only apply if it does not break existing regex-driven alias power. - -## Practical Guidance For Future Alias Additions -- Prefer specific patterns over broad regexes. -- Avoid introducing aliases that can match common built-in forms unless override is intentional. -- When overriding a built-in alias, add tests that assert both match winner and output behavior. diff --git a/packages/tempo/plan/alias.registration.md b/packages/tempo/plan/alias.registration.md deleted file mode 100644 index 5f256fa..0000000 --- a/packages/tempo/plan/alias.registration.md +++ /dev/null @@ -1,68 +0,0 @@ -tempo.class will invoke $setEvents() as part of the global, sandbox and instance setup. -events will be an array of [eventName, eventTarget] -where eventName is a plain string or a regex-like string (e.g. "xmas( )?eve"), and eventTarget is a string or Function that returns a string. -the eventTarget will name the date-string (e.g. "25-Dec") that should be interpreted when this eventName is detected by the parse-engine. - -$setEvents will then run "events=ownEntries();" on the list of Events it has been provided (most likely just the Default, but could be more from global-discovery, localStorage, etc.). - -If there are no events (which can happen), $setEvents exits... nothing to do. - -If there are events, it should - check if there is an 'own' shape.aliasEngine, else allocate a 'new AliasEngine(...)" -the new aliasEngine should contain a reference to it's parent object... nothing for global, global for sandbox, global-or-sandbox for instances. -This hierarchy is important for Event resolution (see below). -each new aliasEngine should calculate it's own 'depth'... that is, - global => 0, - sandbox => 1+ (increasing for each sandbox created from another sandbox), - instance => 1 (if direct child of global) or => 2+ (if direct child of a sandbox) - -$setEvents should then call aliasEngine.clear('event')... not sure if this is absolutely necessary, but couldn't hurt. - -$setevents should then call "const groups = aliasEngine.registerEvents(events);" -to pass control to the shape's aliasEngine instance. - -That instance will go through the 'events' array, and for each: - stash some related information into the Engine's instance so we can track - ### a sequential number to be allocated on an Event - ### the baseName - ### the eventTarget - ### the eventName (? not sure if this is needed ?) - a Set on the instance will track calc'd 'baseName' - it will also output a 'warn' if it detects that a baseName has already been used (whether in the current events-array or up the proto-chain). - -Once the registration process is complete, it should return a regex-like string back to the caller in tempo.class. -The string will contain (from lowest to highest in the proto-chain) a calculated named-group regex source, with "(?eventTarget)" - the section will be "{depth}evt{index}" where depth is the aliasEngine's instance depth (0, 1, 2, etc.) and index is the sequential number that was assigned to an Event. - For example, passing in ['xmas','25-Dec'] from the global shape will have the registration return "((?<0evt1>xmas))" - For example, later passing in [bday; '20-May'] from an instance shape will have the registration return "((?<1evt1>bday)|(?<0evt1>xmas))" - -When assembling the string to be returned (pipe-delimited named-group regex-source), the registration should: - ensure lower-depth regex-sources are returned prior to higher-depth - ensure that if a lower-depth is marked as a 'collision', then any higher-depth with that same baseName will be excluded - -To use a Period as an example, assuming an instance wants to override a global definition of 'noon': - "new Tempo('noon': {period: {noon:'11:00'}}); - We would expect the depth for the Tempo-instance to be '1' (direct child of global shape) - We would expect the index to be '1' (first Period alias detected) - We would expect the registerEvents to return "((?<1per1>noon)|... the rest of the global Periods *except* where baseName is 'noon')" - -When the calculated alias-string is returned to tempo.class, it will then update it's shadow the definition of the parent's snippets for Token.evt and Token.per. -tempo.class then calls setPatterns which will build the actual patterns (based on the current Layouts / Snippets) - -## Event Resolution -when the parsing engine detects a match against the patterns, and it finds a named-group with the pattern evt or per, then it knows it has an alias to de-reference. - -It will find the aliasEngine that is associated with the current tempo-level being parse (global, sandbox, instance). - -It will then invoke that aliasEngine's instance's method resolveEvent (or resolvePeriod) by passing in the named-group and the 'this' reference. - -The aliasEngine will decode the 'depth' from the alias argument (the leading digits before the 'evt' or 'per' portion of the string), and travel up the proto-chain til it finds the correct instance that matches that depth. - -The aliasEngine will then decode the 'index' from the alias argument (the trailing digit after the 'evt' or 'per' portion of the string) - -That resolved instance will lookup its own registry of Aliases for the index of the eventTarget. - -If the retrieved eventTarget is a string, it will return it to the parsing engine. -If the retrieved eventTarget is a Function, it will invoke the function (binding the 'this' context), and invoke a .toString() on the result before passing it back to the eventTarget;' - -* what to do if the alias resolution is cyclic ? diff --git a/packages/tempo/plan/configuration.md b/packages/tempo/plan/configuration.md deleted file mode 100644 index 43e1d0a..0000000 --- a/packages/tempo/plan/configuration.md +++ /dev/null @@ -1,190 +0,0 @@ -We ran into a problem with one of our test-cases yesterday. - -The test-case was: 'dynamic period alias with `this` binding (e.g. half-hour)' -Tempo.init({ - period: { - 'half-hour': function (this: Tempo) { - return `${this.hh}:30` - } - } - }) - const t = new Tempo('half-hour') - expect(t.format('{mi}:{ss}')).toBe('30:00') - expect(t.hh).toBe(new Tempo().hh) - -The Error: Actual did not match Expected. - -// -The issue turned out to be a clash in alias naming... -1) There is an Period alias registered already in tempo.default -{'half[ -]?hour': function (this: Tempo) { - return this.add({ minutes: 30 }); - }} - -2) This clashed with the instance-specific definition in the test-case. - -3) The fix was to allow instance-specific definitions to override global definitions. - -// -But this raises a bigger issue: what to do with name-clashes. -This discussion document explores that topic, focussing mainly on Event and Period aliases: -"Should Tempo allow name-clashes?" -"What to do when it detects one?" - -Our choices are "last one wins", or "first one wins", or "error out". - -At first blush, it seems to me that "error out" is the best choice, as it will prevent unexpected behavior. -But the whole design of Tempo is based around "config-merging" and a desire to provide a nice API for -applications upon which to build. - -One option is to have Tempo.init() be a function that returns a new Tempo class, which has the -instance-specific definitions baked in. This would allow for name-clashing at the global level, -but prevent it at the instance level. - -Example: -Tempo = Tempo.init({ Tempo.default }) - -Or, we could design things such that if name-clash is detected, it uses the new definition and issues a warning (as well as recording the #parse.result array to assist debugging). The challenge with this is the load that is introduced to check every Event/Period for a possible clash not only in 'name' but in the 'pattern' as well. What if a user defines a Event/Period, and then later defines another one with the same name, but a different pattern? Would the second one override the first one, or would it be ignored? Would the user be notified of the override? Which would be more likely? - -The risk is that this introduces complexity and non-determinism. -That would make debugging more difficult than it needs to be. - -// AI response - -I've reviewed the issues regarding alias naming clashes. Here are my thoughts on how we should handle this to maintain Tempo's flexibility while ensuring predictability: - -### 1. Conflict Philosophy: "Explicit Shadowing" -Tempo's strength is its "config-merging" capability. We should embrace a **"Last One Wins" (Shadowing)** strategy. -- **Reasoning**: This mirrors how CSS or JS modules work. If a user explicitly provides a configuration in `Tempo.init()`, they are stating their intent to use *that* definition for their application context. -- **Hierarchical Priority**: `Local (Instance) > Factory (init) > Plugin > Global Default`. - -### 2. Conflict Detection & Feedback -While "Last One Wins" should be the resolution, the user should be informed if they are accidentally clobbering a built-in alias. -- **Warning Level**: If an alias key (or pattern) matches an existing one, issue a `console.warn` (when `debug: true`). -- **Heuristic Improvement**: Our current `#isAliasCollision` is a good start. We could improve it by checking for "Pattern Saturation"—if a new regex pattern is a subset of an existing one, or vice versa. - -### 3. The "Sandbox" Factory Pattern -I strongly support the idea of `Tempo.init()` returning a **New Tempo Class**. -- **The Pattern**: - ```typescript - const AppTempo = Tempo.init({ period: { 'half-hour': ... } }); - const t = new AppTempo('half-hour'); // Uses AppTempo's registry - ``` -- **Benefits**: This prevents "Global Pollution". Multiple micro-services or components within the same process could have different Tempo configurations without stepping on each other's toes. -- **Implementation**: We could use a `class factory` that extends the base `Tempo` and overrides the static `#global` state for that specific lineage. - -### 4. Traceability & Debugging -The suggestion to record collisions in a `#parse.result` array is excellent. -- **Discovery Trace**: We should include a `meta` property in the result that indicates which registry entry was used (e.g., `source: "local"`, `source: "plugin:astro"`, `source: "default"`). -- **Collision List**: If a clash occurred but was resolved by shadowing, we should keep a list of the "shadowed" candidates in the instance state to assist developers when they wonder why "noon" isn't behaving as expected. - -### 5. Recommendation -- **Default to Overrides**: Don't throw errors for clashes; it breaks the "nice API" goal. -- **Formalize the Factory**: Move towards the `Tempo.init() -> Class` pattern as the primary way to configure Tempo for an application. -- **Enhance Warnings**: Make the collision warnings more descriptive, showing exactly what was overridden and by whom. - -## 2026-04-24: Sandbox Factory Pattern (Consolidated) - -The Sandbox Factory pattern has been finalized to ensure fully isolated, reproducible Tempo configurations without global state pollution. - -### Key Decisions: -1. **API Naming**: We introduced `Tempo.create(options)` as the primary entry point for creating isolated sandboxes. This separates the concern of "forking the library" from `Tempo.init()`, which remains the tool for managing the global singleton or resetting context. -2. **Inheritance vs. Cloning**: Sandboxes now perform a shallow clone of the parent registries (`event`, `period`, etc.) upon initialization. This prevents the "Registry Leak" issue where adding an alias to a sandbox would inadvertently update the parent. -3. **Local Scope Enforcement**: To simplify alias resolution and ensure traceability, sandboxed class states are tagged with `scope: 'local'`. This causes sandboxed aliases to be recorded with a `source: 'local'` tag in parse results, distinguishing them from the base library's global defaults. -4. **Prototype Chain Resolution**: The `ParseEngine` has been hardened to support recursive `ownEntries` lookups. This allows instances to inherit from their sandbox class, which in turn can inherit (selectively) from the base `Tempo` class, providing a robust polymorphic configuration chain. -5. **Secure Discovery**: Each sandbox is registered in `globalThis` via a unique discovery symbol (or string), allowing the `ParseEngine` to correctly resolve custom periods and events even when called from within complex, nested class contexts. - -### Status: -Fully implemented and verified in `sandbox-factory.test.ts`. 100% pass rate achieved. - -## 2026-04-24: Release A Execution Plan (Layout Order Resolver Foundation) - -This section begins the Release A implementation plan from the wishlist roadmap. - -### Release A Goal -Extract layout-order decision logic into a dedicated resolver while preserving behavior. - -### In-Scope (Release A) -- Extract ordering logic currently embedded in Tempo class internals into a standalone helper/module. -- Keep external API behavior unchanged. -- Add deterministic tests for order resolution and swap semantics. -- Add debug-only visibility of final resolved layout order. - -### Out-of-Scope (Release A) -- No `layoutOrder` option yet. -- No input-class pre-filtering yet. -- No planner-based candidate filtering yet. - -### Current Logic Baseline -- Baseline order comes from parse layout object insertion order. -- Locale preference currently swaps named pairs via `mdyLayouts`. -- Existing behavior must remain byte-for-byte equivalent for default configs. - -### Proposed module split - -Target file: `src/engine/engine.layout.ts` - -This follows the existing `engine.*` convention for internal logic extracted from the class engine -(alongside `engine.composer`, `engine.mutate`, `engine.duration`, `engine.term`, `engine.lexer`). -The `discrete/` folder is reserved for standalone importable functionality (`discrete.parse`, -`discrete.format`) — layout-order resolution is internal engine logic, not a public entry point. - -Candidate exported surface: -```ts -export type LayoutEntry = [symbol, string]; - -export interface ResolveLayoutOrderArgs { - layout: Record; - mdyLayouts: [string, string][]; - isMonthDay: boolean; -} - -export function resolveLayoutOrder(args: ResolveLayoutOrderArgs): Record; -``` - -### Work packages - -1. Extract + wire -- Move swap logic from class private method into resolver function. -- Replace class-internal swap implementation with resolver call. -- Keep exact pair-swap semantics unchanged. - -2. Determinism tests -- Add tests that assert: - - no-op when no swap pair matches - - one pair swap in month-day locales - - reverse swap in non-month-day locales - - multiple pair handling remains stable - - unrelated layout relative order is preserved - -3. Debug visibility -- In debug mode, optionally emit resolved order list for diagnostics. -- Keep output behind existing debug gates; no new public API. - -### Test matrix (Release A) - -Core: -- existing parse/order regressions continue passing -- new resolver unit tests pass - -Behavior safety: -- compact 6/8-digit date tests unchanged -- weekday precedence tests unchanged -- relative/offset precedence tests unchanged - -Integration sweep: -- full `vitest` run must be green - -### Acceptance criteria -- All existing behavior preserved with no public API changes. -- Resolver module introduced and used by Tempo class. -- Order determinism covered by focused tests. -- Full test suite passes. - -### Risk controls -- Land extraction first with behavior parity tests before any feature additions. -- Avoid combining this with planner or pre-filtering work in same release. -- Keep commit scope narrow: extraction + tests + minimal wiring. - -### Next step after Release A -- Start Release B by introducing `layoutOrder` option through this resolver. diff --git a/packages/tempo/plan/release-c-prefilter-summary.md b/packages/tempo/plan/release-c-prefilter-summary.md deleted file mode 100644 index 18e8767..0000000 --- a/packages/tempo/plan/release-c-prefilter-summary.md +++ /dev/null @@ -1,113 +0,0 @@ -# Release C: Parse Prefilter Summary (One-Page) - -Date: 2026-04-25 -Branch: `release-c-layout-order-planner` -Scope: Input-class prefiltering behind feature flag (`parsePrefilter`) - -## Current Implementation Status - -- Planner shell extracted and integrated into parse path. -- Prefilter rules implemented and guarded by `parsePrefilter` (default: `false`). -- Numeric-safety constraints protected by targeted tests. -- Debug-only planner telemetry is available when both: - - `parsePrefilter === true` - - `config.debug` enabled - -## Current Benchmarks (selection phase only) - -Source: `npx tsx scratch/bench.parse.prefilter.ts` - -- Candidate reduction: - - Prefilter off: `168 / 168` - - Prefilter on: `113 / 168` - - Reduction: `32.74%` - -- Timing: - - Prefilter off: `165.161 ms` (5000 iterations, 60000 operations) - - Prefilter on: `165.013 ms` - - Delta: `-0.09%` - -## Current Benchmarks (end-to-end constructor + parse path) - -Source: `npx tsx --conditions=development scratch/bench.parse.prefilter.e2e.ts` (expanded real-world corpus) - -- Timing: - - Prefilter off: `112,821 ms` (1000 iterations, 123,000 operations) - - Prefilter on: `111,793 ms` - - Delta: `-0.91%` - - Checksum parity: outputs are consistent - -### Latest Run (April 26, 2026) - -- Prefilter off: `89,788.897 ms` (1000 iterations, 109,000 operations) -- Prefilter on: `78,703.616 ms` -- Delta: `-12.35%` -- Checksum parity: outputs are consistent - -- Rule-hit distribution (`prefilter:on`): - - `isPureNumeric`: 4 - - `hasColon`: 3 - - `isAlphaOnly`: 2 - - `isSixDigits`: 2 - - `hasAgoHence`: 1 - - `isEightDigits`: 1 - -## Interpretation - -- The planner currently removes about one-third of candidate checks. -- The latest selection-phase micro-benchmark is effectively latency-neutral and slightly favorable. -- Trend improved from earlier iterations (`+28.46%` -> `+13.96%` -> `-0.09%`) after optimization and caching. -- The integrated end-to-end benchmark is also favorable (`-0.91%`) on the current representative corpus. -- The expanded-corpus end-to-end benchmark confirms the performance gain is robust (`-0.91%`), not dataset-specific. -- This indicates the architecture is viable for real-world usage, pending further CI and regression validation before any default-on decision. - -## Safety Status - -- Feature flag remains default-off, so current user behavior is unchanged. -- Tests passing for: - - Planner behavior and rule selection - - Flag wiring (global + per-instance) - - Numeric-safety constraints with prefilter enabled - -## Proposed Go/No-Go Thresholds (for broader test-run enablement) - -Use these gates before enabling `parsePrefilter` in wider CI runs: - -1. Correctness gate: -- All current planner, layout, compact-time, numeric-safety, and full regression parse tests must pass with `parsePrefilter: true` in targeted suites. - -2. Candidate reduction gate: -- Maintain at least `25%` average candidate reduction on representative corpus. - -3. Latency gate: -- Selection-phase delta should be `<= +5%` in micro-benchmark before opt-in expansion. -- End-to-end parse latency (integrated benchmark) should be `<= 0%` regression on hot-path corpus before considering default-on. - -4. Observability gate: -- Debug telemetry must remain stable and low-noise (only emits for reductions/fallbacks). - -## Next Work Items - -- Reduce classifier/selection overhead further (target: close gap to <= +5%). -- Verified end-to-end parse latency benchmark (constructor + parse path) confirms favorable performance. -- Expand corpus with high-frequency real-world patterns (ticker-like loops, mixed timezone/event strings). -- Re-check thresholds after optimization pass. - -## Recommendation (Current) - -- Keep `parsePrefilter` as experimental and default-off. -- Keep optimization focused on preserving neutral-or-better latency under larger and real-world corpora. -- Enable in broader CI experiments only after thresholds above are satisfied. - -## CI Integration Plan - -- Add a test matrix job with `parsePrefilter: true` (global and per-instance) for all core and regression suites. -- Monitor for any test failures, output mismatches, or unexpected regressions. -- Capture and review debug/telemetry output for noise or missed reductions. -- If all tests pass and telemetry is clean, consider opt-in enablement for select environments. - -### PR Checklist -- [x] All focused and regression tests pass with `parsePrefilter: true` -- [x] End-to-end and micro-benchmarks show neutral or better performance -- [x] Telemetry is stable and low-noise -- [ ] CI matrix job added and green diff --git a/packages/tempo/src/discrete/discrete.index.ts b/packages/tempo/src/discrete/discrete.index.ts deleted file mode 100644 index 797dbe5..0000000 --- a/packages/tempo/src/discrete/discrete.index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { parse } from './discrete.parse.js'; -export { format } from './discrete.format.js'; diff --git a/packages/tempo/src/engine/engine.term.ts b/packages/tempo/src/engine/engine.term.ts index c9b7930..2187ae6 100644 --- a/packages/tempo/src/engine/engine.term.ts +++ b/packages/tempo/src/engine/engine.term.ts @@ -3,12 +3,12 @@ import { isDefined, isString, isZonedDateTime, isNumeric } from '#library/assert import { asArray } from '#library/coercion.library.js'; import { TermError, getLargestUnit, SCHEMA, Match, isTempo } from '#tempo/support'; -import { getRange, getTermRange, resolveTermShift, findTermPlugin } from '../plugin/term.util.js'; +import { getRange, getTermRange, resolveTermShift, findTermPlugin } from '../plugin/term/term.util.js'; import { getHost } from '../plugin/plugin.util.js'; import { parseModifier } from './engine.lexer.js'; import type { Tempo } from '../tempo.class.js'; -import type { TempoType } from '../plugin/plugin.type.js'; +import type { TempoTermType } from '../plugin/term/term.type.js'; /** * Internal helper to safely get the ZonedDateTime from a Tempo instance or raw object @@ -26,7 +26,7 @@ const toZdt = (v: any): Temporal.ZonedDateTime => isTempo(v) ? v.toDateTime() : * @param zdt - The current ZonedDateTime state * @returns The mutated ZonedDateTime */ -export function resolveTermMutation(Tempo: TempoType, instance: Tempo, mutate: string, unit: string, offset: any, zdt: Temporal.ZonedDateTime): Temporal.ZonedDateTime | null { +export function resolveTermMutation(Tempo: TempoTermType, instance: Tempo, mutate: string, unit: string, offset: any, zdt: Temporal.ZonedDateTime): Temporal.ZonedDateTime | null { if (!isZonedDateTime(zdt)) return zdt; const [termPart, rangePart] = unit.startsWith('#') @@ -513,7 +513,7 @@ export function resolveTermMutation(Tempo: TempoType, instance: Tempo, mutate: s /** * Resolves a term identifier (e.g. '#quarter') to its current value (start of cycle). */ -export function resolveTermValue(Tempo: TempoType, instance: Tempo, term: string, zdt: Temporal.ZonedDateTime): Temporal.ZonedDateTime | null { +export function resolveTermValue(Tempo: TempoTermType, instance: Tempo, term: string, zdt: Temporal.ZonedDateTime): Temporal.ZonedDateTime | null { return resolveTermMutation(Tempo, instance, 'start', term, term, zdt); } diff --git a/packages/tempo/src/module/module.duration.ts b/packages/tempo/src/module/module.duration.ts index 6d73004..ec7c4fb 100644 --- a/packages/tempo/src/module/module.duration.ts +++ b/packages/tempo/src/module/module.duration.ts @@ -5,10 +5,9 @@ import { getAccessors } from '#library/reflection.library.js'; import { ifDefined } from '#library/object.library.js'; import { getRelativeTime } from '#library/international.library.js'; -import { defineInterpreterModule, interpret } from '../plugin/plugin.util.js'; +import { defineInterpreterModule, interpret, type TempoModule } from '../plugin/plugin.util.js'; import { enums, isTempo } from '#tempo/support'; -import type { Module } from '../plugin/plugin.type.js'; -import type { Tempo } from '../tempo.class.js'; +import { Tempo } from '../tempo.class.js'; declare module '../tempo.class.js' { namespace Tempo { @@ -158,7 +157,7 @@ function duration(this: Tempo, type: 'until' | 'since', arg?: any, until?: any) * string -> EDO * DurationLikeObject -> EDO (with iso string) */ -duration.toDuration = (input: string | Temporal.DurationLikeObject) => { +(duration as any).toDuration = (input: string | Temporal.DurationLikeObject) => { const dur = Temporal.Duration.from(input); return toDuration(dur); } @@ -166,7 +165,7 @@ duration.toDuration = (input: string | Temporal.DurationLikeObject) => { /** * Functional Module to attach duration methods to Tempo. */ -export const DurationModule: Module = defineInterpreterModule('DurationModule', duration, { +export const DurationModule: TempoModule = defineInterpreterModule('DurationModule', duration, { duration(this: typeof Tempo, input: any) { return interpret(this, 'DurationModule', 'toDuration', false, input); } diff --git a/packages/tempo/src/discrete/discrete.format.ts b/packages/tempo/src/module/module.format.ts similarity index 100% rename from packages/tempo/src/discrete/discrete.format.ts rename to packages/tempo/src/module/module.format.ts diff --git a/packages/tempo/src/module/module.index.ts b/packages/tempo/src/module/module.index.ts new file mode 100644 index 0000000..02f3511 --- /dev/null +++ b/packages/tempo/src/module/module.index.ts @@ -0,0 +1,2 @@ +export { parse } from './module.parse.js'; +export { format } from './module.format.js'; diff --git a/packages/tempo/src/module/module.mutate.ts b/packages/tempo/src/module/module.mutate.ts index c3ec211..5ace831 100644 --- a/packages/tempo/src/module/module.mutate.ts +++ b/packages/tempo/src/module/module.mutate.ts @@ -3,7 +3,7 @@ import { singular } from '#library/string.library.js'; import { sym, enums } from '#tempo/support'; import { defineInterpreterModule } from '../plugin/plugin.util.js'; -import { findTermPlugin } from '../plugin/term.util.js'; +import { findTermPlugin } from '../plugin/term/term.util.js'; import { resolveTermMutation } from '../engine/engine.term.js'; import type { Tempo } from '../tempo.class.js'; import type * as t from '../tempo.type.js'; @@ -19,6 +19,7 @@ declare module '#library/type.library.js' { */ function mutate(this: Tempo, type: 'add' | 'set', args?: any, options: t.Options = {}) { const state = (this as any)[sym.$Internal](); + if (isUndefined(state.mutateDepth)) state.mutateDepth = 0; if (!isZonedDateTime(state.zdt)) return this; const { zdt: selfZdt } = state; const overrides = { @@ -119,7 +120,7 @@ function mutate(this: Tempo, type: 'add' | 'set', args?: any, options: t.Options case 'add.year': case 'add.month': case 'add.week': case 'add.day': case 'add.hour': case 'add.minute': case 'add.second': case 'add.millisecond': case 'add.microsecond': case 'add.nanosecond': - return currZdt.add({ [singular(single) + 's']: offset }); + return currZdt.add({ [`${single}s`]: offset }); case 'set.period': case 'set.time': case 'set.date': case 'set.event': case 'set.dow': case 'set.wkd': { diff --git a/packages/tempo/src/discrete/discrete.parse.ts b/packages/tempo/src/module/module.parse.ts similarity index 99% rename from packages/tempo/src/discrete/discrete.parse.ts rename to packages/tempo/src/module/module.parse.ts index d634ed7..8a40ddb 100644 --- a/packages/tempo/src/discrete/discrete.parse.ts +++ b/packages/tempo/src/module/module.parse.ts @@ -12,9 +12,9 @@ import { selectLayoutPatterns } from '../engine/engine.planner.js'; import { compose } from '../engine/engine.composer.js'; import { normalizeMatch, accumulateResult } from '../engine/engine.normalizer.js'; -import { getRange, getTermRange } from '../plugin/term.util.js'; +import { getRange, getTermRange } from '../plugin/term/term.util.js'; import { defineInterpreterModule } from '../plugin/plugin.util.js'; -import type { Range, ResolvedRange } from '../plugin/plugin.type.js'; +import type { Range, ResolvedRange } from '../plugin/term/term.type.js'; import { sym, isTempo, TermError, getRuntime, Match } from '../support/support.index.js'; import { markConfig, setPatterns, init, extendState } from '../support/support.index.js'; import { setProperty } from '#tempo/support/support.util.js'; diff --git a/packages/tempo/src/plugin/extend/extend.ticker.ts b/packages/tempo/src/plugin/extend/extend.ticker.ts index be49067..30470ad 100644 --- a/packages/tempo/src/plugin/extend/extend.ticker.ts +++ b/packages/tempo/src/plugin/extend/extend.ticker.ts @@ -1,12 +1,11 @@ -import { isObject, isFunction, isDefined, isUndefined, isEmpty, isNumber, isNumeric, isFiniteNumber } from '#library/assertion.library.js'; +import { isObject, isFunction, isDefined, isUndefined, isEmpty, isNumeric, isFiniteNumber } from '#library/assertion.library.js'; import { Pledge } from '#library/pledge.class.js'; import { asArray } from '#library/coercion.library.js'; import { instant, normaliseFractionalDurations } from '#library/temporal.library.js'; import { sym, markConfig, enums } from '#tempo/support'; -import { defineExtension, attachStatics } from '../plugin.util.js' -import type { Tempo } from '../../tempo.class.js' -import type { Extension, TempoType } from '../plugin.type.js' +import { defineExtension, attachStatics, type TempoExtension, type TempoType } from '../plugin.util.js'; +import { Tempo } from '../../tempo.class.js'; declare module '../../tempo.class.js' { namespace Tempo { @@ -351,9 +350,9 @@ class TickerInstance implements Ticker.Descriptor { /** * # TickerModule */ -export const TickerModule: Extension = defineExtension({ +export const TickerModule: TempoExtension = defineExtension({ name: 'TickerModule', - install(this: Tempo, TempoClass: TempoType) { + install(this: TempoType, TempoClass: TempoType) { attachStatics(TempoClass, { ticker: function (this: TempoType, arg1: any, arg2?: any): Ticker.Instance { const instance = new TickerInstance(this as unknown as TempoType, arg1, arg2); diff --git a/packages/tempo/src/plugin/plugin.index.ts b/packages/tempo/src/plugin/plugin.index.ts index 59b5198..f383d37 100644 --- a/packages/tempo/src/plugin/plugin.index.ts +++ b/packages/tempo/src/plugin/plugin.index.ts @@ -8,3 +8,4 @@ export * from './plugin.util.js'; export * from './plugin.type.js'; +export * from './term/term.type.js'; diff --git a/packages/tempo/src/plugin/plugin.type.ts b/packages/tempo/src/plugin/plugin.type.ts index 094ac76..aa1c3cc 100644 --- a/packages/tempo/src/plugin/plugin.type.ts +++ b/packages/tempo/src/plugin/plugin.type.ts @@ -1,42 +1,17 @@ -import type { Prettify, Property } from '#library/type.library.js'; -import type { Tempo } from '../tempo.class.js'; -import { TermError } from '#tempo/support'; - -export type TempoType = typeof Tempo & { - [TermError]?: (config: any, term: string) => void; -} - -/** - * ## TermPlugin - * Interface for term-driven parsing and resolution. - */ -export interface TermPlugin { - key: string; - scope?: string; - description?: string; - groups?: any; - ranges?: any[]; - resolve?: (this: Tempo, anchor?: any) => Range[]; - define: (this: Tempo, keyOnly?: boolean, anchor?: any) => string | Range | Range[] | undefined; -} - -/** mapping of terms to their resolved values */ -export type Terms = Property; - /** * ## Plugin * Interface for general Tempo plugins (Modules/Extensions). */ -export interface Plugin { +export interface Plugin { name: string; - install: (this: Tempo, t: TempoType) => void; + install: (this: T, t: T) => void; } /** * ## Module * Type for Module plugins. */ -export interface Module extends Plugin { +export interface Module extends Plugin { [key: string]: any; } @@ -44,55 +19,6 @@ export interface Module extends Plugin { * ## Extension * Type for Extension plugins. */ -export interface Extension extends Plugin { +export interface Extension extends Plugin { [key: string]: any; -} - - -/** - * ## Range - * Discrete time interval within a specific term. - * - * When Range.year is a number it is interpreted as a relative offset if |year| ≤ 10 - * and as an absolute year otherwise. - */ -export type Range = Prettify<{ - key: string; - group?: string; // categorization marker (e.g. 'western', 'chinese', 'fiscal') - [meta: string]: any; -} & ( - { year: number } | { month: number } | { week: number } | { day: number } | - { hour: number } | { minute: number } | { second: number } | - { millisecond: number } | { microsecond: number } | { nanosecond: number } - ) & { - year?: number; - month?: number; - week?: number; - day?: number; - hour?: number; - minute?: number; - second?: number; - millisecond?: number; - microsecond?: number; - nanosecond?: number; - }>; - - -/** - * ## ResolvedRange - * Range with additional metadata. - */ -// export interface ResolvedRange extends Range { -// label: string; -// active: boolean; -// index: number; -// } -export type ResolvedRange = Range & { - start: Tempo; - end: Tempo; - scope?: string; - label?: string; - unit?: string; - rollover?: string; - [str: string]: any; } \ No newline at end of file diff --git a/packages/tempo/src/plugin/plugin.util.ts b/packages/tempo/src/plugin/plugin.util.ts index d974a06..91d1fc8 100644 --- a/packages/tempo/src/plugin/plugin.util.ts +++ b/packages/tempo/src/plugin/plugin.util.ts @@ -4,7 +4,12 @@ import { secureRef } from '#library/proxy.library.js'; import { sym, getRuntime, isTempo } from '#tempo/support'; import { hasOwn } from '#tempo/support/support.util.js'; import type { Tempo } from '../tempo.class.js'; -import type { Plugin } from './plugin.type.js'; +import type { Plugin, Module, Extension } from './plugin.type.js'; + +export type TempoType = typeof Tempo; +export type TempoPlugin = Plugin; +export type TempoModule = Module; +export type TempoExtension = Extension; export function getHost(t: any): any { const TempoClass = getRuntime().modules['Tempo']; @@ -83,7 +88,7 @@ export function interpret(t: any, module: string, methodOrFallback?: any, silent * ## defineModule * Used to register an internal modularization component. */ -export const defineModule = (module: T): T => { +export const defineModule = >(module: T): T => { registerPlugin(module); return module; } @@ -130,7 +135,7 @@ export function attachStatics(TempoClass: any, props: Record) { export const defineInterpreterModule = (name: string, logic: any, statics?: Record) => defineModule({ name, - install(this: Tempo, TempoClass: typeof Tempo) { + install(this: TempoType, TempoClass: TempoType) { const rt = getRuntime(); const modules = rt.modules; @@ -166,7 +171,7 @@ export const defineInterpreterModule = (name: string, logic: any, statics?: Reco * ## defineExtension * Used to register a class-augmenting extension. */ -export const defineExtension = (extension: T): T => { +export const defineExtension = >(extension: T): T => { registerPlugin(extension); return extension; } diff --git a/packages/tempo/src/plugin/term/term.index.ts b/packages/tempo/src/plugin/term/term.index.ts index 685f296..b3de6d3 100644 --- a/packages/tempo/src/plugin/term/term.index.ts +++ b/packages/tempo/src/plugin/term/term.index.ts @@ -8,12 +8,12 @@ import { TimelineTerm } from './term.timeline.js' /** collection of built-in terms for initial registration */ export const StandardTerms = [QuarterTerm, SeasonTerm, ZodiacTerm, TimelineTerm]; -export { defineTerm, defineRange, getTermRange } from '../term.util.js'; +export { defineTerm, defineRange, getTermRange } from './term.util.js'; /** Aggregator module for all standard Terms */ export const TermsModule = defineModule({ name: 'TermsModule', - install(this: Tempo, TempoClass: typeof Tempo) { + install(this: typeof Tempo, TempoClass: typeof Tempo) { getRuntime().modules['TermsModule'] = true; // mark as canonical module onRegistryReset(() => { TempoClass.extend(StandardTerms); }); TempoClass.extend(StandardTerms); diff --git a/packages/tempo/src/plugin/term/term.quarter.ts b/packages/tempo/src/plugin/term/term.quarter.ts index b1fec7c..e4ffee5 100644 --- a/packages/tempo/src/plugin/term/term.quarter.ts +++ b/packages/tempo/src/plugin/term/term.quarter.ts @@ -1,4 +1,4 @@ -import { defineTerm, getTermRange, defineRange, resolveCycleWindow } from '../term.util.js'; +import { defineTerm, getTermRange, defineRange, resolveCycleWindow } from './term.util.js'; import { COMPASS } from '../../support/support.enum.js'; import { isNumber } from '#library/assertion.library.js'; import { asArray } from '#library'; diff --git a/packages/tempo/src/plugin/term/term.season.ts b/packages/tempo/src/plugin/term/term.season.ts index 02321db..e7b9e92 100644 --- a/packages/tempo/src/plugin/term/term.season.ts +++ b/packages/tempo/src/plugin/term/term.season.ts @@ -1,4 +1,4 @@ -import { getTermRange, defineTerm, defineRange, resolveCycleWindow } from '../term.util.js'; +import { getTermRange, defineTerm, defineRange, resolveCycleWindow } from './term.util.js'; import { COMPASS } from '../../support/support.enum.js'; import type { Tempo } from '../../tempo.class.js'; diff --git a/packages/tempo/src/plugin/term/term.timeline.ts b/packages/tempo/src/plugin/term/term.timeline.ts index b23f49c..c7e3026 100644 --- a/packages/tempo/src/plugin/term/term.timeline.ts +++ b/packages/tempo/src/plugin/term/term.timeline.ts @@ -1,4 +1,4 @@ -import { defineTerm, getTermRange, defineRange, resolveCycleWindow } from '../term.util.js'; +import { defineTerm, getTermRange, defineRange, resolveCycleWindow } from './term.util.js'; import type { Tempo } from '../../tempo.class.js'; /** definition of daily time periods */ diff --git a/packages/tempo/src/plugin/term/term.type.ts b/packages/tempo/src/plugin/term/term.type.ts new file mode 100644 index 0000000..648acac --- /dev/null +++ b/packages/tempo/src/plugin/term/term.type.ts @@ -0,0 +1,71 @@ +import type { Prettify, Property } from '#library/type.library.js'; +import type { Tempo } from '../../tempo.class.js'; +import { TermError } from '#tempo/support'; + +/** + * ## TempoTermType + * Specialized Tempo class type including term resolution error handling. + */ +export type TempoTermType = typeof Tempo & { + [TermError]?: (config: any, term: string) => void; +} + +/** + * ## TermPlugin + * Interface for term-driven parsing and resolution. + */ +export interface TermPlugin { + key: string; + scope?: string; + description?: string; + groups?: any; + ranges?: any[]; + resolve?: (this: Tempo, anchor?: any) => Range[]; + define: (this: Tempo, keyOnly?: boolean, anchor?: any) => string | Range | Range[] | undefined; +} + +/** mapping of terms to their resolved values */ +export type Terms = Property; + +/** + * ## Range + * Discrete time interval within a specific term. + * + * When Range.year is a number it is interpreted as a relative offset if |year| ≤ 10 + * and as an absolute year otherwise. + */ +export type Range = Prettify<{ + key: string; + group?: string; // categorization marker (e.g. 'western', 'chinese', 'fiscal') + [meta: string]: any; +} & ( + { year: number } | { month: number } | { week: number } | { day: number } | + { hour: number } | { minute: number } | { second: number } | + { millisecond: number } | { microsecond: number } | { nanosecond: number } + ) & { + year?: number; + month?: number; + week?: number; + day?: number; + hour?: number; + minute?: number; + second?: number; + millisecond?: number; + microsecond?: number; + nanosecond?: number; + }>; + + +/** + * ## ResolvedRange + * Range with additional metadata. + */ +export type ResolvedRange = Range & { + start: Tempo; + end: Tempo; + scope?: string; + label?: string; + unit?: string; + rollover?: string; + [str: string]: any; +} diff --git a/packages/tempo/src/plugin/term.util.ts b/packages/tempo/src/plugin/term/term.util.ts similarity index 98% rename from packages/tempo/src/plugin/term.util.ts rename to packages/tempo/src/plugin/term/term.util.ts index c44f88b..77c1f14 100644 --- a/packages/tempo/src/plugin/term.util.ts +++ b/packages/tempo/src/plugin/term/term.util.ts @@ -3,9 +3,9 @@ import { isDefined, isFunction, isString, isUndefined, isNumber, isZonedDateTime import { secure } from '#library/proxy.library.js'; import { sortKey, byKey } from '#library/array.library.js'; import { sym, TermError, SCHEMA, getLargestUnit, isTempo, getRuntime } from '#tempo/support'; -import type { Tempo } from '../tempo.class.js'; -import type { TermPlugin, Range, ResolvedRange } from './plugin.type.js'; -import { getHost } from './plugin.util.js'; +import type { Tempo } from '../../tempo.class.js'; +import type { TermPlugin, Range, ResolvedRange } from './term.type.js'; +import { getHost } from '../plugin.util.js'; /** * ## defineTerm @@ -373,7 +373,7 @@ export function resolveCycleWindow(source: Tempo | any, template: Range[] | Reco // Normalize year semantics: Treat small offsets as relative to the cycle, // while treating larger numbers as absolute years (e.g. for fixed historical dates). if (isNumber(itm.year)) { - clone.year = (itm.year >= -10 && itm.year <= 10) ? itm.year + targetYY : itm.year; // See Range JSDoc in plugin.type.ts (|year| ≤ 10 is relative) + clone.year = (itm.year >= -10 && itm.year <= 10) ? itm.year + targetYY : itm.year; // See Range JSDoc in term.type.ts (|year| ≤ 10 is relative) } else { clone.year = targetYY; } diff --git a/packages/tempo/src/plugin/term/term.zodiac.ts b/packages/tempo/src/plugin/term/term.zodiac.ts index 1ce0ad2..dd1e85d 100644 --- a/packages/tempo/src/plugin/term/term.zodiac.ts +++ b/packages/tempo/src/plugin/term/term.zodiac.ts @@ -1,4 +1,4 @@ -import { defineTerm, getTermRange, defineRange, resolveCycleWindow } from '../term.util.js'; +import { defineTerm, getTermRange, defineRange, resolveCycleWindow } from './term.util.js'; import { isNumber } from '#library/assertion.library.js'; import type { Tempo } from '../../tempo.class.js'; diff --git a/packages/tempo/src/support/support.runtime.ts b/packages/tempo/src/support/support.runtime.ts index df2fbcc..48fdd6e 100644 --- a/packages/tempo/src/support/support.runtime.ts +++ b/packages/tempo/src/support/support.runtime.ts @@ -1,5 +1,6 @@ import { sym } from './support.symbol.js'; -import type { TermPlugin, Extension, Plugin } from '../plugin/plugin.type.js'; +import type { TermPlugin } from '../plugin/term/term.type.js'; +import type { Extension, Plugin } from '../plugin/plugin.type.js'; import type { Internal } from '../tempo.type.js'; /** diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index 0ed3704..1916738 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -17,9 +17,9 @@ import { instant, getTemporalIds } from '#library/temporal.library.js'; import { getDateTimeFormat, getHemisphere, canonicalLocale } from '#library/international.library.js'; import type { Property, Secure } from '#library/type.library.js'; -import { registerPlugin, interpret, ensureModule } from './plugin/plugin.util.js' -import { registerTerm, getTermRange } from './plugin/term.util.js'; -import type { TermPlugin, Plugin } from './plugin/plugin.type.js'; +import { registerPlugin, interpret, ensureModule, type TempoPlugin } from './plugin/plugin.util.js' +import { registerTerm, getTermRange } from './plugin/term/term.util.js'; +import type { TermPlugin } from './plugin/term/term.type.js'; import { AliasEngine } from './engine/engine.alias.js'; import { PatternCompiler } from './engine/engine.pattern.js'; @@ -37,8 +37,11 @@ declare module '#library/type.library.js' { } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** current execution context */ const Context = getContext(); -/** return whether the shape is 'local' or 'global' */ const isLocal = (shape: { config: { scope: string } }) => shape.config.scope === 'local'; /** */ const ClassStates = new WeakMap(); +// shortcut functions to common Tempo properties / methods +/** current timestamp (ts) */ export const getStamp = ((tempo: t.DateTime, options: t.Options) => new Tempo(tempo, options).ts) as t.Params; +/** create new Tempo */ export const getTempo = ((tempo: t.DateTime, options: t.Options) => new Tempo(tempo, options)) as t.Params; +/** format a Tempo */ export const fmtTempo = ((fmt: string, tempo: t.DateTime, options: t.Options) => new Tempo(tempo, options).format(fmt as any)) as Internal.Fmt; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ namespace Internal { // ...existing code... @@ -48,7 +51,7 @@ namespace Internal { export type Config = t.Internal.Config; export type Discovery = t.Internal.Discovery; export type Registry = t.Internal.Registry; - export type PluginContainer = t.Internal.PluginContainer; + export interface PluginContainer extends TempoPlugin { } export type Fmt = { // used for the fmtTempo() shortcut (fmt: F, tempo?: t.DateTime, options?: t.Options): t.FormatType; @@ -451,14 +454,14 @@ export class Tempo { * @param plugin - A plugin or term extension to register. * @param options - Optional configuration for the plugin. */ - static extend(plugin: Plugin, options?: t.Options): typeof Tempo; + static extend(plugin: TempoPlugin, options?: t.Options): typeof Tempo; /** * Register an array of plugins or term extensions. * * @param plugins - An array of plugins, terms, or extensions to register. * @param options - Optional configuration for the plugins. */ - static extend(plugins: (Plugin | TermPlugin | any)[], options?: t.Options): typeof Tempo; + static extend(plugins: (TempoPlugin | TermPlugin | any)[], options?: t.Options): typeof Tempo; /** * Register multiple plugins or term extensions. * @@ -503,7 +506,7 @@ export class Tempo { rt.installed.add(name); registerPlugin(item); - (item as Plugin).install.call(this as any, this); + (item as TempoPlugin).install.call(this as any, this); } else if (isObject(item)) { // 1. handle TermPlugin @@ -1521,10 +1524,6 @@ export class Tempo { } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// shortcut functions to common Tempo properties / methods -/** current timestamp (ts) */ export const getStamp = ((tempo: t.DateTime, options: t.Options) => new Tempo(tempo, options).ts) as t.Params; -/** create new Tempo */ export const getTempo = ((tempo: t.DateTime, options: t.Options) => new Tempo(tempo, options)) as t.Params; -/** format a Tempo */ export const fmtTempo = ((fmt: string, tempo: t.DateTime, options: t.Options) => new Tempo(tempo, options).format(fmt as any)) as Internal.Fmt; export namespace Tempo { export type DateTime = t.DateTime; diff --git a/packages/tempo/src/tempo.index.ts b/packages/tempo/src/tempo.index.ts index f400aba..a2fb30e 100644 --- a/packages/tempo/src/tempo.index.ts +++ b/packages/tempo/src/tempo.index.ts @@ -20,8 +20,7 @@ onRegistryReset(() => { Tempo.extend(core); -export { parse } from './discrete/discrete.parse.js'; -export { format } from './discrete/discrete.format.js'; +export { parse, format } from '#tempo/module'; export { enums }; export * from './tempo.class.js'; diff --git a/packages/tempo/src/tempo.type.ts b/packages/tempo/src/tempo.type.ts index c8a7ef8..795e253 100644 --- a/packages/tempo/src/tempo.type.ts +++ b/packages/tempo/src/tempo.type.ts @@ -12,10 +12,11 @@ import * as enums from '#tempo/support/support.enum.js'; import type { Logify } from '#library/logify.class.js'; import type { Snippet, Layout, Event, Period, Ignore } from '#tempo/support/support.default.js'; import type { IntRange, NonOptional, Property, Plural, Prettify, TemporalObject, TypeValue, RegistryOption } from '#library/type.library.js'; -import type { TermPlugin, Plugin } from '#tempo/plugin/plugin.type.js'; +import type { TermPlugin } from '#tempo/plugin/term/term.type.js'; +import type { TempoPlugin } from '#tempo/plugin/plugin.util.js'; import type { Token } from '#tempo/support/support.symbol.js'; import type { Tempo } from '#tempo/tempo.class.js'; -import { AliasEngine } from './engine/engine.alias.js'; +import type { AliasEngine } from './engine/engine.alias.js'; import type { PatternCompiler } from './engine/engine.pattern.js'; declare global { @@ -199,7 +200,7 @@ export namespace Internal { /** custom time aliases (periods). */ period: Period | RegistryOption; /** noise words to ignore during parsing. */ ignore: Ignore; /** custom format strings to merge in the FORMAT enum */formats: Property; - /** plugins to be automatically extended */ plugins: Plugin | Plugin[]; + /** plugins to be automatically extended */ plugins: TempoPlugin | TempoPlugin[]; /** supplied value to parse */ value: DateTime; /** @internal temporary anchor used during parsing */ anchor: any; /** @internal accumulated parse results */ result?: Match[] | undefined; @@ -284,7 +285,7 @@ export namespace Internal { /** term plugins to be registered via Tempo.addTerm() */terms?: TermPlugin | TermPlugin[]; /** custom format strings to merge in the FORMAT dictionary */formats?: Property; /** noise words to ignore during parsing via Tempo.ignore() */ ignore?: Ignore - /** plugins to be automatically extended via Tempo.extend() */plugins?: Plugin | Plugin[]; + /** plugins to be automatically extended via Tempo.extend() */plugins?: TempoPlugin | TempoPlugin[]; } } diff --git a/packages/tempo/src/tsconfig.json b/packages/tempo/src/tsconfig.json index cdef1a9..8442bcd 100644 --- a/packages/tempo/src/tsconfig.json +++ b/packages/tempo/src/tsconfig.json @@ -14,9 +14,9 @@ "#server/*": ["../../library/src/server/*"], "#tempo": ["./tempo.index.ts"], "#tempo/core": ["./core.index.ts"], - "#tempo/parse": ["./discrete/discrete.parse.ts"], - "#tempo/format": ["./discrete/discrete.format.ts"], - "#tempo/discrete": ["./discrete/discrete.index.ts"], + "#tempo/parse": ["./module/module.parse.ts"], + "#tempo/format": ["./module/module.format.ts"], + "#tempo/discrete": ["./module/module.index.ts"], "#tempo/duration": ["./module/module.duration.ts"], "#tempo/mutate": ["./module/module.mutate.ts"], "#tempo/ticker": ["./plugin/extend/extend.ticker.ts"], diff --git a/packages/tempo/test/README.md b/packages/tempo/test/README.md index df3b1b4..af8dd3e 100644 --- a/packages/tempo/test/README.md +++ b/packages/tempo/test/README.md @@ -10,7 +10,7 @@ This directory contains the automated test suite for the `@magmacomputing/tempo` | **`engine/`** | The internal parsing engine: lexer, planner, layout resolution, and pattern matching. | | **`instance/`** | Public methods on the `Tempo` instance (e.g., `add()`, `since()`, `format()`, `set()`). | | **`plugins/`** | Extension modules and registries: Terms, Tickers, and Duration modules. | -| **`discrete/`** | Standalone helper functions that operate independently of a `Tempo` instance. | +| **`module/`** | Modules (Parse, Format, Duration, Mutate) that can operate on a `Tempo` instance. | | **`issues/`** | Regression tests linked to specific bug reports or edge cases. | | **`support/`** | Infrastructure, Vitest setup files, and general test utilities. | diff --git a/packages/tempo/test/engine/parse.prefilter.numeric-safety.test.ts b/packages/tempo/test/engine/parse.prefilter.numeric-safety.test.ts index dc0a5de..81a8181 100644 --- a/packages/tempo/test/engine/parse.prefilter.numeric-safety.test.ts +++ b/packages/tempo/test/engine/parse.prefilter.numeric-safety.test.ts @@ -20,7 +20,7 @@ describe('parse prefilter numeric safety constraints', () => { const first = t.parse.result?.[0] as any; // Using a delimiter ('-') ensures selectLayoutPatterns() is exercised instead of - // the pure numeric short-circuit (BigInt) in discrete.parse.ts. + // the pure numeric short-circuit (BigInt) in module.parse.ts. expect(first?.match).toBe('yearMonthDay'); expect(t.yy).toBe(1959); expect(t.mm).toBe(5); diff --git a/packages/tempo/test/tsconfig.json b/packages/tempo/test/tsconfig.json index 3125f1a..44340a7 100644 --- a/packages/tempo/test/tsconfig.json +++ b/packages/tempo/test/tsconfig.json @@ -35,13 +35,13 @@ "../src/module/module.mutate.ts" ], "#tempo/format": [ - "../src/discrete/discrete.format.ts" + "../src/module/module.format.ts" ], "#tempo/parse": [ - "../src/discrete/discrete.parse.ts" + "../src/module/module.parse.ts" ], - "#tempo/discrete": [ - "../src/discrete/discrete.index.ts" + "#tempo/module": [ + "../src/module/module.index.ts" ], "#tempo/ticker": [ "../src/plugin/extend/extend.ticker.ts" diff --git a/packages/tempo/vitest.config.ts b/packages/tempo/vitest.config.ts index ee931ee..ed19a4e 100644 --- a/packages/tempo/vitest.config.ts +++ b/packages/tempo/vitest.config.ts @@ -36,8 +36,8 @@ export default defineConfig({ { find: /^#tempo\/core$/, replacement: resolve(__dirname, './dist/core.index.js') }, { find: /^#tempo\/term$/, replacement: resolve(__dirname, './dist/plugin/term/term.index.js') }, { find: /^#tempo\/duration$/, replacement: resolve(__dirname, './dist/module/module.duration.js') }, - { find: /^#tempo\/(parse|format)$/, replacement: resolve(__dirname, './dist/discrete/discrete.$1.js') }, - { find: /^#tempo\/discrete$/, replacement: resolve(__dirname, './dist/discrete/discrete.index.js') }, + { find: /^#tempo\/(parse|format)$/, replacement: resolve(__dirname, './dist/module/module.$1.js') }, + { find: /^#tempo\/module$/, replacement: resolve(__dirname, './dist/module/module.index.js') }, { find: /^#tempo\/mutate$/, replacement: resolve(__dirname, './dist/module/module.mutate.js') }, { find: /^#tempo\/ticker$/, replacement: resolve(__dirname, './dist/plugin/extend/extend.ticker.js') }, { find: /^#tempo\/scripts\/(.*)\.js$/, replacement: resolve(__dirname, './scripts/$1.js') }, @@ -57,8 +57,8 @@ export default defineConfig({ { find: /^#tempo\/term\/(.*)$/, replacement: resolve(__dirname, './src/plugin/term/$1') }, { find: /^#tempo\/ticker$/, replacement: resolve(__dirname, './src/plugin/extend/extend.ticker.ts') }, { find: /^#tempo\/duration$/, replacement: resolve(__dirname, './src/module/module.duration.ts') }, - { find: /^#tempo\/(parse|format)$/, replacement: resolve(__dirname, './src/discrete/discrete.$1.ts') }, - { find: /^#tempo\/discrete$/, replacement: resolve(__dirname, './src/discrete/discrete.index.ts') }, + { find: /^#tempo\/(parse|format)$/, replacement: resolve(__dirname, './src/module/module.$1.ts') }, + { find: /^#tempo\/module$/, replacement: resolve(__dirname, './src/module/module.index.ts') }, { find: /^#tempo\/mutate$/, replacement: resolve(__dirname, './src/module/module.mutate.ts') }, { find: /^#tempo\/scripts\/(.*)\.js$/, replacement: resolve(__dirname, './scripts/$1.ts') }, { find: /^#tempo\/plugin\/plugin\.(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/plugin.$1.ts') }, From cc57f60961b49194e2efe1d512ef42be4139e9b5 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Fri, 8 May 2026 12:03:36 +1000 Subject: [PATCH 15/16] Guard PR review #5 --- .../library/src/common/temporal.polyfill.ts | 6 ++- .../test/common/temporal_library.test.ts | 6 +-- .../tempo/src/engine/engine.normalizer.ts | 50 ++++++++++--------- packages/tempo/src/module/module.mutate.ts | 5 +- packages/tempo/src/module/module.parse.ts | 2 +- packages/tempo/src/tempo.type.ts | 2 +- 6 files changed, 36 insertions(+), 35 deletions(-) diff --git a/packages/library/src/common/temporal.polyfill.ts b/packages/library/src/common/temporal.polyfill.ts index f4e4bbf..1519a06 100644 --- a/packages/library/src/common/temporal.polyfill.ts +++ b/packages/library/src/common/temporal.polyfill.ts @@ -25,8 +25,10 @@ try { const zdt = Temporal.Now.zonedDateTimeISO(); if (typeof zdt.add !== 'function') throw new Error('Incomplete Temporal implementation'); } -} catch (err) { - console.warn('Tempo: Native Temporal implementation appears incomplete. Consider loading a polyfill.'); +} catch (err: any) { + console.warn('Tempo: Native Temporal implementation appears incomplete. Consider loading a polyfill.', err); + if (err?.message !== 'Incomplete Temporal implementation') + throw err; } export { } diff --git a/packages/library/test/common/temporal_library.test.ts b/packages/library/test/common/temporal_library.test.ts index 7b92c78..5d1dfb6 100644 --- a/packages/library/test/common/temporal_library.test.ts +++ b/packages/library/test/common/temporal_library.test.ts @@ -23,11 +23,7 @@ describe('Temporal Library Helpers', () => { it('should handle "Z" as a zone designator and pass it through (even if ZonedDateTime.from throws without a bracket)', () => { const bag = '2024-01-01T12:00:00Z'; - try { - toZonedDateTime(bag, 'Australia/Sydney'); - } catch (e) { - expect((e as Error).message).toContain('requires a time zone ID in brackets'); - } + expect(() => toZonedDateTime(bag, 'Australia/Sydney')).toThrow(/requires a time zone ID in brackets/); }); }); }); diff --git a/packages/tempo/src/engine/engine.normalizer.ts b/packages/tempo/src/engine/engine.normalizer.ts index 065701d..00cb37b 100644 --- a/packages/tempo/src/engine/engine.normalizer.ts +++ b/packages/tempo/src/engine/engine.normalizer.ts @@ -141,38 +141,42 @@ export function resolveAliases( resolvingKeys.add(aliasKey); - const host = getResolutionContext(ctx, dateTime); - const res = aliasEngine?.resolveAlias(key as any, host); - if (!res) continue; - try { - const mapped = ({ - evt: { type: 'Event', pat: 'dt' }, - per: { type: 'Period', pat: 'tm' } - } as const)[res.type as 'evt' | 'per']; + const host = getResolutionContext(ctx, dateTime); + const res = aliasEngine?.resolveAlias(key as any, host); + if (!res) continue; + + try { + const mapped = ({ + evt: { type: 'Event', pat: 'dt' }, + per: { type: 'Period', pat: 'tm' } + } as const)[res.type as 'evt' | 'per']; - if (!mapped) - throw new Error(`[ParseEngine] Unexpected AliasType: ${res.type}`); + if (!mapped) + throw new Error(`[ParseEngine] Unexpected AliasType: ${res.type}`); - const { type, pat } = mapped; + const { type, pat } = mapped; - accumulateResult(state, { type, value: res.key as any, match: pat, source: res.source, groups: { [key]: res.value } }); + accumulateResult(state, { type, value: res.key as any, match: pat, source: res.source, groups: { [key]: res.value } }); - if (!isEmpty(res.value) && res.value !== String(groups[key])) { - const resolving = new Set(resolvingKeys); - resolving.add(res.key); + if (!isEmpty(res.value) && res.value !== String(groups[key])) { + const resolving = new Set(resolvingKeys); + resolving.add(res.key); - const subAnchor: any = state.anchor; - state.anchor = dateTime; - const resMatch = subParse(res.value, dateTime, resolving); - state.anchor = subAnchor; + const subAnchor: any = state.anchor; + state.anchor = dateTime; + const resMatch = subParse(res.value, dateTime, resolving); + state.anchor = subAnchor; - if (resMatch.type === 'Temporal.ZonedDateTime') - dateTime = resMatch.value; + if (resMatch.type === 'Temporal.ZonedDateTime') + dateTime = resMatch.value; + } + } finally { + state.zdt = dateTime; + delete groups[key]; } } finally { - state.zdt = dateTime; - delete groups[key]; + resolvingKeys.delete(aliasKey); } } } finally { diff --git a/packages/tempo/src/module/module.mutate.ts b/packages/tempo/src/module/module.mutate.ts index 5ace831..414517e 100644 --- a/packages/tempo/src/module/module.mutate.ts +++ b/packages/tempo/src/module/module.mutate.ts @@ -2,7 +2,7 @@ import { isDefined, isObject, isString, isUndefined, isZonedDateTime } from '#li import { singular } from '#library/string.library.js'; import { sym, enums } from '#tempo/support'; -import { defineInterpreterModule } from '../plugin/plugin.util.js'; +import { defineInterpreterModule, type TempoModule } from '../plugin/plugin.util.js'; import { findTermPlugin } from '../plugin/term/term.util.js'; import { resolveTermMutation } from '../engine/engine.term.js'; import type { Tempo } from '../tempo.class.js'; @@ -37,7 +37,6 @@ function mutate(this: Tempo, type: 'add' | 'set', args?: any, options: t.Options // Shift the current instance to the target timezone first let zdt = selfZdt.withTimeZone(overrides.timeZone).withCalendar(overrides.calendar); state.parseDepth++; - const isRoot = state.parseDepth === 1; const matches = Array.isArray(this.parse?.result) ? Array.from(this.parse.result) : []; try { @@ -211,4 +210,4 @@ const MutateEngine = { /** * MutateModule registration */ -export const MutateModule = defineInterpreterModule('MutateModule', MutateEngine); +export const MutateModule: TempoModule = defineInterpreterModule('MutateModule', MutateEngine); diff --git a/packages/tempo/src/module/module.parse.ts b/packages/tempo/src/module/module.parse.ts index 8a40ddb..996ad17 100644 --- a/packages/tempo/src/module/module.parse.ts +++ b/packages/tempo/src/module/module.parse.ts @@ -170,9 +170,9 @@ const _ParseEngine = { return undefined as any; } - if (!isEmpty(options)) zdt = zdt.with(options as Temporal.ZonedDateTimeLikeObject); if (timeZone) zdt = zdt.withTimeZone(timeZone); if (calendar) zdt = zdt.withCalendar(calendar); + if (!isEmpty(options)) zdt = zdt.with(options as Temporal.ZonedDateTimeLikeObject); accumulateResult(state, { type: 'Temporal.ZonedDateTimeLike', value: zdt, match: 'Temporal.ZonedDateTimeLike' }); diff --git a/packages/tempo/src/tempo.type.ts b/packages/tempo/src/tempo.type.ts index 795e253..6f777d0 100644 --- a/packages/tempo/src/tempo.type.ts +++ b/packages/tempo/src/tempo.type.ts @@ -210,7 +210,7 @@ export namespace Internal { export type TimeStamp = 'ss' | 'ms' | 'us' | 'ns' /** internal metadata for a plugin to track installation */ - export interface PluginContainer extends Plugin { + export interface PluginContainer extends TempoPlugin { installed?: boolean; } From 4f4e4af29d3936e3da9202e740df56c1bc09ebc1 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Fri, 8 May 2026 12:17:48 +1000 Subject: [PATCH 16/16] Guard PR review #6 --- package-lock.json | 497 +++++++++++++++++++++++++++- package.json | 1 + packages/tempo/.vitepress/config.ts | 8 +- packages/tempo/doc/releases/v2.x.md | 16 + packages/tempo/package.json | 2 +- 5 files changed, 518 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6c9e609..a1583d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@types/node": "^25.5.2", "@vitest/ui": "^2.1.8", "cross-env": "^10.1.0", + "markdown-it-mathjax3": "^4.3.2", "rollup": "^4.60.1", "tslib": "^2.8.1", "tsx": "^4.21.0", @@ -2163,6 +2164,16 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, "node_modules/algoliasearch": { "version": "5.50.2", "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.50.2.tgz", @@ -2189,6 +2200,16 @@ "node": ">= 14.0.0" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2216,6 +2237,13 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -2286,6 +2314,45 @@ "node": ">= 16" } }, + "node_modules/cheerio": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", + "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^1.5.0", + "dom-serializer": "^1.3.2", + "domhandler": "^4.2.0", + "htmlparser2": "^6.1.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.6.0.tgz", + "integrity": "sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "css-select": "^4.3.0", + "css-what": "^6.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.3.1", + "domutils": "^2.8.0" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -2297,6 +2364,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/copy-anything": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", @@ -2346,6 +2423,36 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2415,6 +2522,75 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/emoji-regex-xs": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", @@ -2501,6 +2677,29 @@ "node": ">=18" } }, + "node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -2669,6 +2868,36 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -2719,6 +2948,26 @@ "devOptional": true, "license": "Apache-2.0" }, + "node_modules/juice": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/juice/-/juice-8.1.0.tgz", + "integrity": "sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio": "1.0.0-rc.10", + "commander": "^6.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^6.0.1" + }, + "bin": { + "juice": "bin/juice" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", @@ -2778,6 +3027,30 @@ "markdown-it": "bin/markdown-it.mjs" } }, + "node_modules/markdown-it-mathjax3": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/markdown-it-mathjax3/-/markdown-it-mathjax3-4.3.2.tgz", + "integrity": "sha512-TX3GW5NjmupgFtMJGRauioMbbkGsOXAAt1DZ/rzzYmTHqzkO1rNAdiMD4NiruurToPApn2kYy76x02QN26qr2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "juice": "^8.0.0", + "mathjax-full": "^3.2.0" + } + }, + "node_modules/mathjax-full": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.1.tgz", + "integrity": "sha512-aUz9o16MGZdeiIBwZjAfUBTiJb7LRqzZEl1YOZ8zQMGYIyh1/nxRebxKxjDe9L+xcZCr2OHdzoFBMcd6VnLv9Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esm": "^3.2.25", + "mhchemparser": "^4.1.0", + "mj-context-menu": "^0.6.1", + "speech-rule-engine": "^4.0.6" + } + }, "node_modules/mathxyjax3": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/mathxyjax3/-/mathxyjax3-0.8.3.tgz", @@ -2814,6 +3087,20 @@ "dev": true, "license": "MIT" }, + "node_modules/mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/mhchemparser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz", + "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/micromark-util-character": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", @@ -2908,6 +3195,19 @@ ], "license": "MIT" }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/minisearch": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", @@ -2922,6 +3222,13 @@ "dev": true, "license": "MIT" }, + "node_modules/mj-context-menu": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", + "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -2958,6 +3265,40 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/oniguruma-to-es": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", @@ -2970,6 +3311,23 @@ "regex-recursion": "^6.0.2" } }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3314,6 +3672,16 @@ "node": ">=18" } }, + "node_modules/slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==", + "dev": true, + "license": "MIT (http://mootools.net/license.txt)", + "engines": { + "node": "*" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3345,6 +3713,31 @@ "node": ">=0.10.0" } }, + "node_modules/speech-rule-engine": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.1.4.tgz", + "integrity": "sha512-i/VCLG1fvRc95pMHRqG4aQNscv+9aIsqA2oI7ZQS51sTdUcDHYX6cpT8/tqZ+enjs1tKVwbRBWgxut9SWn+f9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xmldom/xmldom": "0.9.10", + "commander": "13.1.0", + "wicked-good-xpath": "1.3.0" + }, + "bin": { + "sre": "bin/sre" + } + }, + "node_modules/speech-rule-engine/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -3478,6 +3871,13 @@ "node": ">=6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -4182,6 +4582,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -4436,6 +4846,84 @@ } } }, + "node_modules/web-resource-inliner": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", + "integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/web-resource-inliner/node_modules/domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/htmlparser2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/fb55/htmlparser2?sponsor=1" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4469,6 +4957,13 @@ "node": ">=8" } }, + "node_modules/wicked-good-xpath": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", + "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==", + "dev": true, + "license": "MIT" + }, "node_modules/yaml": { "version": "2.8.3", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", @@ -4527,7 +5022,7 @@ "@magmacomputing/library": "2.9.2", "@rollup/plugin-alias": "^6.0.0", "magic-string": "^0.30.21", - "markdown-it-mathjax3": "^5.2.0", + "markdown-it-mathjax3": "^4.3.2", "typedoc": "^0.28.19", "typedoc-plugin-markdown": "^4.11.0", "typedoc-vitepress-theme": "^1.1.2", diff --git a/package.json b/package.json index 1d233c5..36815a2 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@vitest/ui": "^2.1.8", "cross-env": "^10.1.0", "rollup": "^4.60.1", + "markdown-it-mathjax3": "^4.3.2", "tslib": "^2.8.1", "tsx": "^4.21.0", "typescript": "^6.0.2", diff --git a/packages/tempo/.vitepress/config.ts b/packages/tempo/.vitepress/config.ts index 34610d0..7a78bd0 100644 --- a/packages/tempo/.vitepress/config.ts +++ b/packages/tempo/.vitepress/config.ts @@ -126,15 +126,15 @@ export default defineConfig({ }, { find: /^@magmacomputing\/tempo\/parse$/, - replacement: fileURLToPath(new URL('../dist/discrete/discrete.parse.js', import.meta.url)) + replacement: fileURLToPath(new URL('../dist/module/module.parse.js', import.meta.url)) }, { find: /^@magmacomputing\/tempo\/format$/, - replacement: fileURLToPath(new URL('../dist/discrete/discrete.format.js', import.meta.url)) + replacement: fileURLToPath(new URL('../dist/module/module.format.js', import.meta.url)) }, { - find: /^@magmacomputing\/tempo\/discrete$/, - replacement: fileURLToPath(new URL('../dist/discrete/discrete.index.js', import.meta.url)) + find: /^@magmacomputing\/tempo\/module$/, + replacement: fileURLToPath(new URL('../dist/module/module.index.js', import.meta.url)) }, { find: /^@magmacomputing\/tempo$/, diff --git a/packages/tempo/doc/releases/v2.x.md b/packages/tempo/doc/releases/v2.x.md index 3489b6b..b1e33cb 100644 --- a/packages/tempo/doc/releases/v2.x.md +++ b/packages/tempo/doc/releases/v2.x.md @@ -1,5 +1,21 @@ # 📜 Version 2.x History +## [v2.9.2] - 2026-05-08 +### New Features +- Enhanced Temporal support with improved timezone and calendar handling +- Added documentation for prototyping with regexp patterns +- Expanded installation guidance for Node.js Temporal support +### Bug Fixes +- Improved parsing robustness with better symbol mapping and prototype-shadowing fixes +- Enhanced identity-based layout resolution +- Strengthened lifecycle handling for timezone and calendar operations +### Improvements +- Optimized performance with dedicated guard and normalizer modules +- Updated UI styling with theme-aware color variables +- Refined error handling in engine components +### Documentation +- Expanded main branch protection guidance +- Enhanced architecture documentation with modular design overview ## [v2.9.0] - 2026-05-06 ### 🏗️ Alias Architecture Stabilization diff --git a/packages/tempo/package.json b/packages/tempo/package.json index 22376e2..2307383 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -224,7 +224,7 @@ "@magmacomputing/library": "2.9.2", "@rollup/plugin-alias": "^6.0.0", "magic-string": "^0.30.21", - "markdown-it-mathjax3": "^5.2.0", + "markdown-it-mathjax3": "^4.3.2", "typedoc": "^0.28.19", "typedoc-plugin-markdown": "^4.11.0", "typedoc-vitepress-theme": "^1.1.2",