From aadb7da8d5bc100615a3b98bc3b70426d0a04632 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Sat, 2 May 2026 19:04:34 +1000 Subject: [PATCH 1/8] chore: prepare Release D (v2.9.0) --- package.json | 2 +- packages/library/package.json | 2 +- packages/tempo/package.json | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 398ec0a..9c49f85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tempo-monorepo", - "version": "2.8.0", + "version": "2.9.0", "private": true, "description": "Magma Computing Monorepo", "repository": { diff --git a/packages/library/package.json b/packages/library/package.json index 870ab1a..9915cf7 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/library", - "version": "2.8.0", + "version": "2.9.0", "description": "Shared utility library for Tempo", "author": "Magma Computing Solutions", "license": "MIT", diff --git a/packages/tempo/package.json b/packages/tempo/package.json index 0b7c6b8..c9848cd 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/tempo", - "version": "2.8.0", + "version": "2.9.0", "description": "The Tempo core library", "author": "Magma Computing Solutions", "license": "MIT", @@ -238,7 +238,7 @@ }, "devDependencies": { "@js-temporal/polyfill": "^0.5.1", - "@magmacomputing/library": "2.8.0", + "@magmacomputing/library": "2.9.0", "@rollup/plugin-alias": "^6.0.0", "cross-env": "^7.0.3", "magic-string": "^0.30.21", From 79d3cace6b70f4d5df7d418b6ac468859ee2cfd6 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Sat, 2 May 2026 19:23:33 +1000 Subject: [PATCH 2/8] fix doco --- package.json | 2 +- packages/library/package.json | 2 +- packages/tempo/archive/tempo.api.md | 6 +++--- packages/tempo/package.json | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 9c49f85..0e2644a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tempo-monorepo", - "version": "2.9.0", + "version": "2.9.1", "private": true, "description": "Magma Computing Monorepo", "repository": { diff --git a/packages/library/package.json b/packages/library/package.json index 9915cf7..1ba9e56 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/library", - "version": "2.9.0", + "version": "2.9.1", "description": "Shared utility library for Tempo", "author": "Magma Computing Solutions", "license": "MIT", diff --git a/packages/tempo/archive/tempo.api.md b/packages/tempo/archive/tempo.api.md index 6e7b5af..8a697dd 100644 --- a/packages/tempo/archive/tempo.api.md +++ b/packages/tempo/archive/tempo.api.md @@ -5,7 +5,7 @@ This document provides a comprehensive technical reference for the `Tempo` class --- - [TypeScript Types Reference](./tempo.types.md) -- [Tempo Cookbook](./tempo.cookbook.md) +- [Tempo Cookbook](../doc/tempo.cookbook.md) --- @@ -67,7 +67,7 @@ Retrieves or registers a `Symbol` for internal token mapping. ### `Tempo.ticker(arg1?, arg2?)` (Plugin required) Creates a reactive stream of `Tempo` instances at regular intervals. - **Returns:** An `AsyncGenerator` (if no callback) or a `stop` function (if callback provided). -- **See:** [Tempo Ticker Guide](./tempo.ticker.md) for the full polymorphic signature and usage patterns. +- **See:** [Tempo Ticker Guide](../doc/tempo.ticker.md) for the full polymorphic signature and usage patterns. ### `Tempo.regexp(layout, snippet?)` Translates a Tempo layout string into a compiled `RegExp`. @@ -201,5 +201,5 @@ Returns a `Temporal.PlainDateTime` representation. ::: tip **Looking for the full technical details?** -For an exhaustive, auto-generated reference of every property, internal type, and class member, see our [Full Technical API Reference](./api/README.md). +For an exhaustive, auto-generated reference of every property, internal type, and class member, see our [Full Technical API Reference](../doc/api/index.md). ::: diff --git a/packages/tempo/package.json b/packages/tempo/package.json index c9848cd..393b602 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/tempo", - "version": "2.9.0", + "version": "2.9.1", "description": "The Tempo core library", "author": "Magma Computing Solutions", "license": "MIT", @@ -238,7 +238,7 @@ }, "devDependencies": { "@js-temporal/polyfill": "^0.5.1", - "@magmacomputing/library": "2.9.0", + "@magmacomputing/library": "2.9.1", "@rollup/plugin-alias": "^6.0.0", "cross-env": "^7.0.3", "magic-string": "^0.30.21", From e462af785172484e14295c524e3251e069406dd7 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Sun, 3 May 2026 14:54:33 +1000 Subject: [PATCH 3/8] 1st draft PR --- packages/library/package.json | 2 +- packages/tempo/.vitepress/config.ts | 1 + packages/tempo/package.json | 2 +- packages/tempo/plan/.WISHLIST.md | 127 +++++++++++- packages/tempo/plan/RELEASE-D.md | 171 ++++++++++++++++ packages/tempo/src/engine/engine.alias.ts | 185 ++++++++++++++++++ .../src/engine/engine.layout.resolver.ts.bak | 0 .../tempo/src/engine/engine.layout.ts.bak | 0 packages/tempo/src/tempo.class.ts | 73 +++---- packages/tempo/src/tempo.type.ts | 2 + packages/tempo/test/core/alias-engine.test.ts | 103 ++++++++++ .../tempo/test/core/sandbox-factory.test.ts | 10 +- .../tempo/test/support/setup.console-spy.ts | 34 ++-- 13 files changed, 625 insertions(+), 85 deletions(-) create mode 100644 packages/tempo/plan/RELEASE-D.md create mode 100644 packages/tempo/src/engine/engine.alias.ts delete mode 100644 packages/tempo/src/engine/engine.layout.resolver.ts.bak delete mode 100644 packages/tempo/src/engine/engine.layout.ts.bak create mode 100644 packages/tempo/test/core/alias-engine.test.ts diff --git a/packages/library/package.json b/packages/library/package.json index 1ba9e56..789c165 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -88,7 +88,7 @@ "test": "vitest run", "build": "tsc -b", "clean": "tsc -b --clean", - "prepublishOnly": "npm run build" + "prepublishOnly": "if [ $(git rev-parse --abbrev-ref HEAD) != main ]; then echo 'ERROR: Must be on main branch to publish.'; exit 1; fi && npm run build" }, "dependencies": { "tslib": "^2.8.1" diff --git a/packages/tempo/.vitepress/config.ts b/packages/tempo/.vitepress/config.ts index 5a73754..82ad0fd 100644 --- a/packages/tempo/.vitepress/config.ts +++ b/packages/tempo/.vitepress/config.ts @@ -12,6 +12,7 @@ export default defineConfig({ base: '/magma/', title: "Tempo", description: "The Professional Date-Time Library for Temporal", + srcDir: './doc', markdown: { math: true }, diff --git a/packages/tempo/package.json b/packages/tempo/package.json index 393b602..374ed6d 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -223,7 +223,7 @@ "build:resolve": "tsx bin/resolve-types.ts", "clean": "rm -rf dist && (tsc -b --clean || true)", "publish": "npm publish --access public", - "prepublishOnly": "npm run build", + "prepublishOnly": "if [ $(git rev-parse --abbrev-ref HEAD) != main ]; then echo 'ERROR: Must be on main branch to publish.'; exit 1; fi && npm run build", "docs:api": "typedoc", "docs:dev": "npm run build && npm run docs:api && vitepress dev", "docs:build": "npm run build && npm run docs:api && vitepress build", diff --git a/packages/tempo/plan/.WISHLIST.md b/packages/tempo/plan/.WISHLIST.md index e410313..24c7d41 100644 --- a/packages/tempo/plan/.WISHLIST.md +++ b/packages/tempo/plan/.WISHLIST.md @@ -155,13 +155,128 @@ Exit criteria: ### Release D: deeper decomposition cleanup -- [ ] Extract Pattern Compiler + Cache module. -- [ ] Extract Alias Resolution Engine module. -- [ ] Optional: extract Guard Builder and Parse Result Normalizer if churn justifies. +--- + +#### 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. -Exit criteria: -- Reduced cyclomatic complexity in primary class engine. -- Internal module contracts documented and covered by focused unit tests. +**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) diff --git a/packages/tempo/plan/RELEASE-D.md b/packages/tempo/plan/RELEASE-D.md new file mode 100644 index 0000000..f2caf2e --- /dev/null +++ b/packages/tempo/plan/RELEASE-D.md @@ -0,0 +1,171 @@ +# 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 +- [ ] 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 + +### 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 + +### 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 + +### 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 + +## 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 — Assessment Outline +**Purpose:** +Evaluate the value and feasibility of extracting all logic related to token ingestion and fast-fail guard rebuild lifecycle into a dedicated module. + +**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. + +**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. + +**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. + +### 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. + +**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. diff --git a/packages/tempo/src/engine/engine.alias.ts b/packages/tempo/src/engine/engine.alias.ts new file mode 100644 index 0000000..37744cb --- /dev/null +++ b/packages/tempo/src/engine/engine.alias.ts @@ -0,0 +1,185 @@ +// engine.alias.ts +// Alias Resolution Engine for Tempo +// Responsible for event/period alias mapping, collision detection, and snippet rebinding + +import { asType } from '#library/type.library.js'; +import type { Logify } from '#library/logify.class.js'; + +export type AliasTarget = string | Function + +export interface AliasEngineOptions { + parent?: AliasEngine | undefined; + logger?: Logify | undefined; +} + +export class AliasEngine { + #parentEngine?: AliasEngineOptions["parent"]; + #logger?: AliasEngineOptions["logger"]; + + constructor(options: AliasEngineOptions = {}) { + this.#parentEngine = options.parent; + this.#logger = options.logger; + } + + /** + * Detect likely overlap between two alias keys/patterns (moved from Tempo) + */ + static isAliasCollision(a: string, b: string): boolean { + const left = a.trim().toLowerCase(); + const right = b.trim().toLowerCase(); + + if (!left || !right) return false; + if (left === right) return true; + + // Extract the 'core' characters to determine if they conceptually target the same word + const getBaseWord = (s: string) => s + .replace(/\[[^\]]*\]\?/g, '') // remove optional character classes (e.g. [ -]?) + .replace(/.\?/g, '') // remove optional single characters (e.g. s?) + .replace(/[^a-z0-9]/g, ''); // remove all non-alphanumeric characters (regex metachars, spaces, hyphens) + + const baseLeft = getBaseWord(left); + const baseRight = getBaseWord(right); + + if (!baseLeft || !baseRight) return false; + + return baseLeft === baseRight; + } + + #eventMap: Map = new Map(); + #periodMap: Map = new Map(); + #eventCollisions: Map = new Map(); + #periodCollisions: Map = new Map(); + + // Event alias management + registerEventAlias(name: string, target: AliasTarget): void { + this.#registerAliasWithCollision(name, target, this.#eventMap, this.#eventCollisions, 'event'); + } + + registerEvents(events: [string, AliasTarget][]): void { + for (const [name, target] of events) + this.registerEventAlias(name, target); + } + resolveEventAlias(name: string, thisArg?: any) { + return this.#resolveAlias(name, this.#eventMap, thisArg); + } + hasEventAlias(name: string): boolean { + return this.#eventMap.has(name); + } + getAllEventAliases(): Record { + return Object.fromEntries(this.#eventMap.entries()); + } + detectEventCollisions(): Record { + return Object.fromEntries(this.#eventCollisions.entries()); + } + + // Period alias management + registerPeriodAlias(name: string, target: AliasTarget): void { + this.#registerAliasWithCollision(name, target, this.#periodMap, this.#periodCollisions, 'period'); + } + + registerPeriods(periods: [string, AliasTarget][]): void { + for (const [name, target] of periods) + this.registerPeriodAlias(name, target); + } + resolvePeriodAlias(name: string, thisArg?: any) { + return this.#resolveAlias(name, this.#periodMap, thisArg); + } + hasPeriodAlias(name: string): boolean { + return this.#periodMap.has(name); + } + getAllPeriodAliases(): Record { + return Object.fromEntries(this.#periodMap.entries()); + } + detectPeriodCollisions(): Record { + return Object.fromEntries(this.#periodCollisions.entries()); + } + + // Shared logic + #registerAliasWithCollision( + name: string, + target: AliasTarget, + map: Map, + collisions: Map, + type: 'event' | 'period' + ) { + let collisionDetected = false; + // Check for local collisions using isAliasCollision + for (const [existingName, existingTarget] of map.entries()) { + if ( + existingTarget !== target && + AliasEngine.isAliasCollision(existingName, name) + ) { + const existing = collisions.get(existingName) || []; + collisions.set( + existingName, + Array.from(new Set([...existing, target, existingTarget])) + ); + collisionDetected = true; + } + } + + // Check for parent collisions using isAliasCollision + let parent = this.#parentEngine; + while (parent) { + const parentMap = type === 'event' ? parent.#eventMap : parent.#periodMap; + for (const [parentName, parentTarget] of parentMap.entries()) { + if ( + parentTarget !== target && + AliasEngine.isAliasCollision(parentName, name) + ) { + const parentCollisions = collisions.get(parentName) || []; + collisions.set( + parentName, + Array.from(new Set([...parentCollisions, target, parentTarget])) + ); + collisionDetected = true; + } + } + parent = parent.#parentEngine; + } + + if (collisionDetected && this.#logger) { + this.#logger.warn( + `[AliasEngine] Potential Collision detected for ${type} alias "${name}". Multiple definitions found. This may shadow or overwrite an existing alias.` + ); + } + + map.set(name, target); + } + + #resolveAlias(name: string, map: Map, thisArg?: any): string { + let currentEngine: AliasEngine | undefined = this; + + while (currentEngine) { + const { type, value } = asType(map.get(name)); + switch (type) { + case 'Function': + return value.call(thisArg); + + case 'String': + return value; + + default: + currentEngine = currentEngine.#parentEngine; + map = currentEngine + ? (map === this.#eventMap + ? currentEngine.#eventMap + : currentEngine.#periodMap) + : map; + } + } + + return name; + } + + clear(type?: 'event' | 'period'): void { + if (!type || type === 'event') { + this.#eventMap.clear(); + this.#eventCollisions.clear(); + } + if (!type || type === 'period') { + this.#periodMap.clear(); + this.#periodCollisions.clear(); + } + } +} diff --git a/packages/tempo/src/engine/engine.layout.resolver.ts.bak b/packages/tempo/src/engine/engine.layout.resolver.ts.bak deleted file mode 100644 index e69de29..0000000 diff --git a/packages/tempo/src/engine/engine.layout.ts.bak b/packages/tempo/src/engine/engine.layout.ts.bak deleted file mode 100644 index e69de29..0000000 diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index 510f7d3..8050b8a 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -13,14 +13,15 @@ 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 type { Property, Secure } from '#library/type.library.js'; import { instant } 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 { AliasEngine } from './engine/engine.alias.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'; @@ -39,6 +40,7 @@ declare module '#library/type.library.js' { /** */ const ClassStates = new WeakMap(); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ namespace Internal { + // ...existing code... export type State = t.Internal.State; export type Parse = t.Internal.Parse; export type MatchResult = t.Internal.Match; @@ -134,9 +136,6 @@ export class Tempo { Tempo.#dbg.debug(config, ...args); } - // ...rest of the class definition remains unchanged... - - /** * {dt} is a layout that combines date-related {snippets} (e.g. dd, mm -or- evt) into a pattern against which a string can be tested. * because it will also include a list of events (e.g. 'new_years' | 'xmas'), we need to rebuild {dt} if the user adds a new event @@ -147,7 +146,12 @@ export class Tempo { if (isLocal(shape) && !hasOwn(shape.parse, 'event') && !hasOwn(shape.parse.monthDay, 'active')) return; // no local change needed - const src = shape.config.scope === 'global' ? 'g' : 'l'; // 'g'lobal or 'l'ocal (sandbox also uses 'l') + // Use the correct alias engine: static for global, instance for local, and assign parentEngine for locals + const engine = shape.aliasEngine ??= new AliasEngine({ parent: Tempo.#global.aliasEngine, logger: Tempo.#dbg }); + engine.clear('event'); + engine.registerEvents(events); + + const src = shape.config.scope === 'global' ? 'g' : 'l';// 'g'lobal or 'l'ocal (sandbox also uses 'l') const groups = events .map(([pat, _], idx) => `(?<${src}evt${idx}>${pat})`) // assign a number to the pattern .join('|') // make an 'Or' pattern for the event-keys @@ -187,19 +191,14 @@ export class Tempo { static [$setPeriods](shape: Internal.State) { const periods = ownEntries(shape.parse.period, true); if (isLocal(shape) && !hasOwn(shape.parse, 'period')) - return; // no local change needed - - const src = shape.config.scope === 'global' ? 'g' : 'l'; // 'g'lobal or 'l'ocal (sandbox also uses 'l') + return; // no local change needed - // Check for alias collisions among period keys - const keys = periods.map(([pat]) => pat); - for (let i = 0; i < keys.length; i++) { - for (let j = i + 1; j < keys.length; j++) { - if (Tempo.#isAliasCollision(keys[i], keys[j])) - Tempo.#dbg.warn(`Potential period alias collision: "${keys[i]}" overlaps with existing alias(es): ${keys[j]}`); - } - } + // Use the correct alias engine: static for global, instance for local + const engine = shape.aliasEngine ??= new AliasEngine({ parent: Tempo.#global.aliasEngine, logger: Tempo.#dbg }); + engine.clear('period'); + engine.registerPeriods(periods); + const src = shape.config.scope === 'global' ? 'g' : 'l';// 'g'lobal or 'l'ocal (sandbox also uses 'l') const groups = periods .map(([pat, _], idx) => `(?<${src}per${idx}>${pat})`) // {pattern} is the 1st element of the tuple .join('|') // make an 'or' pattern for the period-keys @@ -302,28 +301,6 @@ export class Tempo { locale // cannot determine locale } - /** detect likely overlap between two alias keys/patterns */ - static #isAliasCollision(a: string, b: string): boolean { - const left = a.trim().toLowerCase(); - const right = b.trim().toLowerCase(); - - if (!left || !right) return false; - if (left === right) return true; - - // Extract the 'core' characters to determine if they conceptually target the same word - const getBaseWord = (s: string) => s - .replace(/\[[^\]]*\]\?/g, '') // remove optional character classes (e.g. [ -]?) - .replace(/.\?/g, '') // remove optional single characters (e.g. s?) - .replace(/[^a-z0-9]/g, ''); // remove all non-alphanumeric characters (regex metachars, spaces, hyphens) - - const baseLeft = getBaseWord(left); - const baseRight = getBaseWord(right); - - if (!baseLeft || !baseRight) return false; - - return baseLeft === baseRight; - } - /** * conform input of Snippet / Layout / Event / Period options * This is needed because we allow the user to flexibly provide detail as {[key]:val} or {[key]:val}[] or [key,val][] @@ -540,13 +517,6 @@ export class Tempo { return proxify(omit({ ...discovery, scope: 'discovery' }, 'value')); } - /** - * Unified loader for library extensions. - * - * @param arg - A `Plugin` function, a `TermPlugin` object (or array), or a `Discovery` object. - * @param options - Optional configuration for a standard `Plugin`. - * @returns The `Tempo` class for chaining. - */ /** * Register a plugin or term extension. * @@ -1271,11 +1241,6 @@ export class Tempo { // shadowing chain (only if extensible) if (Reflect.isExtensible(target)) Object.defineProperty(target, name, { get, enumerable: true, configurable: true }); - // if (Reflect.isExtensible(target)) { - // const shadow = Object.create(Object.getPrototypeOf(target)); - // Object.defineProperty(shadow, name, { get, enumerable: true, configurable: true }); - // Object.setPrototypeOf(target, shadow); - // } return get; // return getter closure } @@ -1499,6 +1464,13 @@ export class Tempo { this.#local.parse.planner = { ...classState.parse.planner }; // clone the planner object setProperty(this.#local.parse, 'result', [...(options.result ?? [])]); + Object.defineProperty(this.#local, 'tempoInstance', { // Link this instance to its state for static alias access + value: this, + writable: false, + configurable: true, + enumerable: false + }); + (this.constructor as any)[$setConfig](this.#local, options); // set #local config } @@ -1623,4 +1595,3 @@ export namespace Tempo { export interface Params extends t.Params { } } - diff --git a/packages/tempo/src/tempo.type.ts b/packages/tempo/src/tempo.type.ts index 78b9db5..e40460c 100644 --- a/packages/tempo/src/tempo.type.ts +++ b/packages/tempo/src/tempo.type.ts @@ -15,6 +15,7 @@ import type { IntRange, NonOptional, Property, Plural, Prettify, TemporalObject, 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'; declare global { interface globalThis { @@ -223,6 +224,7 @@ export namespace Internal { /** @internal current anchor during parsing */ anchor?: Temporal.ZonedDateTime; /** @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; } /** debug a Tempo instantiation */ diff --git a/packages/tempo/test/core/alias-engine.test.ts b/packages/tempo/test/core/alias-engine.test.ts new file mode 100644 index 0000000..a24b8ed --- /dev/null +++ b/packages/tempo/test/core/alias-engine.test.ts @@ -0,0 +1,103 @@ +import { AliasEngine } from '#tempo/engine/engine.alias.js'; +import { Logify } from '#library/logify.class.js'; + +// Simple logger mock +const logger = new Logify({ debug: true }); +// const logger = { +// warn: vi.fn(), +// }; + +beforeEach(() => { + // logger.warn.mockClear(); +}); + +describe('AliasEngine', () => { + it('registers and resolves string and function aliases', () => { + const engine = new AliasEngine({ logger }); + engine.registerEventAlias('foo', 'bar'); + expect(engine.resolveEventAlias('foo')).toBe('bar'); + engine.registerPeriodAlias('noon', function () { return '12:00'; }); + expect(engine.resolvePeriodAlias('noon')).toBe('12:00'); + }); + + it('supports parent/child shadowing and fallback', () => { + const globalEngine = new AliasEngine({ logger }); + globalEngine.registerEventAlias('foo', 'bar'); + const localEngine = new AliasEngine({ parent: globalEngine, logger }); + expect(localEngine.resolveEventAlias('foo')).toBe('bar'); + localEngine.registerEventAlias('foo', 'baz'); + expect(localEngine.resolveEventAlias('foo')).toBe('baz'); + expect(globalEngine.resolveEventAlias('foo')).toBe('bar'); + }); + + it('warns on local/global collision', () => { + const globalEngine = new AliasEngine({ logger }); + globalEngine.registerPeriodAlias('noon', '12:00'); + const localEngine = new AliasEngine({ parent: globalEngine, logger }); + localEngine.registerPeriodAlias('noon', '11:00'); + expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('noon')); + }); + + it('warns on local collision', () => { + const engine = new AliasEngine({ logger }); + engine.registerEventAlias('foo', 'bar'); + engine.registerEventAlias('foo', 'baz'); + expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('foo')); + }); + + it('registers and resolves batch aliases', () => { + const engine = new AliasEngine({ logger }); + engine.registerEvents([ + ['a', '1'], + ['b', '2'], + ]); + expect(engine.resolveEventAlias('a')).toBe('1'); + expect(engine.resolveEventAlias('b')).toBe('2'); + }); + + it('clears only events or periods', () => { + const engine = new AliasEngine({ logger }); + engine.registerEventAlias('foo', 'bar'); + engine.registerPeriodAlias('noon', '12:00'); + engine.clear('event'); + expect(engine.resolveEventAlias('foo')).toBe('foo'); + expect(engine.resolvePeriodAlias('noon')).toBe('12:00'); + engine.clear('period'); + expect(engine.resolvePeriodAlias('noon')).toBe('noon'); + }); + + it('handles regex-like collision heuristics', () => { + const globalEngine = new AliasEngine({ logger }); + globalEngine.registerPeriodAlias('noon', '12:00'); + const localEngine = new AliasEngine({ parent: globalEngine, logger }); + localEngine.registerPeriodAlias('([after[ -]?])?noon', '11:00'); + + // This should warn, even if not a perfect regex match + expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('noon')); + }); + + it('does not warn on non-colliding aliases', () => { + const engine = new AliasEngine({ logger }); + engine.registerEventAlias('foo', 'bar'); + engine.registerEventAlias('baz', 'qux'); + expect(console.warn).not.toHaveBeenCalled(); + }); + + it('resolves to parent after clear', () => { + const globalEngine = new AliasEngine({ logger }); + globalEngine.registerEventAlias('foo', 'bar'); + const localEngine = new AliasEngine({ parent: globalEngine, logger }); + localEngine.registerEventAlias('foo', 'baz'); + expect(localEngine.resolveEventAlias('foo')).toBe('baz'); + localEngine.clear('event'); + expect(localEngine.resolveEventAlias('foo')).toBe('bar'); + }); + + it('handles empty/optional/edge-case aliases', () => { + const engine = new AliasEngine({ logger }); + engine.registerEventAlias('', 'empty'); + expect(engine.resolveEventAlias('')).toBe('empty'); + engine.registerEventAlias('?', 'question'); + expect(engine.resolveEventAlias('?')).toBe('question'); + }); +}); diff --git a/packages/tempo/test/core/sandbox-factory.test.ts b/packages/tempo/test/core/sandbox-factory.test.ts index b88526b..a1052ef 100644 --- a/packages/tempo/test/core/sandbox-factory.test.ts +++ b/packages/tempo/test/core/sandbox-factory.test.ts @@ -1,4 +1,5 @@ import { Tempo } from '#tempo'; +import { spies } from '../support/setup.console-spy.js'; describe('Sandbox Factory Pattern', () => { it('should return a subclass when create is called with options', () => { @@ -24,12 +25,18 @@ describe('Sandbox Factory Pattern', () => { }); it('should support shadowing global aliases', () => { + const warnCountBefore = spies.warn.mock.calls.length; + // Global 'noon' is 12:00 const EarlyNoon = Tempo.create({ period: { 'noon': '11:00' } }); + + expect(console.error).not.toHaveBeenCalled(); + expect(console.warn).toHaveBeenCalled(); + expect(spies.warn.mock.calls.length).toBe(warnCountBefore + 1); // Original remains unaffected (if not manually reset in a way that changes it) // We expect 12:00 for the base Tempo @@ -38,9 +45,6 @@ describe('Sandbox Factory Pattern', () => { const t2 = new EarlyNoon('noon'); expect(t2.hh).toBe(11); - - expect(console.warn).not.toHaveBeenCalled(); - expect(console.error).not.toHaveBeenCalled(); }); it('should record traceability info in parse results', () => { diff --git a/packages/tempo/test/support/setup.console-spy.ts b/packages/tempo/test/support/setup.console-spy.ts index 04de23c..712f735 100644 --- a/packages/tempo/test/support/setup.console-spy.ts +++ b/packages/tempo/test/support/setup.console-spy.ts @@ -1,30 +1,18 @@ import { vi, afterAll, beforeEach } from 'vitest'; -// Global console suppression for all tests -// (You can comment out lines to allow specific console methods) - -declare global { - // eslint-disable-next-line no-var - var _consoleSpies: Array>; - - // Note: To use mockClear/mockRestore on console methods in tests, use (console.error as any).mockClear() +// 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(() => {}), } -/** setup global console spies before all tests */ -globalThis._consoleSpies = [ - vi.spyOn(console, 'error').mockImplementation(() => { }), - vi.spyOn(console, 'warn').mockImplementation(() => { }), - vi.spyOn(console, 'debug').mockImplementation(() => { }), - vi.spyOn(console, 'log').mockImplementation(() => { }), - vi.spyOn(console, 'info').mockImplementation(() => { }), -]; - -/** restore global console spies after all tests */ -afterAll(() => { - globalThis._consoleSpies.forEach(spy => spy.mockRestore()); +beforeEach(() => { + Object.values(spies).forEach(spy => spy.mockClear()); }); -/** clear global console spies before each test */ -beforeEach(() => { - globalThis._consoleSpies.forEach(spy => (spy as any).mockClear()); +afterAll(() => { + Object.values(spies).forEach(spy => spy.mockRestore()); }); From 2af76987347181eb961a67fbf8c3eca1ebd54fb7 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Sun, 3 May 2026 15:29:13 +1000 Subject: [PATCH 4/8] 2nd draft PR review --- .github/workflows/ci.yml | 45 +++++++++++++++++ packages/tempo/src/engine/engine.alias.ts | 13 +++-- packages/tempo/src/tempo.class.ts | 6 ++- .../test/core/alias-engine-protochain.test.ts | 48 +++++++++++++++++++ 4 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 packages/tempo/test/core/alias-engine-protochain.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edfff3c..5a84ed0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,28 @@ jobs: timeout-minutes: 30 if: github.ref == 'refs/heads/release-c-layout-order-planner' || github.event.pull_request.base.ref == 'main' steps: + - name: Print GitHub event context (for debug) + run: | + echo "GITHUB_REF: $GITHUB_REF" + echo "GITHUB_EVENT_NAME: $GITHUB_EVENT_NAME" + echo "GITHUB_HEAD_REF: $GITHUB_HEAD_REF" + echo "GITHUB_BASE_REF: $GITHUB_BASE_REF" + echo "github.ref: ${{ github.ref }}" + echo "github.event_name: ${{ github.event_name }}" + echo "github.head_ref: ${{ github.head_ref }}" + echo "github.base_ref: ${{ github.base_ref }}" + echo "github.event.pull_request.base.ref: ${{ github.event.pull_request.base.ref }}" + echo "github.sha: ${{ github.sha }}" + echo "github.repository: ${{ github.repository }}" + echo "github.actor: ${{ github.actor }}" + echo "github.workflow: ${{ github.workflow }}" + echo "github.run_id: ${{ github.run_id }}" + echo "github.run_number: ${{ github.run_number }}" + echo "github.job: ${{ github.job }}" + echo "github.ref_name: ${{ github.ref_name }}" + echo "github.ref_type: ${{ github.ref_type }}" + echo "github.event: $(cat $GITHUB_EVENT_PATH)" + shell: bash - uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 @@ -69,3 +91,26 @@ jobs: 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 }} + - 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/packages/tempo/src/engine/engine.alias.ts b/packages/tempo/src/engine/engine.alias.ts index 37744cb..37109b3 100644 --- a/packages/tempo/src/engine/engine.alias.ts +++ b/packages/tempo/src/engine/engine.alias.ts @@ -5,7 +5,7 @@ import { asType } from '#library/type.library.js'; import type { Logify } from '#library/logify.class.js'; -export type AliasTarget = string | Function +export type AliasTarget = string | number | Function export interface AliasEngineOptions { parent?: AliasEngine | undefined; @@ -147,8 +147,9 @@ export class AliasEngine { map.set(name, target); } - #resolveAlias(name: string, map: Map, thisArg?: any): string { + #resolveAlias(name: string, map: Map, thisArg?: any): string | number { let currentEngine: AliasEngine | undefined = this; + const isEvent = map === this.#eventMap; while (currentEngine) { const { type, value } = asType(map.get(name)); @@ -157,15 +158,13 @@ export class AliasEngine { return value.call(thisArg); case 'String': + case 'Number': return value; default: currentEngine = currentEngine.#parentEngine; - map = currentEngine - ? (map === this.#eventMap - ? currentEngine.#eventMap - : currentEngine.#periodMap) - : map; + if (currentEngine) + map = isEvent ? currentEngine.#eventMap : currentEngine.#periodMap; } } diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index 8050b8a..9cd7dcb 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -147,7 +147,8 @@ export class Tempo { return; // no local change needed // Use the correct alias engine: static for global, instance for local, and assign parentEngine for locals - const engine = shape.aliasEngine ??= new AliasEngine({ parent: Tempo.#global.aliasEngine, logger: Tempo.#dbg }); + const proto = Object.getPrototypeOf(shape); + const engine = shape.aliasEngine ??= new AliasEngine({ parent: proto.aliasEngine, logger: Tempo.#dbg }); engine.clear('event'); engine.registerEvents(events); @@ -194,7 +195,8 @@ export class Tempo { return; // no local change needed // Use the correct alias engine: static for global, instance for local - const engine = shape.aliasEngine ??= new AliasEngine({ parent: Tempo.#global.aliasEngine, logger: Tempo.#dbg }); + const proto = Object.getPrototypeOf(shape); + const engine = shape.aliasEngine ??= new AliasEngine({ parent: proto.aliasEngine, logger: Tempo.#dbg }); engine.clear('period'); engine.registerPeriods(periods); diff --git a/packages/tempo/test/core/alias-engine-protochain.test.ts b/packages/tempo/test/core/alias-engine-protochain.test.ts new file mode 100644 index 0000000..1cf32a5 --- /dev/null +++ b/packages/tempo/test/core/alias-engine-protochain.test.ts @@ -0,0 +1,48 @@ +import { AliasEngine } from '#tempo/engine/engine.alias.js'; +import { Logify } from '#library/logify.class.js'; + +describe('AliasEngine prototype chain (Global → Sandbox → Instance)', () => { + const logger = new Logify({ debug: true }); + + // Simulate a global state + const globalShape = {} as { aliasEngine: AliasEngine }; + globalShape.aliasEngine = new AliasEngine({ logger }); + globalShape.aliasEngine.registerEventAlias('globalEvt', 'globalValue'); + + // Simulate a sandbox state inheriting from global + const sandboxShape = Object.create(globalShape); + sandboxShape.aliasEngine = new AliasEngine({ parent: globalShape.aliasEngine, logger }); + sandboxShape.aliasEngine.registerEventAlias('sandboxEvt', 'sandboxValue'); + + // Simulate a local/instance state inheriting from sandbox + const localShape = Object.create(sandboxShape); + localShape.aliasEngine = new AliasEngine({ parent: sandboxShape.aliasEngine, logger }); + localShape.aliasEngine.registerEventAlias('localEvt', 'localValue'); + + it('resolves local, sandbox, and global aliases in correct order', () => { + // Local should resolve its own + expect(localShape.aliasEngine.resolveEventAlias('localEvt')).toBe('localValue'); + // Local should resolve sandbox + expect(localShape.aliasEngine.resolveEventAlias('sandboxEvt')).toBe('sandboxValue'); + // Local should resolve global + expect(localShape.aliasEngine.resolveEventAlias('globalEvt')).toBe('globalValue'); + // Sandbox should not see local + expect(sandboxShape.aliasEngine.resolveEventAlias('localEvt')).toBe('localEvt'); + // Sandbox should resolve its own and global + expect(sandboxShape.aliasEngine.resolveEventAlias('sandboxEvt')).toBe('sandboxValue'); + expect(sandboxShape.aliasEngine.resolveEventAlias('globalEvt')).toBe('globalValue'); + // Global should only resolve its own + expect(globalShape.aliasEngine.resolveEventAlias('globalEvt')).toBe('globalValue'); + expect(globalShape.aliasEngine.resolveEventAlias('sandboxEvt')).toBe('sandboxEvt'); + expect(globalShape.aliasEngine.resolveEventAlias('localEvt')).toBe('localEvt'); + }); + + it('collision detection traverses the prototype chain', () => { + // Register a colliding alias in local + localShape.aliasEngine.registerEventAlias('globalEvt', 'localShadow'); + // Should warn about collision with global + // (You may want to spy on logger.warn for a real assertion) + expect(localShape.aliasEngine.resolveEventAlias('globalEvt')).toBe('localShadow'); + expect(globalShape.aliasEngine.resolveEventAlias('globalEvt')).toBe('globalValue'); + }); +}); From 945d84565e68233237f5be718d9518a3866c1b6e Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Sun, 3 May 2026 20:00:15 +1000 Subject: [PATCH 5/8] 3rd draft PR review --- packages/tempo/src/engine/engine.alias.ts | 6 ++++-- packages/tempo/src/support/tempo.init.ts | 4 ++-- packages/tempo/src/tempo.class.ts | 12 ++++++++---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/tempo/src/engine/engine.alias.ts b/packages/tempo/src/engine/engine.alias.ts index 37109b3..cf5329d 100644 --- a/packages/tempo/src/engine/engine.alias.ts +++ b/packages/tempo/src/engine/engine.alias.ts @@ -121,8 +121,8 @@ export class AliasEngine { // Check for parent collisions using isAliasCollision let parent = this.#parentEngine; while (parent) { - const parentMap = type === 'event' ? parent.#eventMap : parent.#periodMap; - for (const [parentName, parentTarget] of parentMap.entries()) { + const parentAliases = type === 'event' ? parent.getAllEventAliases() : parent.getAllPeriodAliases(); + for (const [parentName, parentTarget] of Object.entries(parentAliases)) { if ( parentTarget !== target && AliasEngine.isAliasCollision(parentName, name) @@ -135,6 +135,8 @@ export class AliasEngine { collisionDetected = true; } } + // Access parent's parent engine via a public way if possible, or keep it private if safe + // Since we're in the same class, we can still use #parentEngine for the chain parent = parent.#parentEngine; } diff --git a/packages/tempo/src/support/tempo.init.ts b/packages/tempo/src/support/tempo.init.ts index 8de3be3..5d15b76 100644 --- a/packages/tempo/src/support/tempo.init.ts +++ b/packages/tempo/src/support/tempo.init.ts @@ -22,10 +22,10 @@ export function init(options: t.Options = {}, isGlobal = true, baseState?: t.Int if (isGlobal && runtime.state && !baseState) return runtime.state; const { timeZone, calendar } = getDateTimeFormat(); - const state = { + const state = (baseState ? Object.create(baseState) : { config: {}, parse: {} - } as t.Internal.State + }) as t.Internal.State; // 1. Establish the base parsing state state.parse = markConfig({ diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index 9cd7dcb..f2eeb25 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -147,8 +147,10 @@ export class Tempo { return; // no local change needed // Use the correct alias engine: static for global, instance for local, and assign parentEngine for locals - const proto = Object.getPrototypeOf(shape); - const engine = shape.aliasEngine ??= new AliasEngine({ parent: proto.aliasEngine, logger: Tempo.#dbg }); + const parent = proto(shape); + const engine = Object.hasOwn(shape, 'aliasEngine') + ? shape.aliasEngine! + : (shape.aliasEngine = new AliasEngine({ parent: parent.aliasEngine, logger: Tempo.#dbg })); engine.clear('event'); engine.registerEvents(events); @@ -195,8 +197,10 @@ export class Tempo { return; // no local change needed // Use the correct alias engine: static for global, instance for local - const proto = Object.getPrototypeOf(shape); - const engine = shape.aliasEngine ??= new AliasEngine({ parent: proto.aliasEngine, logger: Tempo.#dbg }); + const parent = proto(shape); + const engine = Object.hasOwn(shape, 'aliasEngine') + ? shape.aliasEngine! + : (shape.aliasEngine = new AliasEngine({ parent: parent.aliasEngine, logger: Tempo.#dbg })); engine.clear('period'); engine.registerPeriods(periods); From a22e7c71dc2caddc9618a5b23379027c0fe77979 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Sun, 3 May 2026 20:05:21 +1000 Subject: [PATCH 6/8] hasOwn --- packages/tempo/src/tempo.class.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index f2eeb25..6ba4a1a 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -148,7 +148,7 @@ export class Tempo { // Use the correct alias engine: static for global, instance for local, and assign parentEngine for locals const parent = proto(shape); - const engine = Object.hasOwn(shape, 'aliasEngine') + const engine = hasOwn(shape, 'aliasEngine') ? shape.aliasEngine! : (shape.aliasEngine = new AliasEngine({ parent: parent.aliasEngine, logger: Tempo.#dbg })); engine.clear('event'); @@ -198,7 +198,7 @@ export class Tempo { // Use the correct alias engine: static for global, instance for local const parent = proto(shape); - const engine = Object.hasOwn(shape, 'aliasEngine') + const engine = hasOwn(shape, 'aliasEngine') ? shape.aliasEngine! : (shape.aliasEngine = new AliasEngine({ parent: parent.aliasEngine, logger: Tempo.#dbg })); engine.clear('period'); From fed3f71f2a9d037477359576a97c0e566f340a50 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Sun, 3 May 2026 20:09:51 +1000 Subject: [PATCH 7/8] all hasOwn --- packages/tempo/src/discrete/discrete.format.ts | 3 ++- packages/tempo/src/plugin/plugin.util.ts | 3 ++- packages/tempo/src/support/tempo.register.ts | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/tempo/src/discrete/discrete.format.ts b/packages/tempo/src/discrete/discrete.format.ts index 21a145c..703aaac 100644 --- a/packages/tempo/src/discrete/discrete.format.ts +++ b/packages/tempo/src/discrete/discrete.format.ts @@ -7,6 +7,7 @@ import { delegator } from '#library/proxy.library.js'; import { isTempo, enums, Match, getRuntime, NumericPattern } from '#tempo/support'; import { defineInterpreterModule } from '../plugin/plugin.util.js'; import type { Tempo } from '../tempo.class.js'; +import { hasOwn } from '#tempo/support/tempo.util.js'; declare module '../tempo.class.js' { interface Tempo { @@ -65,7 +66,7 @@ export function format(obj?: Temporal.ZonedDateTime | any, fmt?: string | symbol if (!isZonedDateTime(zdt)) return ''; - let template = (isString(fmt) && formats && (typeof (formats as any).has === 'function' ? (formats as any).has(fmt as string) : Object.prototype.hasOwnProperty.call(formats, fmt as string))) + let template = (isString(fmt) && formats && (typeof (formats as any).has === 'function' ? (formats as any).has(fmt as string) : hasOwn(formats, fmt as string))) ? (formats as Record)[fmt as string] : String(fmt); diff --git a/packages/tempo/src/plugin/plugin.util.ts b/packages/tempo/src/plugin/plugin.util.ts index 8148902..8302393 100644 --- a/packages/tempo/src/plugin/plugin.util.ts +++ b/packages/tempo/src/plugin/plugin.util.ts @@ -2,6 +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 type { Tempo } from '../tempo.class.js'; import type { Plugin } from './plugin.type.js'; @@ -94,7 +95,7 @@ export const defineModule = (module: T): T => { */ export function attachStatics(TempoClass: any, props: Record) { for (const [key, val] of Object.entries(props)) { - if (Object.hasOwn(TempoClass, key)) { + if (hasOwn(TempoClass, key)) { const msg = `Static name collision on "${key}". Property is already defined on the host class.`; if (isFunction(TempoClass[sym.$logError])) { // use catch:true to report the collision without a fatal throw (supports re-extension in shared environments) diff --git a/packages/tempo/src/support/tempo.register.ts b/packages/tempo/src/support/tempo.register.ts index cb3113c..1bf410d 100644 --- a/packages/tempo/src/support/tempo.register.ts +++ b/packages/tempo/src/support/tempo.register.ts @@ -7,7 +7,7 @@ import { sym } from './tempo.symbol.js'; import type { Property } from '#library/type.library.js'; import { getRuntime } from './tempo.runtime.js'; -import { setProperty } from './tempo.util.js'; +import { hasOwn, setProperty } from './tempo.util.js'; // Import the live enums and their mutable state from the enum module import { STATE, REGISTRIES, DEFAULTS } from './tempo.enum.js'; @@ -50,7 +50,7 @@ export function registryReset() { Object.defineProperty(obj, key, desc); } else { // For non-extensible objects, only update if property exists - if (Object.prototype.hasOwnProperty.call(obj, key)) { + if (hasOwn(obj, key)) { Object.defineProperty(obj, key, desc); } else { console.warn(`[tempo] registryReset: Cannot define property '${String(key)}' on non-extensible object (property does not exist)`, obj); From ef10d0812890d19585a5e8de8f5531da6d36f2d5 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Sun, 3 May 2026 20:35:41 +1000 Subject: [PATCH 8/8] ci.yml --- .github/workflows/ci.yml | 46 +------------------ .../tempo/src/discrete/discrete.format.ts | 4 +- .../test/core/alias-engine-protochain.test.ts | 19 +++++++- 3 files changed, 20 insertions(+), 49 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a84ed0..edc9095 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,28 +40,6 @@ jobs: timeout-minutes: 30 if: github.ref == 'refs/heads/release-c-layout-order-planner' || github.event.pull_request.base.ref == 'main' steps: - - name: Print GitHub event context (for debug) - run: | - echo "GITHUB_REF: $GITHUB_REF" - echo "GITHUB_EVENT_NAME: $GITHUB_EVENT_NAME" - echo "GITHUB_HEAD_REF: $GITHUB_HEAD_REF" - echo "GITHUB_BASE_REF: $GITHUB_BASE_REF" - echo "github.ref: ${{ github.ref }}" - echo "github.event_name: ${{ github.event_name }}" - echo "github.head_ref: ${{ github.head_ref }}" - echo "github.base_ref: ${{ github.base_ref }}" - echo "github.event.pull_request.base.ref: ${{ github.event.pull_request.base.ref }}" - echo "github.sha: ${{ github.sha }}" - echo "github.repository: ${{ github.repository }}" - echo "github.actor: ${{ github.actor }}" - echo "github.workflow: ${{ github.workflow }}" - echo "github.run_id: ${{ github.run_id }}" - echo "github.run_number: ${{ github.run_number }}" - echo "github.job: ${{ github.job }}" - echo "github.ref_name: ${{ github.ref_name }}" - echo "github.ref_type: ${{ github.ref_type }}" - echo "github.event: $(cat $GITHUB_EVENT_PATH)" - shell: bash - uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 @@ -91,26 +69,4 @@ jobs: 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 }} - - 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/packages/tempo/src/discrete/discrete.format.ts b/packages/tempo/src/discrete/discrete.format.ts index 703aaac..a2023ad 100644 --- a/packages/tempo/src/discrete/discrete.format.ts +++ b/packages/tempo/src/discrete/discrete.format.ts @@ -7,7 +7,7 @@ import { delegator } from '#library/proxy.library.js'; import { isTempo, enums, Match, getRuntime, NumericPattern } from '#tempo/support'; import { defineInterpreterModule } from '../plugin/plugin.util.js'; import type { Tempo } from '../tempo.class.js'; -import { hasOwn } from '#tempo/support/tempo.util.js'; + declare module '../tempo.class.js' { interface Tempo { @@ -66,7 +66,7 @@ export function format(obj?: Temporal.ZonedDateTime | any, fmt?: string | symbol if (!isZonedDateTime(zdt)) return ''; - let template = (isString(fmt) && formats && (typeof (formats as any).has === 'function' ? (formats as any).has(fmt as string) : hasOwn(formats, fmt as string))) + let template = (isString(fmt) && formats && (fmt as string in formats)) ? (formats as Record)[fmt as string] : String(fmt); diff --git a/packages/tempo/test/core/alias-engine-protochain.test.ts b/packages/tempo/test/core/alias-engine-protochain.test.ts index 1cf32a5..28bb4fa 100644 --- a/packages/tempo/test/core/alias-engine-protochain.test.ts +++ b/packages/tempo/test/core/alias-engine-protochain.test.ts @@ -1,8 +1,16 @@ import { AliasEngine } from '#tempo/engine/engine.alias.js'; import { Logify } from '#library/logify.class.js'; +import { vi } from 'vitest'; describe('AliasEngine prototype chain (Global → Sandbox → Instance)', () => { - const logger = new Logify({ debug: true }); + const logger = { + warn: vi.fn(), + debug: vi.fn(), + error: vi.fn(), + log: vi.fn(), + info: vi.fn(), + trace: vi.fn() + } as unknown as Logify; // Simulate a global state const globalShape = {} as { aliasEngine: AliasEngine }; @@ -38,11 +46,18 @@ describe('AliasEngine prototype chain (Global → Sandbox → Instance)', () => }); it('collision detection traverses the prototype chain', () => { + (logger.warn as any).mockClear(); + // Register a colliding alias in local localShape.aliasEngine.registerEventAlias('globalEvt', 'localShadow'); + // Should warn about collision with global - // (You may want to spy on logger.warn for a real assertion) + expect(logger.warn).toHaveBeenCalled(); + expect((logger.warn as any).mock.calls[0][0]).toMatch(/Collision detected/i); + expect(localShape.aliasEngine.resolveEventAlias('globalEvt')).toBe('localShadow'); expect(globalShape.aliasEngine.resolveEventAlias('globalEvt')).toBe('globalValue'); + + (logger.warn as any).mockReset(); }); });