Skip to content

Feature/tempo pattern compiler#28

Open
magmacomputing wants to merge 14 commits intomainfrom
feature/tempo-pattern-compiler
Open

Feature/tempo pattern compiler#28
magmacomputing wants to merge 14 commits intomainfrom
feature/tempo-pattern-compiler

Conversation

@magmacomputing
Copy link
Copy Markdown
Owner

@magmacomputing magmacomputing commented May 7, 2026

Guard Builder isolation

Summary by CodeRabbit

  • New Features
    • Improved pattern compilation, master guard validation, and layout classification for more reliable parsing
  • Bug Fixes
    • Fixed utility crashes, compiler state isolation, and regex/safety edge cases
  • Documentation
    • Added main-branch protection guide; clarified Node.js Temporal installation; new prototyping/regexp docs; updated changelog
  • Tests
    • Added tests for guard behavior and pattern-compiler robustness
  • Chores
    • Bumped versions to 2.9.2 and simplified CI by removing benchmark/prefilter job

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Warning

Rate limit exceeded

@magmacomputing has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 33 minutes and 12 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 56197fe3-3a19-48f9-9c3b-2463186e6d1a

📥 Commits

Reviewing files that changed from the base of the PR and between 8099d82 and b75a209.

📒 Files selected for processing (10)
  • doc/main_branch_protection.md
  • packages/library/src/common/temporal.library.ts
  • packages/library/test/common/temporal_library.test.ts
  • packages/tempo/CHANGELOG.md
  • packages/tempo/index.md
  • packages/tempo/plan/RELEASE-D.md
  • packages/tempo/src/engine/engine.composer.ts
  • packages/tempo/src/engine/engine.normalizer.ts
  • packages/tempo/src/engine/engine.pattern.ts
  • packages/tempo/src/tempo.class.ts
📝 Walkthrough

Walkthrough

This PR bumps versions to 2.9.2 and restructures Tempo internals: extracts pattern compilation into a per-state PatternCompiler, extracts guard logic into createMasterGuard, migrates many support modules from tempo.*support.*, moves layout/normalizer responsibilities into engine modules, updates wiring/tests/docs, and removes a CI prefilter job.

Changes

Engine & Support Extraction

Layer / File(s) Summary
Data Shape / Types
packages/tempo/src/tempo.type.ts, packages/tempo/package.json
Adds optional patternCompiler?: PatternCompiler to Internal.State; package exports for ./enums now point at support.enum artifacts; package versions bumped to 2.9.2.
Core Implementation: Pattern & Guard
packages/tempo/src/engine/engine.pattern.ts, packages/tempo/src/engine/engine.guard.ts
Adds PatternCompiler (compileRegExp, setPatterns, per-state cache) and exported setPatterns(state) wrapper; adds MasterGuard interface and createMasterGuard(words) greedy left-to-right scanner.
Core Implementation: Normalizer & Composer
packages/tempo/src/engine/engine.normalizer.ts, packages/tempo/src/engine/engine.composer.ts
Introduces normalization pipeline (normalizeMatch, resolveAliases, accumulateResult) and refactors composer to explicit dateTime initialization and timezone extraction via getTemporalIds.
Layout Resolution / Migration
packages/tempo/src/engine/engine.layout.ts, packages/tempo/src/parse/parse.resolver.ts
Moves/resolves layout classification/order handling into engine.layout; controller type generalized to allow `(string
Support Module Consolidation & Utilities
packages/tempo/src/support/* (support.util.ts, support.init.ts, support.register.ts, support.index.ts, support.enum.ts, support.symbol.ts, support.runtime.ts, support.default.ts)
Rewires many imports from tempo.*support.*; introduces additional support symbols; moves compileRegExp/setPatterns out of util; adds helpers (isProxy, hasOwn, proto) and changes create() to log and return sanitized object instead of throwing on prototype issues; sanitizes suspicious snippet regexes by logging and escaping.
Tempo Class Wiring
packages/tempo/src/tempo.class.ts
Rewires to use createMasterGuard for Tempo.#buildGuard, delegates Tempo.regexp to per-state PatternCompiler, updates tz/cal extraction via getTemporalIds, and removes inline allowed-token logic.
Discrete Parse / Parse Engine
packages/tempo/src/discrete/discrete.parse.ts, packages/tempo/src/parse/*
Refactors parse pipeline to use normalizeMatch and accumulateResult, updates basis/today tz/cal computation to getTemporalIds, and wires ParseEngine.result to accumulateResult.
Alias Engine & Terms
packages/tempo/src/engine/engine.alias.ts, packages/tempo/src/engine/engine.term.ts
AliasEngine constructor no longer throws on invalid parent (logs and falls back to root); term mutation/resolution derives tz/cal via getTemporalIds.
Module Imports & Build Config
packages/tempo/vitest.config.ts, packages/tempo/src/tsconfig.json, various files under packages/tempo/src/** and tests
Adds vite/vitest alias entries for #tempo/engine and plugin patterns; removes #tempo/parse TS path; updates ~30+ files to import #tempo/support/support.* and new engine modules.
Tests
packages/tempo/test/engine/engine.guard.test.ts, packages/tempo/test/engine/pattern_compiler_optimization.test.ts, packages/tempo/test/engine/parse.layout.test.ts, others
Adds tests for MasterGuard and PatternCompiler behaviors; updates parse.layout tests to import engine.layout; minor test wiring changes (runtime import paths, console spy formatting).
Docs, Changelog, CI, Misc
packages/tempo/CHANGELOG.md, packages/tempo/doc/*, packages/tempo/index.md, .agent/workflows/interactive-testing.md, .github/workflows/ci.yml, doc/main_branch_protection.md, packages/tempo/plan/RELEASE-D.md
Adds changelog and docs updates (architecture, installation/Temporal guidance, Tempo.regexp doc), updates REPL docs to use ./bin/repl.ts, hardcodes some CSS colors, removes CI test-parse-prefilter job, and adds local main-branch protection docs.

Sequence Diagram

sequenceDiagram
    participant User as User Code
    participant Tempo as Tempo Class
    participant State as Internal State
    participant Pattern as PatternCompiler
    participant Guard as createMasterGuard
    participant Engine as Engine Modules

    User->>Tempo: new Tempo(...)
    Tempo->>Tempo: `#buildGuard`()
    Tempo->>Guard: createMasterGuard(wordsList)
    Guard-->>Tempo: MasterGuard.test()

    User->>Tempo: Tempo.regexp(layout)
    Tempo->>State: get state.patternCompiler
    alt not initialized
        Tempo->>Pattern: new PatternCompiler(state)
        Tempo->>State: store patternCompiler
    end
    Tempo->>Pattern: compileRegExp(layout)
    Pattern->>Engine: resolve placeholders / layout
    Pattern-->>Tempo: anchored RegExp

    User->>Tempo: parse(input)
    Tempo->>Guard: test(input)
    Guard-->>Tempo: boolean
    alt allowed
        Tempo->>Engine: normalizeMatch / parse pipeline
        Engine-->>Tempo: DateTime result
    else denied
        Tempo-->>User: reject / defer
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Patterns stitched in morning light,
Guards stand watch through day and night,
Support modules hopped in line,
Engines hum, regexes shine,
v2.9.2 — a joyful design.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/tempo pattern compiler' partially describes the changeset, focusing on pattern compilation infrastructure while the broader work involves guard builder isolation, engine refactoring, support module reorganization, and documentation updates.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/tempo-pattern-compiler

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/tempo/src/engine/engine.alias.ts (1)

83-103: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Invalid parent is logged but still retained — downstream methods will crash.

The constructor sets this.#parent = options.parent ?? null before the instanceof AliasEngine check, and the new else branch only logs when parent is truthy but not an AliasEngine. The invalid reference is still kept on this.#parent, so later code that gates on if (this.#parent) { this.#parent.getPatterns(...) } (line 172–175), this.#parent?.getVersion() (line 181), this.#parent?.getAlias(key) (line 219), and (this.#parent as any).#words (line 247) will throw TypeErrors the next time aliases are registered/resolved. Previously this surfaced as an explicit TypeError at construction; now it only surfaces deep inside parsing.

Either restore the strict throw or null out the parent in the invalid branch so root semantics are actually root semantics.

🛡️ Proposed fix — null out invalid parent so root paths stay root
 		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");
-
+			if (this.#parent) {
+				this.#logger?.error(this.#config, "Parent engine must be an instance of AliasEngine");
+				this.#parent = null;	// avoid retaining an invalid reference that downstream methods would dereference
+			}
 			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)
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tempo/src/engine/engine.alias.ts` around lines 83 - 103, In the
AliasEngine constructor, don't keep an invalid options.parent on this.#parent;
instead test options.parent first and only assign when it's an instance of
AliasEngine, otherwise log the error and set this.#parent = null so downstream
checks (e.g. calls to getPatterns, getVersion, getAlias or accessing `#words`)
behave like a root engine; reposition the instanceof AliasEngine check to
control assignment to this.#parent and then initialize this.#depth, this.#state
and this.#words accordingly (root vs child) so invalid parents are nulled rather
than retained.
🧹 Nitpick comments (1)
packages/tempo/test/engine/engine.guard.test.ts (1)

1-62: ⚡ Quick win

LGTM — test suite covers the primary guard behaviors well.

One coverage gap worth noting: there is no test for a whitespace-only input (e.g. guard.test(' ')). The current implementation would return true for such input (the loop exits cleanly after consuming only spaces), which may not be the intended contract. A test asserting false would either catch this or explicitly document the intended behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tempo/test/engine/engine.guard.test.ts` around lines 1 - 62, Add a
test asserting that a string consisting only of whitespace is treated as
invalid: call createMasterGuard(...) to produce a guard and add an expectation
like expect(guard.test('   ')).toBe(false) (or add a new it block alongside the
existing "should fail on empty or non-string input" case) to ensure guard.test
handles whitespace-only input; locate usage of createMasterGuard and guard.test
in engine.guard.test.ts to insert the new assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@doc/main_branch_protection.md`:
- Around line 127-129: The docs currently recommend using "git commit -am" which
omits new/untracked files; update the recovery flow to stage all changes first
by advising to run "git add ." or "git add -A" before the commit step (replace
or augment the "git commit -am" guidance), and mention the rationale briefly so
users know new files will be included; reference the git commit -am step and the
new recommended git add commands so readers can locate and apply the change.
- Around line 139-143: Update the README step before the destructive command
"git reset --hard origin/main" to add an explicit safety check: instruct users
to verify their working tree is clean (for example by running a status/porcelain
check or confirming there are no uncommitted changes) and to stash or commit any
work before proceeding; include a short warning sentence explaining that the
reset will discard local changes and recommend alternatives (stash/commit) if
the check fails so users don't lose work.

In `@packages/tempo/doc/tempo.layout.md`:
- Around line 88-94: The illustrative console output currently shows sequential
duplicate named groups for the `{sep}` token which is invalid; update the
example output produced by Tempo.regexp('{dd}{sep}{mm}{sep}{yy}') to reflect
Tempo's actual behavior (e.g., deduplicating or renaming groups so names are
unique within the same branch such as `(?<sep>...)(?<sep1>...)` or showing how
Tempo merges repeated tokens) and ensure the comment no longer displays
`(?<sep>...)(?<sep>...)`; reference the Tempo.regexp usage and the format string
'{dd}{sep}{mm}{sep}{yy}' when updating the illustrative output.

In `@packages/tempo/index.md`:
- Around line 558-565: The .tempo-btn-brand and .tempo-btn-brand:hover rules
hardcode light blue colors that fail WCAG contrast; replace them with either (a)
a darker background color that yields at least 4.5:1 contrast against white
(update the values in .tempo-btn-brand and .tempo-btn-brand:hover) or (b) revert
to using the existing theme variables like .tempo-btn-alt does so styling is
consistent and accessible; ensure the new colors are tested to meet 4.5:1 and
keep .tempo-btn-alt’s variable-based approach as the reference for consistency.

In `@packages/tempo/src/engine/engine.guard.ts`:
- Around line 30-31: The test(input: string): boolean in engine.guard.ts
currently returns true for inputs that are only whitespace because it only
checks !input and then advances over spaces; fix by adding a boolean flag (e.g.,
sawNonWhitespace or matchedToken) inside test to track whether any
non-whitespace/token character was actually consumed/matched before returning
true, set it when any token-accepting branch runs (not when skipping spaces),
and use that flag in the final return so pure-whitespace inputs return false;
apply the same change to the other test occurrence referenced (the second test
method around the other guard).
- Around line 18-21: The map step building wordsList can crash when a Symbol has
no description because (isSymbol(w) ? w.description : ... ) may be undefined
before .toLowerCase() — update the logic in the wordsList creation (the chain
around isSymbol, isString, and .map) to guard against undefined descriptions:
either filter out symbols with no description before mapping or coerce undefined
to an empty string/default (e.g., skip or return ''), and then call
.toLowerCase(); specifically change the sequence around wordsList (the
filter(...).map(...) chain) so that any Symbol without a description is excluded
or handled before calling .toLowerCase() to avoid the TypeError.

In `@packages/tempo/src/tempo.class.ts`:
- Around line 829-833: The current use of the nullish-assignment on
state.patternCompiler may reuse a PatternCompiler instance from a
prototype/parent state; change the logic in the accessor where you obtain state
via (this as any)[$Internal]() so that you either always construct a new
PatternCompiler bound to this state or recreate it when an existing
state.patternCompiler is present but its bound state !== state; ensure the
stored field (state.patternCompiler) is set to new PatternCompiler({ state }) in
that case and then call state.patternCompiler.compileRegExp(layout, snippet) so
PatternCompiler ownership and cache remain isolated per-state.

---

Outside diff comments:
In `@packages/tempo/src/engine/engine.alias.ts`:
- Around line 83-103: In the AliasEngine constructor, don't keep an invalid
options.parent on this.#parent; instead test options.parent first and only
assign when it's an instance of AliasEngine, otherwise log the error and set
this.#parent = null so downstream checks (e.g. calls to getPatterns, getVersion,
getAlias or accessing `#words`) behave like a root engine; reposition the
instanceof AliasEngine check to control assignment to this.#parent and then
initialize this.#depth, this.#state and this.#words accordingly (root vs child)
so invalid parents are nulled rather than retained.

---

Nitpick comments:
In `@packages/tempo/test/engine/engine.guard.test.ts`:
- Around line 1-62: Add a test asserting that a string consisting only of
whitespace is treated as invalid: call createMasterGuard(...) to produce a guard
and add an expectation like expect(guard.test('   ')).toBe(false) (or add a new
it block alongside the existing "should fail on empty or non-string input" case)
to ensure guard.test handles whitespace-only input; locate usage of
createMasterGuard and guard.test in engine.guard.test.ts to insert the new
assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3508f842-c96f-4325-aa34-907a59a46647

📥 Commits

Reviewing files that changed from the base of the PR and between e66d9aa and f2f2a86.

📒 Files selected for processing (40)
  • .agent/workflows/interactive-testing.md
  • .github/workflows/ci.yml
  • doc/main_branch_protection.md
  • package.json
  • packages/library/package.json
  • packages/tempo/CHANGELOG.md
  • packages/tempo/doc/architecture.md
  • packages/tempo/doc/installation.md
  • packages/tempo/doc/tempo.layout.md
  • packages/tempo/index.md
  • packages/tempo/package.json
  • packages/tempo/plan/RELEASE-D.md
  • packages/tempo/src/discrete/discrete.parse.ts
  • packages/tempo/src/engine/engine.alias.ts
  • packages/tempo/src/engine/engine.guard.ts
  • packages/tempo/src/engine/engine.layout.ts
  • packages/tempo/src/engine/engine.pattern.ts
  • packages/tempo/src/engine/engine.resolver.ts
  • packages/tempo/src/parse/parse.layout.ts
  • packages/tempo/src/parse/parse.resolver.ts
  • packages/tempo/src/plugin/plugin.util.ts
  • packages/tempo/src/plugin/term/term.quarter.ts
  • packages/tempo/src/plugin/term/term.season.ts
  • packages/tempo/src/support/support.default.ts
  • packages/tempo/src/support/support.enum.ts
  • packages/tempo/src/support/support.index.ts
  • packages/tempo/src/support/support.init.ts
  • packages/tempo/src/support/support.intl.ts
  • packages/tempo/src/support/support.register.ts
  • packages/tempo/src/support/support.runtime.ts
  • packages/tempo/src/support/support.symbol.ts
  • packages/tempo/src/support/support.util.ts
  • packages/tempo/src/tempo.class.ts
  • packages/tempo/src/tempo.type.ts
  • packages/tempo/src/tsconfig.json
  • packages/tempo/test/engine/engine.guard.test.ts
  • packages/tempo/test/engine/parse.layout.test.ts
  • packages/tempo/test/engine/pattern_compiler_optimization.test.ts
  • packages/tempo/test/plugins/plugin_registration.test.ts
  • packages/tempo/vitest.config.ts
💤 Files with no reviewable changes (5)
  • packages/tempo/src/parse/parse.layout.ts
  • packages/tempo/src/tsconfig.json
  • packages/tempo/src/parse/parse.resolver.ts
  • packages/tempo/vitest.config.ts
  • .github/workflows/ci.yml

Comment thread doc/main_branch_protection.md
Comment thread doc/main_branch_protection.md
Comment thread packages/tempo/doc/tempo.layout.md
Comment thread packages/tempo/index.md
Comment thread packages/tempo/src/engine/engine.guard.ts
Comment thread packages/tempo/src/engine/engine.guard.ts
Comment thread packages/tempo/src/tempo.class.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (8)
packages/tempo/src/tempo.class.ts (1)

1287-1288: 💤 Low value

Minor: tz and cal getters duplicate work.

Each getter independently calls this.toDateTime() and getTemporalIds(...). When both are read in succession (a common pattern, e.g. in toNow or duration paths), this resolves the ZonedDateTime twice and re-extracts ids twice. If the call sites are hot, consider memoizing per-instance or exposing a single #ids() helper. Low priority — toDateTime() is already memoized via #resolve, so the cost is just the ID extraction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tempo/src/tempo.class.ts` around lines 1287 - 1288, tz and cal both
call this.toDateTime() and getTemporalIds(...) causing duplicated work; add a
single private helper (e.g., a method named `#ids`() or a lazy memoized pair like
this.#tz and this.#cal) that calls getTemporalIds(this.toDateTime()) once and
returns the tuple, then change the getters tz and cal to return the first and
second elements from that helper; ensure the helper is private to the Tempo
class and lazily computes/stores the ids so subsequent tz/cal accesses reuse the
result.
packages/tempo/src/engine/engine.composer.ts (1)

24-24: 💤 Low value

Defensive dateTime ?? today fallback is now unreachable — consider tightening typing/intent.

With let dateTime: Temporal.ZonedDateTime = today; on Line 24 and every default branch returning early (Lines 152, and Line 46 for the string failure path), dateTime is always assigned by the time control reaches Line 156, so dateTime ?? today is dead. Either drop the ?? (since the type is now non-optional) or remove the explicit today initializer to keep the fallback meaningful — the current pairing is contradictory.

Suggested cleanup
-	return { dateTime: dateTime ?? today, timeZone };
+	return { dateTime, timeZone };

Also applies to: 156-156

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tempo/src/engine/engine.composer.ts` at line 24, The variable
dateTime is declared as a non-optional ZonedDateTime and initialized to today,
making the later defensive "dateTime ?? today" check unreachable; either remove
the initializer (let dateTime: Temporal.ZonedDateTime | undefined) so the
nullish fallback remains meaningful, or keep the non-optional type and
initializer and remove the "?? today" at the use site. Locate the declaration of
dateTime (let dateTime: Temporal.ZonedDateTime = today;) and the later usage
with "dateTime ?? today" and change one of them so the type and fallback
behavior are consistent (adjust the type to allow undefined or drop the nullish
operator).
packages/tempo/plan/RELEASE-D.md (1)

129-173: 💤 Low value

Detailed Outlines for Guard Builder/Normalizer still read as "Assessment Outline" despite checklist showing completed extraction.

The checklist (Lines 24-35) marks both Guard Builder Extraction and Parse Result Normalizer Extraction as completed (with concrete extractions to engine.guard.ts and engine.normalizer.ts), but the corresponding detailed outlines below (Lines 129-173) are still phrased as evaluation/assessment of feasibility ("Evaluate the value and feasibility...", "Assessment Steps", "If justified, outline module boundaries..."). Worth updating these sections to reflect the realized design (modules, exported APIs like createMasterGuard, normalizeMatch, accumulateResult) so the plan stays consistent with the implemented state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tempo/plan/RELEASE-D.md` around lines 129 - 173, Update the "Guard
Builder Extraction" and "Parse Result Normalizer Extraction" sections to reflect
that extraction is complete: replace assessment language with concrete design
and migration details, mention the new modules (engine.guard.ts and
engine.normalizer.ts) and their exported APIs such as createMasterGuard,
normalizeMatch, and accumulateResult, describe how they integrate with
tempo.class.ts's parse pipeline and pattern system, and remove/replace
"Assessment Steps" with brief notes on module responsibilities, public function
signatures, and any migration/usage examples so the document matches the
implemented state.
packages/tempo/src/engine/engine.normalizer.ts (3)

88-178: 💤 Low value

prevAnchor is shadowed inside the alias loop — high risk of future regressions.

Line 88 captures the caller's anchor; line 91 overwrites state.anchor to the current dateTime. Inside the alias-resolution branch, line 158 re-declares const prevAnchor: any = state.anchor; to snapshot the current anchor before subParse, then line 161 restores it. The outer finally at line 172 then restores the original outer prevAnchor. Functionally correct today because they are in separate scopes, but the identical name across nested scopes is a maintenance hazard — a stray edit promoting either declaration (or fusing the blocks) silently destroys the save/restore protocol.

Rename the inner one (e.g. innerPrevAnchor or subAnchor) to make the layered restore explicit.

♻️ Disambiguate
-					const prevAnchor: any = state.anchor;
-					state.anchor = dateTime;
-					const resMatch = subParse(res.value, dateTime, resolving);
-					state.anchor = prevAnchor;
+					const subPrevAnchor: any = state.anchor;
+					state.anchor = dateTime;
+					const resMatch = subParse(res.value, dateTime, resolving);
+					state.anchor = subPrevAnchor;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tempo/src/engine/engine.normalizer.ts` around lines 88 - 178, The
inner snapshot of state.anchor inside the alias-resolution branch shadows the
outer prevAnchor; rename the inner const prevAnchor (the one declared before
calling subParse) to a distinct identifier like innerPrevAnchor or subAnchor and
update its corresponding restore (state.anchor = innerPrevAnchor) so the outer
prevAnchor used in the outer finally block (and the outer save/restore logic
around state.anchor/state.zdt) remains untouched; locate the declaration and
restore around subParse in the alias handling code path (the block that sets
state.anchor = dateTime, calls subParse, then restores state.anchor) and rename
the local variable consistently.

127-133: 💤 Low value

Magic recursion limit > 50.

The hard-coded 50 mirrors no other constant in this file and is unrelated to the depth limit (> 10) used inside PatternCompiler.matcher. Hoist it to a named constant and document why this ceiling was chosen, so future tuning (or test injection) doesn't have to grep for 50.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tempo/src/engine/engine.normalizer.ts` around lines 127 - 133,
Replace the magic number 50 used in the recursion guard inside the normalization
loop with a well-named constant (e.g., MAX_TEMPO_RESOLVE_DEPTH) declared at the
top of the module and add a short comment explaining why this ceiling was chosen
and how it relates to the PatternCompiler.matcher depth limit (~10); update the
check from "resolvingKeys.size > 50" to use that constant, leaving the rest of
the logic (setting state.errored, calling (TempoClass as any)[sym.$logError],
deleting groups[key], and continue) unchanged so behavior is identical but the
limit is discoverable and configurable.

40-41: 💤 Low value

Redundant getTemporalIds evaluations in tz/cal getters.

Each property access recomputes the full normalization. cal even passes state.config.timeZone only to discard [0]. Trivial to consolidate with toNow above.

♻️ Compute once
-		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] },
+		toNow: () => {
+			const [tz, cal] = getTemporalIds(state.config.timeZone, state.config.calendar);
+			return instant().toZonedDateTimeISO(tz).withCalendar(cal);
+		},
+		get tz() { return getTemporalIds(state.config.timeZone, state.config.calendar)[0] },
+		get cal() { return getTemporalIds(state.config.timeZone, state.config.calendar)[1] },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tempo/src/engine/engine.normalizer.ts` around lines 40 - 41, The tz
and cal getters call getTemporalIds separately (and cal calls it with only
timeZone), causing duplicate work; compute getTemporalIds once (e.g., call
getTemporalIds(state.config.timeZone, state.config.calendar) once above or
alongside toNow and store the tuple/values) and have the tz getter return the
first element and cal return the second, removing the redundant per-getter calls
and the incorrect single-argument call in cal.
packages/library/src/common/temporal.library.ts (1)

86-89: ⚡ Quick win

toZonedDateTime has a design flaw with string inputs that should be fixed to prevent future issues.

The function accepts ISO strings in its signature but blindly appends [${tz}] without checking if the input already contains a zone designator. Passing '2024-01-01T00:00:00-05:00[America/New_York]' would create '2024-01-01T00:00:00-05:00[America/New_York][UTC]' (invalid per Temporal spec), and '2024-01-01T00:00Z' with a different tz parameter would produce '2024-01-01T00:00Z[UTC]' (which silently succeeds only if the tz matches Z).

Current code avoids this by passing property bags only (not strings), but since this is a reachable public API, adding a guard is sensible to prevent misuse:

Suggested fix
 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)) {
+		const hasZoneBracket = /\[[^\]]+\]\s*$/.test(bag);
+		return Temporal.ZonedDateTime.from(hasZoneBracket ? bag : `${bag}[${String(tz)}]`);
+	}
 	return Temporal.ZonedDateTime.from(bag);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/library/src/common/temporal.library.ts` around lines 86 - 89, The
toZonedDateTime function currently appends `[${tz}]` to any string input which
can produce invalid double-zone strings; update toZonedDateTime to first detect
whether the string already includes a zone designator (either an offset like
±HH:MM or a bracketed IANA zone like [...]) and only append `[${tz}]` when no
zone is present, otherwise pass the original string through to
Temporal.ZonedDateTime.from; keep the existing behavior for non-string bags. Use
a simple regex test on the string input inside toZonedDateTime to decide which
branch to call Temporal.ZonedDateTime.from on.
packages/tempo/src/engine/engine.pattern.ts (1)

79-83: 💤 Low value

Add defensive placeholder name sanitization to prevent invalid named-capture identifiers.

The current code at lines 79-83 only escapes dots (name.replace(/\./g, '_')), but JS named capture groups require identifiers matching [A-Za-z_$][A-Za-z0-9_$]*. If a placeholder name starts with a digit (e.g., {123ext}) or contains other special characters, new RegExp() will throw at line 93—but the error is silently caught and falls back to escaped literal matching, masking the real layout/snippet authoring bug.

🛡️ Defensive sanitization
-			if (res && name.includes('.')) {										// wrap dotted extensions for identification
-				const safeName = name.replace(/\./g, '_');
-				if (!res.startsWith(`(?<${safeName}>`))
-					res = `(?<${safeName}>${res})`;
-			}
+			if (res && name.includes('.')) {										// wrap dotted extensions for identification
+				const safeName = name.replace(/[^A-Za-z0-9_$]/g, '_');
+				if (/^[A-Za-z_$]/.test(safeName) && !res.startsWith(`(?<${safeName}>`))
+					res = `(?<${safeName}>${res})`;
+			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tempo/src/engine/engine.pattern.ts` around lines 79 - 83, The
placeholder sanitization only replaces dots but must produce a valid JS
named-capture identifier; update the logic that builds safeName (the code that
currently does const safeName = name.replace(/\./g, '_')) to: trim/ensure
non-empty, replace any character not in [A-Za-z0-9_$] with '_', and if the first
character is not [A-Za-z_$] prefix an underscore (or replace it) so the final
safeName matches [A-Za-z_$][A-Za-z0-9_$]* before it is interpolated into the
group `(?<${safeName}>${res})`; keep the existing fallback behavior for RegExp
construction (the code that calls new RegExp on res) but ensure we generate a
valid safeName to avoid throwing there and to surface real template issues
instead of silently falling back.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@doc/main_branch_protection.md`:
- Around line 98-101: Remove or relocate the premature "Stage all changes" and
"Commit your work" steps in the introductory section that currently advise
running `git add .` / `git add -A` and `git commit -m "Your descriptive
message"` before creating a branch; these instructions conflict with the
pre-commit hook described in the scenario and already appear correctly in the
branch-first workflows below—either delete those two steps or move them into the
method sections that instruct creating a new branch first so staging/commit
guidance appears only after branch creation.

In `@packages/library/src/common/temporal.library.ts`:
- Around line 115-139: The guard in getTemporalIds wrongfully treats a
ZonedDateTime as a timeZone-like object when a Calendar is passed as the second
argument; add explicit TypeScript overloads for getTemporalIds to support
(tzOrZdt: ZonedDateTime, cal?: Calendar), (tzOrZdt: TimeZoneLike, cal?:
Calendar) and the general fallback to improve type safety, then change the
runtime guard in getTemporalIds (currently using isZonedDateTime(tzOrZdt) &&
isUndefined(cal)) to detect ZonedDateTime regardless of whether a calendar arg
was supplied (e.g., if isZonedDateTime(tzOrZdt) then use its
timeZoneId/calendarId and, if cal is provided, treat that as the explicit
calendar override), and ensure engine.normalizer.ts callers (tz and cal getters)
rely on the typed overloads so swapped-argument mistakes are caught by the
compiler.

In `@packages/tempo/src/engine/engine.normalizer.ts`:
- Around line 192-205: accumulateResult currently builds a fresh match object
each call so the res.includes(match) check is a no-op; either remove that check
to always append, or implement real deduping by comparing stable identity
fields. Update the function accumulateResult to compute a stable identity (e.g.
combine match.match and match.source or compare match.match, match.source,
match.anchor as appropriate) and replace res.includes(match) with a lookup like
res.some(existing => existing.match === match.match && existing.source ===
match.source [&& existing.anchor === match.anchor if needed]) before pushing, or
simply delete the includes branch if deduplication is not required; refer to
symbols: accumulateResult, match, state.parse.result, and state.parse.anchor.

---

Nitpick comments:
In `@packages/library/src/common/temporal.library.ts`:
- Around line 86-89: The toZonedDateTime function currently appends `[${tz}]` to
any string input which can produce invalid double-zone strings; update
toZonedDateTime to first detect whether the string already includes a zone
designator (either an offset like ±HH:MM or a bracketed IANA zone like [...])
and only append `[${tz}]` when no zone is present, otherwise pass the original
string through to Temporal.ZonedDateTime.from; keep the existing behavior for
non-string bags. Use a simple regex test on the string input inside
toZonedDateTime to decide which branch to call Temporal.ZonedDateTime.from on.

In `@packages/tempo/plan/RELEASE-D.md`:
- Around line 129-173: Update the "Guard Builder Extraction" and "Parse Result
Normalizer Extraction" sections to reflect that extraction is complete: replace
assessment language with concrete design and migration details, mention the new
modules (engine.guard.ts and engine.normalizer.ts) and their exported APIs such
as createMasterGuard, normalizeMatch, and accumulateResult, describe how they
integrate with tempo.class.ts's parse pipeline and pattern system, and
remove/replace "Assessment Steps" with brief notes on module responsibilities,
public function signatures, and any migration/usage examples so the document
matches the implemented state.

In `@packages/tempo/src/engine/engine.composer.ts`:
- Line 24: The variable dateTime is declared as a non-optional ZonedDateTime and
initialized to today, making the later defensive "dateTime ?? today" check
unreachable; either remove the initializer (let dateTime: Temporal.ZonedDateTime
| undefined) so the nullish fallback remains meaningful, or keep the
non-optional type and initializer and remove the "?? today" at the use site.
Locate the declaration of dateTime (let dateTime: Temporal.ZonedDateTime =
today;) and the later usage with "dateTime ?? today" and change one of them so
the type and fallback behavior are consistent (adjust the type to allow
undefined or drop the nullish operator).

In `@packages/tempo/src/engine/engine.normalizer.ts`:
- Around line 88-178: The inner snapshot of state.anchor inside the
alias-resolution branch shadows the outer prevAnchor; rename the inner const
prevAnchor (the one declared before calling subParse) to a distinct identifier
like innerPrevAnchor or subAnchor and update its corresponding restore
(state.anchor = innerPrevAnchor) so the outer prevAnchor used in the outer
finally block (and the outer save/restore logic around state.anchor/state.zdt)
remains untouched; locate the declaration and restore around subParse in the
alias handling code path (the block that sets state.anchor = dateTime, calls
subParse, then restores state.anchor) and rename the local variable
consistently.
- Around line 127-133: Replace the magic number 50 used in the recursion guard
inside the normalization loop with a well-named constant (e.g.,
MAX_TEMPO_RESOLVE_DEPTH) declared at the top of the module and add a short
comment explaining why this ceiling was chosen and how it relates to the
PatternCompiler.matcher depth limit (~10); update the check from
"resolvingKeys.size > 50" to use that constant, leaving the rest of the logic
(setting state.errored, calling (TempoClass as any)[sym.$logError], deleting
groups[key], and continue) unchanged so behavior is identical but the limit is
discoverable and configurable.
- Around line 40-41: The tz and cal getters call getTemporalIds separately (and
cal calls it with only timeZone), causing duplicate work; compute getTemporalIds
once (e.g., call getTemporalIds(state.config.timeZone, state.config.calendar)
once above or alongside toNow and store the tuple/values) and have the tz getter
return the first element and cal return the second, removing the redundant
per-getter calls and the incorrect single-argument call in cal.

In `@packages/tempo/src/engine/engine.pattern.ts`:
- Around line 79-83: The placeholder sanitization only replaces dots but must
produce a valid JS named-capture identifier; update the logic that builds
safeName (the code that currently does const safeName = name.replace(/\./g,
'_')) to: trim/ensure non-empty, replace any character not in [A-Za-z0-9_$] with
'_', and if the first character is not [A-Za-z_$] prefix an underscore (or
replace it) so the final safeName matches [A-Za-z_$][A-Za-z0-9_$]* before it is
interpolated into the group `(?<${safeName}>${res})`; keep the existing fallback
behavior for RegExp construction (the code that calls new RegExp on res) but
ensure we generate a valid safeName to avoid throwing there and to surface real
template issues instead of silently falling back.

In `@packages/tempo/src/tempo.class.ts`:
- Around line 1287-1288: tz and cal both call this.toDateTime() and
getTemporalIds(...) causing duplicated work; add a single private helper (e.g.,
a method named `#ids`() or a lazy memoized pair like this.#tz and this.#cal) that
calls getTemporalIds(this.toDateTime()) once and returns the tuple, then change
the getters tz and cal to return the first and second elements from that helper;
ensure the helper is private to the Tempo class and lazily computes/stores the
ids so subsequent tz/cal accesses reuse the result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2ee03d44-ceda-475e-9e94-2bfc6af60d77

📥 Commits

Reviewing files that changed from the base of the PR and between f2f2a86 and 8099d82.

📒 Files selected for processing (19)
  • doc/main_branch_protection.md
  • packages/library/src/common/assertion.library.ts
  • packages/library/src/common/temporal.library.ts
  • packages/tempo/doc/tempo.layout.md
  • packages/tempo/index.md
  • packages/tempo/plan/RELEASE-D.md
  • packages/tempo/src/discrete/discrete.parse.ts
  • packages/tempo/src/engine/engine.alias.ts
  • packages/tempo/src/engine/engine.composer.ts
  • packages/tempo/src/engine/engine.guard.ts
  • packages/tempo/src/engine/engine.normalizer.ts
  • packages/tempo/src/engine/engine.pattern.ts
  • packages/tempo/src/engine/engine.term.ts
  • packages/tempo/src/module/module.duration.ts
  • packages/tempo/src/support/support.default.ts
  • packages/tempo/src/tempo.class.ts
  • packages/tempo/test/core/tempo_guard.test.ts
  • packages/tempo/test/engine/engine.guard.test.ts
  • packages/tempo/test/support/setup.console-spy.ts
✅ Files skipped from review due to trivial changes (4)
  • packages/tempo/src/engine/engine.term.ts
  • packages/tempo/test/support/setup.console-spy.ts
  • packages/tempo/src/engine/engine.guard.ts
  • packages/tempo/src/support/support.default.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/tempo/index.md
  • packages/tempo/test/engine/engine.guard.test.ts
  • packages/tempo/src/engine/engine.alias.ts

Comment thread doc/main_branch_protection.md Outdated
Comment thread packages/library/src/common/temporal.library.ts
Comment thread packages/tempo/src/engine/engine.normalizer.ts
@magmacomputing
Copy link
Copy Markdown
Owner Author

@coderabbitai full review please

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Tip

For best results, initiate chat on the files or code changes.

@magmacomputing Sure! I'll kick off a full review of this PR right away. 🐇

[full_review]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant