Skip to content

Code standards#20

Merged
magmacomputing merged 13 commits intomainfrom
code-standards
Apr 25, 2026
Merged

Code standards#20
magmacomputing merged 13 commits intomainfrom
code-standards

Conversation

@magmacomputing
Copy link
Copy Markdown
Owner

@magmacomputing magmacomputing commented Apr 24, 2026

Sandboxing the Tempo Class to allow for multiple concurrent configurations.
First phase of the 'Layout Ordering' pattern matching.

Summary by CodeRabbit

  • New Features

    • Sandbox factory: create isolated Tempo subclasses via Tempo.create(options).
    • Shorthand duration construction (e.g., new Tempo({ hours: 2 })).
    • Parse results include traceable source metadata (local/global/plugin).
  • Behavior Changes

    • User-defined event/period aliases take precedence over built-ins.
    • Debug accepts numeric levels; optional layout-order trace output when debug enabled.
  • Improvements

    • Enhanced compact 6/8‑digit time/date parsing (24:00 rollover handling).
    • Improved timezone/calendar normalization, docs and tests updated.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Bumps packages to v2.5.0; adds a comprehensive assertion library; consolidates proxy/freezing/unwrap behavior; refactors symbol branding to a shared Identity symbol; introduces deterministic layout-order engine and Tempo.create sandbox factory; reorganizes module/plugin paths with widespread import rewiring, tests, and docs updates.

Changes

Cohort / File(s) Summary
Version bumps
package.json, packages/library/package.json, packages/tempo/package.json
Root and package versions bumped to 2.5.0; tempo package exports/imports and dev dependency pins adjusted.
Assertion library & barrel
packages/library/src/common/assertion.library.ts, packages/library/src/common.index.ts
New comprehensive runtime predicates/assertions added and re-exported from the common barrel.
Symbol surface & Tempo branding
packages/library/src/common/symbol.library.ts, packages/tempo/src/support/tempo.symbol.ts, packages/tempo/src/support/support.index.ts
Library symbols changed to named exports (adds $Identity); Tempo now re-exports/consumes library symbols and switched internal branding to use $Identity.
Proxy / deep-freeze / unwrap
packages/library/src/common/proxy.library.ts, packages/library/src/common/utility.library.ts, packages/library/src/common/primitive.library.ts
Unified proxy factory with delegator/binding cache, new secure/proxify/delegate behaviors routed through factory, securedeepFreeze refactor supporting skip, and added unwrap() helper.
Coercion & nullish helpers
packages/library/src/common/coercion.library.ts, packages/library/src/common/string.library.ts, packages/library/src/common/array.library.ts
Local isNumeric removed (imported from assertion library); added nullishToZero/nullishToEmpty/nullishToValue; replaced nullToValue calls with nullishToValue.
Import rewiring to assertion module
multiple packages/library/src/..., packages/tempo/src/...
Many predicate imports moved from type.library/coercion to assertion.library; numerous relative/alias paths updated to new engine/module locations.
Logify & logging levels
packages/library/src/common/logify.class.ts, packages/library/test/*
Adds numeric LOG levels, trace method, and severity-based gating (debug now boolean
Tempo sandbox / class-state refactor
packages/tempo/src/tempo.class.ts, packages/tempo/src/tempo.type.ts, packages/tempo/src/support/tempo.init.ts
Per-subclass isolated state with symbol-named static accessors, Tempo.create() sandbox factory, parse result source tracing, and type updates for state/options.
Layout-order engine & tests
packages/tempo/src/engine/engine.layout.ts, packages/tempo/test/engine.layout.test.ts, packages/tempo/test/layout.order.test.ts
New layout ordering primitives, controller factory, classification-aware resolver, pairwise swap logic, and unit tests.
Parse/format pipeline updates
packages/tempo/src/discrete/discrete.parse.ts, packages/tempo/src/discrete/discrete.format.ts
Parse now uses centralized getTemporalIds(...), parse state handling changed (isChanged/withState), match source metadata added; format virtualization switched to delegator.
Support registries & defaults
packages/tempo/src/support/tempo.default.ts, packages/tempo/src/support/tempo.enum.ts, packages/tempo/src/support/tempo.register.ts
Adds compact layouts and new Event/Period entries, expands exported symbols/registries, unwrap-based registry handling, and registry update semantics modified.
Module path / build config changes
packages/tempo/bin/tsconfig.json, packages/tempo/src/tsconfig.json, packages/tempo/vitest.config.ts, packages/tempo/test/tsconfig.json, vitest.config.ts
Redirects #tempo/* aliases from plugin→module, adds #tempo/mutate, introduces engine/module/plugin wildcard mappings, and loads temporal polyfill in test setup.
Module reorg & rewiring
packages/tempo/src/module/*, packages/tempo/src/plugin/*, packages/tempo/src/engine/*
Moves/rewires duration/mutate/term/extend modules to new engine/module locations and updates related augmentations/imports.
Docs, changelogs, importmaps, demos
packages/tempo/doc/*, packages/tempo/CHANGELOG.md, packages/tempo/importmap.json, packages/tempo/demo/*, packages/tempo/public/esm_core.index.html
Adds sandbox/migration docs, alias-precedence notes, updates importmaps and demo/import targets to dist/module paths, removes a large Tempo doc file, and adjusts many admonitions.
Tests added/updated
packages/tempo/test/*, packages/library/test/*
Adds/updates tests: compact-time parsing, layout ordering, sandbox factory, logging gating, pledge logging, proxy/delegator, and many other suites.
Planning docs updated/cleansed
packages/tempo/plan/*
Adds new planning docs (sandbox, alias precedence) and removes obsolete plan files.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant TempoBase as Tempo
    participant Sandbox as SandboxSubclass
    participant State as LocalState
    participant Registry as LocalRegistry

    Client->>TempoBase: Tempo.create(options)
    TempoBase->>State: clone/init parse & registries
    TempoBase->>Registry: clone or bind local registry view
    TempoBase->>Sandbox: create derived subclass bound to State/Registry
    TempoBase->>Sandbox: freeze/harden subclass
    TempoBase-->>Client: return Sandbox subclass

    Client->>Sandbox: new Sandbox(input)
    Sandbox->>Registry: resolve aliases (local-first)
    Registry-->>Sandbox: match + source:'local'
    Sandbox->>State: parse using local config
    State-->>Client: parsing result (with source metadata)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I hopped through symbols, assertions, and code,
I stitched sandbox gardens where state can roam,
Layouts now dance in orderly rows,
Delegators whisper where proxies once rose,
v2.5 — a carrot for home. 🥕

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch code-standards

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: 17

Caution

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

⚠️ Outside diff range comments (6)
packages/library/src/common/logify.class.ts (1)

35-70: ⚠️ Potential issue | 🔴 Critical

Critical: default debug:false now silently drops .log/.info/.warn/.debug, and a low numeric debug can also suppress .error rethrows.

Two behavioral problems in the new severity gate:

  1. Default regression. The constructor defaults debug to false (Line 95). With the new mapping, debug:false produces currentLevel = Level[Error] = 1, so .log() (3), .info() (3), .warn() (2), and .debug() (4) all early-return at Line 40. Previously only .debug() was gated on a falsy debug; all other methods emitted. Existing callers that rely on plain .log()/.warn() output will now see nothing by default.

  2. Rethrow suppression. The early return on Line 40 short-circuits the entire #trap body, including the method === Method.Error && !config.catch rethrow at Lines 62–70. If a caller sets debug: 0 (a valid number per the new type) they silently swallow every error — losing both console output and the exception, which violates the comment at Lines 32–34.

🛠️ Suggested fix

Keep the severity filter for console output only, and preserve the old default (info-and-above visible):

 	`#trap`(method: Logify.Method, ...msg: any[]) {
 		const config = (isObject(msg[0]) && (msg[0] as any)[sym.$Logify] === true) ? msg.shift() : this.#opts;
-		const currentLevel = (typeof config.debug === 'number') ? config.debug : (config.debug ? Level[Method.Debug] : Level[Method.Error]);
+		// default (debug===false) keeps legacy behaviour: show everything except Debug.
+		const currentLevel = (typeof config.debug === 'number')
+			? config.debug
+			: (config.debug ? Level[Method.Debug] : Level[Method.Info]);
 		const methodLevel = Level[method] ?? 0;
 
-		if (methodLevel > currentLevel) return;
-
-
-		const output = msg.map(m => {
+		const shouldEmit = methodLevel <= currentLevel;
+		const output = msg.map(m => {
 			if (m instanceof Error) return m.message;
 			...
 		}).filter(s => !isEmpty(s)).join(' ');
 
-		if (!config.silent && !isEmpty(output))
+		if (shouldEmit && !config.silent && !isEmpty(output))
 			(console as any)[method](`${this.#name}: ${output}`);
 
 		if (method === Method.Error && !config.catch) {

This keeps the rethrow path unconditional for .error() (matching the documented contract) and restores the previous default UX.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/src/common/logify.class.ts` around lines 35 - 70, The
severity gate currently maps config.debug=false to Level.Error and early-returns
from `#trap`, which silently drops non-debug logs and suppresses error rethrows;
fix by (1) changing the currentLevel mapping so that typeof config.debug ===
'number' uses that number, config.debug === true maps to Level[Method.Debug],
and falsy/non-number (including false/undefined) maps to Level[Method.Info] to
restore prior defaults, and (2) remove the early return that exits the whole
`#trap`; instead only skip the console emission by checking if methodLevel >
currentLevel immediately before the console call (keep the error path: the
Method.Error && !config.catch rethrow must still execute unconditionally).
packages/tempo/src/plugin/plugin.util.ts (1)

99-105: ⚠️ Potential issue | 🟡 Minor

Silent collision when $logError isn't registered; drop the commented console.error.

With the console.error(msg) fallback removed, if TempoClass[sym.$logError] is not yet a function (e.g., attachStatics runs before the logger/extension is wired, or in a stripped-down host), a static name collision is silently skipped via continue. That removes the only diagnostic signal for an otherwise easy-to-miss conflict. Either restore the console.error as a fallback or at least delete the dead line 103 to make intent explicit.

♻️ Suggested fallback + dead-code cleanup
 		if (Object.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)
 				TempoClass[sym.$logError]({ ...TempoClass.config, catch: true }, msg);
+			} else {
+				console.error(msg);
 			}
-			// console.error(msg);
 			continue;
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/plugin/plugin.util.ts` around lines 99 - 105, The
collision-detection branch silently skips reporting when
TempoClass[sym.$logError] isn't registered; update the attach logic around
TempoClass and sym.$logError so that if isFunction(TempoClass[sym.$logError]) is
false you still emit a diagnostic (restore a console.error fallback that logs a
clear message including msg and TempoClass.config or remove the stray commented
console.error to make intent explicit); ensure the fallback uses the same
context info (TempoClass.config and msg) so collisions are visible even when the
extension logger isn't wired.
packages/tempo/src/discrete/discrete.parse.ts (1)

326-336: ⚠️ Potential issue | 🟡 Minor

prevZdt is captured but never restored — either use it or drop it.

Line 329 saves const prevZdt = state.zdt;, but the finally block (lines 477–482) unconditionally overwrites state.zdt with the mutated dateTime and never references prevZdt. If the intent is to scope state.zdt to the current parse frame (matching the prevAnchor handling right next to it), you should restore it:

 		} finally {
 			state.anchor = prevAnchor;
-			if (isZonedDateTime(dateTime)) state.zdt = dateTime;
+			state.zdt = isZonedDateTime(dateTime) ? dateTime : prevZdt;
 			state.parseDepth--;
 			if (state.parseDepth === 0) state.matches = undefined;
 		}

If the current behavior (latest dateTime persists out of the frame) is intentional, drop prevZdt to remove the misleading capture.

Also applies to: 477-482

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/discrete/discrete.parse.ts` around lines 326 - 336, In
parseGroups, the local const prevZdt = state.zdt is captured but never used;
either restore state.zdt from prevZdt in the finally block (so state.zdt is
scoped to the parse frame like state.anchor) or remove the unused prevZdt
declaration to avoid confusion — update the finally that currently overwrites
state.zdt with the mutated dateTime to instead set state.zdt = prevZdt (or
delete prevZdt if you choose to persist the mutated zdt); refer to parseGroups,
prevZdt, prevAnchor and the finally block that resets state.anchor/state.zdt for
locating the change.
packages/tempo/index.md (1)

252-300: ⚠️ Potential issue | 🟡 Minor

resumeTimer isn't cleared on unmount.

handleVisibility clears resumeTimer on each invocation, but onUnmounted doesn't. If the user navigates away within the 1200 ms grace window, the pending timeout will still fire on an unmounted component, calling startTicker() and setting reactive refs on a torn-down view.

 onUnmounted(() => {
   isMounted = false
   ticker?.stop()
   if (fallbackIntervalId) clearInterval(fallbackIntervalId)
   clearInterval(carouselTimer)
+  if (resumeTimer) clearTimeout(resumeTimer)
   document.removeEventListener('visibilitychange', handleVisibility)
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/index.md` around lines 252 - 300, The component doesn't clear
the pending resumeTimer on unmount which can fire after teardown; update the
onUnmounted block to clearTimeout(resumeTimer) (guarded by a truthy check) and
set resumeTimer = null, and ensure any state-setting in the timeout
(startTicker(), isResuming, tickerActive) won't run after unmount by relying on
isMounted or the cleared timer; reference symbols: resumeTimer,
handleVisibility, onUnmounted, startTicker, isResuming, tickerActive,
clearTimeout.
packages/tempo/src/support/tempo.default.ts (1)

144-156: ⚠️ Potential issue | 🟡 Minor

half hour doesn't fit the Period contract.

Every other Period entry resolves to a clock string (e.g. '24:00', '8:00', '18:00') representing a time-of-day. half[ -]?hour instead returns this.add({ minutes: 30 }), which is a relative duration, not a time-of-day — effectively a relative-offset behavior that belongs in Event (matching tomorrow/yesterday's pattern) or in the {rel} layout, not in {per}. Downstream {tm} matching treats Period as a clock-reading alternative, so a phrase like "at half hour" will route through the wrong resolution path and interact strangely with subsequent date parsing.

Please either (a) move the entry to Event, or (b) have it return a clock string like other Period entries.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/support/tempo.default.ts` around lines 144 - 156, The
Period map contains an entry 'half[ -]?hour' that returns a relative duration
(function using this.add({ minutes: 30 })) which violates Period's time-of-day
contract; either remove that key from Period and add it to the Event mapping (so
the function stays as Event's handler and uses Tempo.add to produce a relative
offset), or change the Period entry to return a concrete clock string like
'00:30'/'0:30' (matching other Period values) so it resolves as a time-of-day;
update the mapping for the symbol Period and the key 'half[ -]?hour' accordingly
and ensure downstream {per}/{tm} behavior is consistent.
packages/tempo/src/support/tempo.enum.ts (1)

246-265: ⚠️ Potential issue | 🟡 Minor

LOG is declared but not included in the default enums export.

The new numeric LOG enum is exported as a named export on Line 18 and its type on Lines 26-27, but it's absent from the default-export registry below. If tempo.type.ts really does type Internal.BaseOptions.debug as boolean | enums.LOG (per the AI summary), consumers accessing it via the public-reachable registry — or Tempo.LOG via a static getter — will receive undefined. Add it to keep symmetry with the other enum entries:

 	OPTION,
 	MODE,
 	PARSE,
+	LOG,
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/support/tempo.enum.ts` around lines 246 - 265, The default
export object in tempo.enum.ts is missing the LOG entry, so add LOG to the
exported registry (alongside SEASON, COMPASS, etc.) to ensure Tempo.LOG and any
consumers using the public enums registry receive the numeric LOG enum; update
the default export list to include LOG exactly as the other symbols are listed
and verify the named export/type for LOG remains intact so tempo.type.ts
consumers still accept enums.LOG.
🧹 Nitpick comments (21)
packages/tempo/src/tempo.index.ts (1)

5-6: Consider matching import order to usage order.

The imports are now ordered as Parse, Format, Mutate, Duration, Terms, but the core array on Line 13 uses them in the order Parse, Mutate, Format, Duration, Terms. Aligning the import order with usage order can improve readability.

📝 Optional refactor for consistency
 import { ParseModule } from '#tempo/parse';
-import { FormatModule } from '#tempo/format';
-
 import { MutateModule } from '#tempo/mutate';
+import { FormatModule } from '#tempo/format';
 import { DurationModule } from '#tempo/duration';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/tempo.index.ts` around lines 5 - 6, Reorder the import
statements so they follow the same sequence used in the core array to improve
readability: import ParseModule first, then MutateModule, then FormatModule,
then DurationModule, then TermsModule (matching the usage order in the core
array). Update the import lines that reference FormatModule and the other tempo
modules to match this order so the file imports align with the core array
ordering.
packages/library/src/common/coercion.library.ts (1)

73-75: LGTM — tiny naming caveat.

?? coalesces both null and undefined, so these helpers apply to nullish values generally rather than only null. That matches the usual convention, but nullishToZero / nullishToEmpty would be technically more accurate. Feel free to ignore.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/src/common/coercion.library.ts` around lines 73 - 75, Rename
the helpers to reflect that they operate on nullish (null or undefined) values:
change nullToZero -> nullishToZero, nullToEmpty -> nullishToEmpty, and
optionally nullToValue -> nullishToValue; update all exports and any call sites
that reference the functions (search for nullToZero, nullToEmpty, nullToValue)
and adjust imports/usage accordingly to avoid breaking references, keeping the
original behavior (obj ?? ...).
packages/tempo/test/term_unified.test.ts (1)

91-96: Good coverage — consider one additional case to strengthen the contract.

The test confirms 'SouthSouthWest' maps onto the 'south' range, which validates substring matching. Since the AI summary states matching is case-insensitive substring, you could strengthen the contract by also asserting an all-caps / all-lowercase variant (e.g. 'SOUTH' or 'southsouthwest') — otherwise a regression that drops toLowerCase() but keeps .includes() would still pass this case (the literal substring south already appears verbatim).

Suggested additional assertion
 	it('should support granular sphere matching (e.g. SouthSouthWest)', () => {
 		// Even though ranges are defined for 'south', 'SouthSouthWest' should match it via .includes()
 		const t = new Tempo(testDate, { catch: true, sphere: 'SouthSouthWest' });
 		expect(t.format('{`#qtr`}')).toBe('Q4');
 		expect(t.set({ start: '#quarter' }).format('{yyyy}-{mm}-{dd}')).toBe('2024-04-01');
+
+		// Also verify case-insensitivity with an all-caps variant that does NOT contain the
+		// literal lowercase substring 'south' — this guards against a regression that drops
+		// the .toLowerCase() normalisation but keeps .includes().
+		const u = new Tempo(testDate, { catch: true, sphere: 'SOUTH' });
+		expect(u.format('{`#qtr`}')).toBe('Q4');
 	});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/test/term_unified.test.ts` around lines 91 - 96, Add an extra
assertion to ensure sphere matching is case-insensitive: when creating a Tempo
instance (new Tempo(testDate, { catch: true, sphere: ... })) add a second case
using an all-caps or all-lowercase sphere value (e.g., 'SOUTH' or
'southsouthwest') and assert the same outputs via t.format('{`#qtr`}') and t.set({
start: '#quarter' }).format('{yyyy}-{mm}-{dd}'), so that the matching logic in
Tempo (constructor handling of sphere and its includes() check) is verified for
case-insensitivity as well as substring matching.
packages/tempo/doc/tempo.cookbook.md (1)

56-59: Consider noting that the hhmiss-vs-compact-date precedence is timezone-independent.

The bullet says 6-digit is checked as compact time first; the adjacent examples mix timeZone: 'UTC' (for the time) and timeZone: 'Europe/London' (for the date fallback), which may read as if timezone participates in the first decision. A short clarification that timezone only influences the compact-date fallback (not the hhmiss validity check) would prevent misreading.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/doc/tempo.cookbook.md` around lines 56 - 59, Clarify that the
6-digit hhmiss-vs-compact-date precedence is timezone-independent: update the
paragraph describing 6-digit parsing to state explicitly that Tempo always first
validates the 6-digit string as compact time (`hhmiss`) regardless of
`timeZone`, and that `timeZone` only affects interpretation when falling back to
compact date layouts (e.g., `ddmmyy` vs `mmddyy`); reference `hhmiss` and the
`timeZone` option so readers see which decision is timezone-agnostic and which
is not.
packages/tempo/CHANGELOG.md (1)

8-15: Consider cutting a version header before release.

PR objectives describe a 2.4.0 → 2.5.0 bump, but the changelog currently parks this as [Unreleased] and the history jumps from 2.2.x2.3.0 with no 2.4.0 entry. Before tagging, replace ## [Unreleased] with the concrete target version and date so consumers can tie the alias-precedence behavioral change (which is a breaking parse-output change in overlap cases) to a specific release.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/CHANGELOG.md` around lines 8 - 15, Replace the placeholder
header "## [Unreleased]" with a concrete release header for the target bump
(e.g. "## [2.5.0] - YYYY-MM-DD") so the "Alias Precedence" change is tied to a
specific release; also verify the changelog sequence around "2.2.x", "2.3.0" and
the missing "2.4.0" entry and add a short 2.4.0 history line if that version was
released, ensuring the final changelog reflects the 2.4.0 → 2.5.0 bump and
includes the release date.
packages/tempo/src/engine/engine.term.ts (1)

2-4: Consolidate duplicate imports from #library/assertion.library.js.

isDefined, isString, isZonedDateTime and isNumeric are imported from the same module across two separate statements. Combine them into one import for cleanliness and to avoid future churn/merge noise.

♻️ Proposed fix
 import { toZonedDateTime, toInstant } from '#library/temporal.library.js';
-import { isDefined, isString, isZonedDateTime } from '#library/assertion.library.js';
+import { isDefined, isNumeric, isString, isZonedDateTime } from '#library/assertion.library.js';
 import { asArray } from '#library/coercion.library.js';
-import { isNumeric } from '#library/assertion.library.js';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/engine/engine.term.ts` around lines 2 - 4, Combine the
duplicate imports from the same module by replacing the two separate import
statements that bring in isDefined, isString, isZonedDateTime and isNumeric from
'#library/assertion.library.js' with a single import declaration that imports
all four symbols (isDefined, isString, isZonedDateTime, isNumeric) together, and
remove the redundant import; update the import group that also contains asArray
from '#library/coercion.library.js' only if necessary (leave as-is otherwise).
packages/tempo/src/plugin/extend/extend.ticker.ts (1)

1-4: Duplicate import source — merge into one statement.

Lines 1 and 4 both import from #library/assertion.library.js. Consolidating avoids churn and keeps the module list tidy.

♻️ Proposed dedupe
-import { isObject, isFunction, isDefined, isUndefined, isEmpty, isNumber } from '#library/assertion.library.js'
+import { isObject, isFunction, isDefined, isUndefined, isEmpty, isNumber, isNumeric } from '#library/assertion.library.js'
 import { Pledge } from '#library/pledge.class.js'
 import { asArray } from '#library/coercion.library.js'
-import { isNumeric } from '#library/assertion.library.js'
 import { instant, normaliseFractionalDurations } from '#library/temporal.library.js'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/plugin/extend/extend.ticker.ts` around lines 1 - 4, The
file imports from '#library/assertion.library.js' twice; consolidate the two
import statements into a single import that includes all needed symbols
(isObject, isFunction, isDefined, isUndefined, isEmpty, isNumber, isNumeric) so
there is only one import from '#library/assertion.library.js' and remove the
duplicate import line; update the top-of-file imports to reference Pledge and
asArray as before and keep the single combined assertion.library import.
packages/library/src/common/utility.library.ts (1)

68-80: skip is read-only inside deepFreeze — document or rename.

skip is never added to; only checked on entry. That's intentional (caller pre-populates with objects it wants preserved mutable), but the signature alone suggests it behaves like seen. Two small improvements:

  1. Add a JSDoc line clarifying skip is an opt-out set the caller owns, distinct from the internal cycle tracker seen.
  2. Consider making skip a required first-class named option (e.g., deepFreeze(obj, { skip })) so it isn't confused positionally with seen by callers.

Also: the default skip = new WeakSet() is allocated on every top-level call even when unused — negligible, but a shared sentinel EMPTY_SKIP would make intent obvious.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/src/common/utility.library.ts` around lines 68 - 80, The
deepFreeze function's skip parameter is currently read-only and confusingly
positioned; update deepFreeze to document and/or convert skip into an explicit
caller-owned opt-out (either via a JSDoc comment and a clearer name or by
changing the signature to deepFreeze(obj, { skip }?: { skip?: WeakSet<object>
}), replace the per-call default new WeakSet() with a shared EMPTY_SKIP sentinel
to avoid allocations, and ensure seen remains the internal cycle-tracker; add a
JSDoc above deepFreeze explaining that skip is an externally owned opt-out set
(not mutated by deepFreeze) and reference the symbols deepFreeze, skip, seen,
and any new EMPTY_SKIP constant to locate the change.
packages/tempo/test/engine.layout.test.ts (1)

14-96: LGTM — thorough resolver coverage.

The toBe(layout) checks (lines 23, 94) implicitly lock in that resolveLayoutOrder/resolveLayoutClassificationOrder return the original reference on no-op/fallback paths. Worth a short comment in the resolver noting this contract so it isn't accidentally broken by a future “always return a new object” refactor.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/test/engine.layout.test.ts` around lines 14 - 96, Tests rely
on referential equality (expect(...).toBe(layout)) so ensure resolveLayoutOrder
and resolveLayoutClassificationOrder preserve and return the original layout
object when no changes are needed; update those functions to explicitly return
the input layout when no swaps/classification occur and add a short in-code
comment above resolveLayoutOrder and resolveLayoutClassificationOrder
documenting this contract (i.e., "must return the original reference for no-op
paths to satisfy tests/consumers expecting referential equality").
packages/library/src/common/primitive.library.ts (1)

16-23: Add a cycle/depth guard to unwrap — currently hangable on self-referential proxies.

The loop terminates only when curr[sym.$Target] (or the string-key fallback) is falsy. A proxy whose $Target returns the proxy itself — or any cycle of length > 1 — produces an infinite loop, because each iteration re-observes a truthy $Target. ownEntries already caps prototype climbing at 50 for the same reason; unwrap should do the same.

🛡️ Proposed guard
 export function unwrap<T extends object>(obj: T): T {
 	let curr = (obj as any)?.[sym.$Target] ?? obj;
-	// Hardened against different Symbol instances across module boundaries
-	while (curr && (Object.prototype.hasOwnProperty.call(curr, sym.$Target) || (curr as any).$Target)) {
-		curr = curr[sym.$Target] ?? (curr as any).$Target;
-	}
+	// Hardened against different Symbol instances across module boundaries
+	// and against self-referential / cyclic $Target chains.
+	const seen = new Set<any>([curr]);
+	let depth = 0;
+	while (curr && depth++ < 50
+		&& (Object.prototype.hasOwnProperty.call(curr, sym.$Target) || (curr as any).$Target)) {
+		const next = curr[sym.$Target] ?? (curr as any).$Target;
+		if (!next || seen.has(next)) break;
+		seen.add(next);
+		curr = next;
+	}
 	return curr;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/src/common/primitive.library.ts` around lines 16 - 23, The
unwrap function can loop forever on self-referential or cyclic $Target chains;
add a depth/cycle guard (e.g., maxDepth = 50) inside unwrap to break and return
the current value when the guard is reached. Modify the while loop in function
unwrap to increment a counter each iteration and stop when the counter exceeds
the guard, ensuring you still check both symbol-based (sym.$Target) and
string-key ('$Target') fallbacks; reference the existing symbols/fields used in
unwrap (curr, sym.$Target, (curr as any).$Target) so the guard integrates
without changing the lookup logic.
packages/tempo/src/discrete/discrete.parse.ts (3)

21-23: Trailing blank-line block left over from refactor.

Three consecutive blank lines right after the imports look like a merge artifact. Collapse to a single blank line for consistency with the rest of the file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/discrete/discrete.parse.ts` around lines 21 - 23, There
are three consecutive blank lines after the import block at the top of
discrete.parse.ts (a leftover merge artifact); collapse them to a single blank
line so the import section is followed by exactly one blank line for consistency
with the rest of the file.

449-452: No-op try/catch: remove the bare rethrow.

catch (e: any) { throw e; }

adds nothing (not even a log or a wrapped message) and only costs readers a second look. Either drop the wrapper or wrap the error with context (alias name, source, etc.):

♻️ Proposed fix
-					catch (e: any) {
-						throw e;
-					}
+					// (let caller handle; add context here if future diagnostics need it)

Or, if diagnostics are wanted:

-					catch (e: any) {
-						throw e;
-					}
+					catch (e: any) {
+						const msg = `[Tempo#parseGroups] ${aliasKey} handler threw: ${e?.message ?? e}`;
+						if (TempoClass) (TempoClass as any)[sym.$logError](state.config, new Error(msg));
+						throw e;
+					}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/discrete/discrete.parse.ts` around lines 449 - 452, The
catch block that simply rethrows the error ("catch (e: any) { throw e; }") in
packages/tempo/src/discrete/discrete.parse.ts is a no-op and should be removed
or replaced with contextual error wrapping; either delete the try/catch so the
original error and stack propagate, or replace the catch with a wrapped error
that adds context (e.g., include alias/source info) while preserving the
original as the cause (throw new Error(`...context...`, { cause: e })). Locate
the trivial catch in the parse logic (the catch block shown) and apply one of
these changes to eliminate the useless rethrow.

133-133: Redundant disjuncts in isAnchoredVal.

val at line 63 is assigned as dateTime ?? state.anchor ?? …, so isDefined(val) (stored in isAnchored) already implies isDefined(dateTime) || isDefined(state.anchor). The extra checks on line 133 are no-ops:

-			const isAnchoredVal = isAnchored || isDefined(dateTime) || isDefined(state.anchor);
+			const isAnchoredVal = isAnchored;

Or just pass isAnchored straight through.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/discrete/discrete.parse.ts` at line 133, The expression
computing isAnchoredVal is redundant because val is assigned as dateTime ??
state.anchor ?? … so isAnchored (or isDefined(val)) already covers dateTime and
state.anchor; replace the compound disjunction with simply using isAnchored (or
isDefined(val)) directly. Locate the isAnchoredVal assignment in discrete.parse
(symbol: isAnchoredVal) and change it to pass through isAnchored (or compute
isDefined(val)) instead of recomputing isDefined(dateTime) ||
isDefined(state.anchor).
packages/tempo/src/tempo.type.ts (1)

190-192: Optional: narrow Match.source to a string-literal union.

Downstream code uses distinct values ('global', 'local', 'default', 'plugin:X'). Consider:

source?: 'global' | 'local' | 'default' | `plugin:${string}`;

to give consumers discriminated-union autocompletion while still allowing plugin:* growth. Purely optional; the current string is fine.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/tempo.type.ts` around lines 190 - 192, Narrow the
Match.source type from a plain string to a discriminated string-literal union to
improve autocompletion and type-safety: change the Match interface's source
property (referenced as Match.source) to optional source?: 'global' | 'local' |
'default' | `plugin:${string}` so existing uses still accept arbitrary plugin
values while giving consumers the specific union variants for discrimination and
tooling support.
packages/tempo/test/sandbox-factory.test.ts (2)

81-90: Immutability assertion is weak — consider asserting the throw.

(Sandbox as any).someNewProp = 'test' will silently succeed in non-strict mode and silently fail under Object.freeze, making the subsequent toBeUndefined() pass for the wrong reason. If @Immutable is intended to throw in strict mode, assert that directly:

-    try {
-      (Sandbox as any).someNewProp = 'test';
-    } catch (e) { }
-
-    expect((Sandbox as any).someNewProp).toBeUndefined();
+    'use strict';
+    expect(() => { (Sandbox as any).someNewProp = 'test'; }).toThrow();
+    expect((Sandbox as any).someNewProp).toBeUndefined();

Or, if frozen-silent is the contract, document it in the test name (silently drops new static props).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/test/sandbox-factory.test.ts` around lines 81 - 90, The
current immutability test for Sandbox is ambiguous because assigning (Sandbox as
any).someNewProp = 'test' can silently fail; update the test to either assert a
thrown error (if `@Immutable` should throw) or explicitly assert the frozen-silent
contract: replace the try/catch with expect(() => { (Sandbox as any).someNewProp
= 'test'; }).toThrow() when you want a thrown error from Tempo.create / Sandbox,
or change the test name to indicate "silently drops new static props" and add a
direct frozen check such as expect(Object.isFrozen(Sandbox)).toBe(true) plus
expect((Sandbox as any).someNewProp).toBeUndefined(); reference the Sandbox
variable created by Tempo.create({ locale: 'fr-FR' }) and update the it(...)
title accordingly.

1-12: Vitest globals are enabled; consider adding explicit imports for clarity.

Lines 1–12 use describe, it, expect, and on line 12 vi without importing them. This works because packages/tempo/vitest.config.ts sets globals: true and packages/tempo/test/tsconfig.json includes vitest/globals in types. Adding an explicit import is optional but improves code clarity and self-documentation:

+import { describe, it, expect, vi } from 'vitest';
 import { Tempo } from '#tempo';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/test/sandbox-factory.test.ts` around lines 1 - 12, The test
uses Vitest globals (describe, it, expect, vi) without explicit imports which
reduces clarity; add a top-level import from 'vitest' (e.g., import { describe,
it, expect, vi } from 'vitest') so the functions used in the tests (describe,
it, expect) and the spy utility (vi) are explicitly imported; update the import
statement near the top of sandbox-factory.test.ts so the test remains
functionally unchanged but is self-documenting.
packages/tempo/src/support/tempo.init.ts (1)

23-60: getDateTimeFormat() is called three times; outer destructure is dead.

Line 23 destructures { timeZone, calendar } but those values aren't used in either branch — the global branch re-calls on line 50 (shadowing), and line 54 calls a third time just to read .locale. Consolidate to a single call:

♻️ Proposed fix
-	const { timeZone, calendar } = getDateTimeFormat();
-
 	const state = {
 		config: {},
 		parse: {}
 	} as t.Internal.State
@@
 	if (isGlobal) {
 		markConfig(Object.assign(state.config, Default));
-		const { timeZone, calendar } = getDateTimeFormat();
+		const fmt = getDateTimeFormat() as any;
+		const { timeZone, calendar } = fmt;
 		Object.defineProperties(state.config, {
 			calendar: { value: calendar, enumerable: true, writable: true, configurable: true },
 			timeZone: { value: timeZone, enumerable: true, writable: true, configurable: true },
-			locale: { value: (getDateTimeFormat() as any).locale ?? 'en-US', enumerable: true, writable: true, configurable: true },
+			locale: { value: fmt.locale ?? 'en-US', enumerable: true, writable: true, configurable: true },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/support/tempo.init.ts` around lines 23 - 60, The initial
call to getDateTimeFormat() is duplicated and its outer destructure (timeZone,
calendar) is unused; remove the outer destructure and call getDateTimeFormat()
once, capture its result into a single local (e.g., dtf = getDateTimeFormat()),
then reuse dtf.timeZone, dtf.calendar and dtf.locale when initializing
state.config inside the isGlobal branch and anywhere else; update references in
the markConfig/isGlobal block (the Object.defineProperties entries for calendar,
timeZone, locale and sphere) to use the single dtf variable and remove the extra
calls to getDateTimeFormat().
packages/library/src/common/type.library.ts (1)

40-44: Defensive check for non-array sym.$Registry.

resetRegistry (Line 62) already guards with Array.isArray(...) before mutating the global registry, but getType assumes the value is iterable. If third-party code writes anything non-array to globalThis[sym.$Registry], the spread at Line 41 will throw at runtime. Consider mirroring the Array.isArray guard here for symmetry.

🛡️ Proposed defensive guard
-			const globalRegistry = (globalThis as any)[sym.$Registry] ?? [];
+			const globalRegistry = Array.isArray((globalThis as any)[sym.$Registry])
+				? (globalThis as any)[sym.$Registry]
+				: [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/src/common/type.library.ts` around lines 40 - 44, The
getType function assumes globalThis[sym.$Registry] is iterable and spreads it
into [...registry, ...globalRegistry], which will throw if a non-array was
written there; mirror the defensive check used in resetRegistry by ensuring
globalRegistry is an array (e.g., use Array.isArray((globalThis as
any)[sym.$Registry]) ? (globalThis as any)[sym.$Registry] : []) before
spreading, then iterate over [...registry, ...safeGlobalRegistry] and continue
using inst.class / (inst.class as any)[sym.$Target] to resolve types.
packages/tempo/src/engine/engine.layout.ts (1)

35-52: Duplicate / undefined description collapses layout entries.

byName is keyed on key.description ?? ''. If the Layout map ever contains two symbols with the same description (or two symbols with undefined description — which both collapse to ''), the Map construction silently drops the earlier one, and resolveLayoutClassificationOrder will reorder based on the second of the two. The Tempo-shipped tokens are unique today, but any plugin authoring layout extensions via raw Symbol(...) could accidentally introduce a dup.

Either (a) assert uniqueness and warn, or (b) switch to a multi-map that preserves insertion order when descriptions collide.

🛡️ Minimal guard
 	const entries = ownEntries(layout) as LayoutEntry[];
-	const byName = new Map(entries.map(([key, value]) => [key.description ?? '', [key, value] as LayoutEntry]));
+	const byName = new Map<string, LayoutEntry>();
+	entries.forEach(([key, value]) => {
+		const name = key.description ?? '';
+		if (!name || byName.has(name)) return;         // skip anonymous/duplicates rather than silently overwrite
+		byName.set(name, [key, value]);
+	});

Additionally, the changed check on Line 51 combines next.length === entries.length (always true by construction — unseen entries are always appended) with .some(...). The length guard is dead — you can drop it and just compare order:

-	const changed = next.length === entries.length && next.some((entry, idx) => entry[0] !== entries[idx][0]);
+	const changed = next.some((entry, idx) => entry[0] !== entries[idx][0]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/engine/engine.layout.ts` around lines 35 - 52, The layout
reordering silently drops earlier entries when two keys share the same
description because byName is a Map keyed on key.description ?? '', so change
byName into a multi-map that preserves insertion order (e.g., Map<string,
LayoutEntry[]>) and push every ([key, value]) into the array for that
description; in resolveLayoutClassificationOrder (or the function using
ownEntries/preferred), when iterating preferred use the first matching entry
from the array and mark each consumed symbol in seen (and optionally log a
warning when a description array length > 1 to signal duplicates), and remove
the dead length check in the changed calculation — simply compare the symbol
order (next.some((entry, idx) => entry[0] !== entries[idx][0])) to decide if
layout changed.
packages/tempo/src/support/tempo.default.ts (1)

30-30: Redundant \- inside guard character class.

\- appears twice in the character class: once after \d\s and again after \+. Harmless but looks unintentional — likely a merge artifact.

💄 Cleanup
-	/** base guard characters (digits and common symbols) */	guard: /[\d\s\-\.\:T\/Z\+\-\(\)\,\=\#\<\>]/i,
+	/** base guard characters (digits and common symbols) */	guard: /[\d\s\-\.\:T\/Z\+\(\)\,\=\#\<\>]/i,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/support/tempo.default.ts` at line 30, The guard regex in
tempo.default.ts defines the property guard:
/[\d\s\-\.\:T\/Z\+\-\(\)\,\=\#\<\>]/i and contains a duplicated escaped hyphen
(\-); remove the redundant \- so the character class only includes a single
hyphen (ensure the hyphen remains escaped or placed at the start/end to avoid
creating a range) while preserving all other characters and the case-insensitive
flag on the guard property.
packages/tempo/src/tempo.class.ts (1)

683-692: Cosmetic: mix of sym.$Internal and bare $Internal.

$Internal is imported on Line 24 and is the same symbol that sym.$Internal resolves to via the prototype chain in tempo.symbol.ts. Within this method alone, lines 683/691/692 use sym.$Internal but 703/714/723 (further down) use $Internal. Unifying to the bare import would be easier to read and slightly cheaper (avoids the prototype lookup).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/tempo.class.ts` around lines 683 - 692, The method mixes
sym.$Internal and the imported bare $Internal; replace uses of sym.$Internal
with the imported $Internal throughout this method so the same symbol is
referenced consistently (e.g., when accessing (this as
any)[sym.$Internal]().parse.period and (this as any)[sym.$Internal]() used
before calling $setPeriods/$setEvents). Update all occurrences so you read (this
as any)[$Internal]() instead of (this as any)[sym.$Internal](), leaving other
symbols ($setPeriods, $setEvents, parse.period, parse.event, target,
config.ranges) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4d488875-2c90-4c58-a492-251634afc4d7

📥 Commits

Reviewing files that changed from the base of the PR and between 5c6b2b6 and 8331545.

📒 Files selected for processing (85)
  • package.json
  • packages/library/package.json
  • packages/library/src/browser/mapper.library.ts
  • packages/library/src/browser/tapper.class.ts
  • packages/library/src/browser/webstore.class.ts
  • packages/library/src/common.index.ts
  • packages/library/src/common/array.library.ts
  • packages/library/src/common/assertion.library.ts
  • packages/library/src/common/coercion.library.ts
  • packages/library/src/common/enumerate.library.ts
  • packages/library/src/common/function.library.ts
  • packages/library/src/common/logify.class.ts
  • packages/library/src/common/object.library.ts
  • packages/library/src/common/pledge.class.ts
  • packages/library/src/common/primitive.library.ts
  • packages/library/src/common/proxy.library.ts
  • packages/library/src/common/reflection.library.ts
  • packages/library/src/common/serialize.library.ts
  • packages/library/src/common/storage.library.ts
  • packages/library/src/common/string.library.ts
  • packages/library/src/common/symbol.library.ts
  • packages/library/src/common/temporal.library.ts
  • packages/library/src/common/type.library.ts
  • packages/library/src/common/utility.library.ts
  • packages/library/test/common/temporal_guards.test.ts
  • packages/tempo/CHANGELOG.md
  • packages/tempo/bin/tsconfig.json
  • packages/tempo/demo/3-modular-granular.html
  • packages/tempo/doc/Tempo.md
  • packages/tempo/doc/installation.md
  • packages/tempo/doc/migration-guide.md
  • packages/tempo/doc/releases/v2.x.md
  • packages/tempo/doc/sandbox-factory.md
  • packages/tempo/doc/tempo.cookbook.md
  • packages/tempo/doc/tempo.modularity.md
  • packages/tempo/importmap.json
  • packages/tempo/index.md
  • packages/tempo/package.json
  • packages/tempo/plan/.WISHLIST.md
  • packages/tempo/plan/alias-precedence-strategy.md
  • packages/tempo/plan/astrological_seasons.md
  • packages/tempo/plan/configuration.md
  • packages/tempo/plan/release-process.md
  • packages/tempo/plan/shorthand.md
  • packages/tempo/plan/standalone_parse.md
  • packages/tempo/public/esm_core.index.html
  • packages/tempo/src/discrete/discrete.format.ts
  • packages/tempo/src/discrete/discrete.parse.ts
  • packages/tempo/src/engine/engine.composer.ts
  • packages/tempo/src/engine/engine.layout.ts
  • packages/tempo/src/engine/engine.lexer.ts
  • packages/tempo/src/engine/engine.term.ts
  • packages/tempo/src/module/module.duration.ts
  • packages/tempo/src/module/module.mutate.ts
  • packages/tempo/src/plugin/extend/extend.ticker.ts
  • packages/tempo/src/plugin/plugin.type.ts
  • packages/tempo/src/plugin/plugin.util.ts
  • packages/tempo/src/plugin/term.util.ts
  • packages/tempo/src/plugin/term/term.quarter.ts
  • packages/tempo/src/plugin/term/term.zodiac.ts
  • packages/tempo/src/support/support.index.ts
  • packages/tempo/src/support/tempo.default.ts
  • packages/tempo/src/support/tempo.enum.ts
  • packages/tempo/src/support/tempo.init.ts
  • packages/tempo/src/support/tempo.register.ts
  • packages/tempo/src/support/tempo.symbol.ts
  • packages/tempo/src/support/tempo.util.ts
  • packages/tempo/src/tempo.class.ts
  • packages/tempo/src/tempo.index.ts
  • packages/tempo/src/tempo.type.ts
  • packages/tempo/src/tsconfig.json
  • packages/tempo/test/compact.time.test.ts
  • packages/tempo/test/constructor.shorthand.test.ts
  • packages/tempo/test/discovery_security.test.ts
  • packages/tempo/test/engine.layout.test.ts
  • packages/tempo/test/instance.since.rtf.test.ts
  • packages/tempo/test/layout.order.test.ts
  • packages/tempo/test/pattern.weekday.test.ts
  • packages/tempo/test/runtime_brand.test.ts
  • packages/tempo/test/sandbox-factory.test.ts
  • packages/tempo/test/symbol-import.test.ts
  • packages/tempo/test/term_unified.test.ts
  • packages/tempo/test/tsconfig.json
  • packages/tempo/vitest.config.ts
  • vitest.config.ts
💤 Files with no reviewable changes (4)
  • packages/tempo/test/runtime_brand.test.ts
  • packages/tempo/plan/astrological_seasons.md
  • packages/tempo/plan/standalone_parse.md
  • packages/tempo/plan/shorthand.md

Comment thread packages/library/src/common/assertion.library.ts Outdated
Comment thread packages/library/src/common/assertion.library.ts
Comment thread packages/library/src/common/enumerate.library.ts Outdated
Comment thread packages/library/src/common/temporal.library.ts
Comment thread packages/tempo/doc/installation.md Outdated
Comment thread packages/tempo/src/support/tempo.init.ts
Comment thread packages/tempo/src/tempo.class.ts
Comment thread packages/tempo/src/tempo.class.ts
Comment thread packages/tempo/src/tempo.class.ts
Comment thread packages/tempo/src/tsconfig.json
@magmacomputing magmacomputing self-assigned this Apr 24, 2026
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: 12

Caution

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

⚠️ Outside diff range comments (1)
packages/tempo/index.md (1)

305-313: ⚠️ Potential issue | 🟠 Major

Clear snap/restore timers when the page becomes hidden.

Line 305 onward pauses autoplay, but pending carouselSnapTimer/carouselRestoreTimer can still fire while hidden and mutate carousel state unexpectedly. Clear them in the hidden branch too.

♻️ Proposed fix
   } else {
     isResuming.value = false
     tickerActive.value = false
     console.info('%c[Tempo]%c 💤 Pausing Ticker (Standby)', 'color: `#71717a`; font-weight: bold', 'color: inherit')
     ticker?.stop()
     if (fallbackIntervalId) clearInterval(fallbackIntervalId)
+    clearCarouselSnapTimers()
     clearInterval(carouselTimer)
     carouselTimer = null
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/index.md` around lines 305 - 313, When handling the
hidden-page branch that pauses autoplay (the else block managing
isResuming.value, tickerActive.value, ticker?.stop(), fallbackIntervalId, and
carouselTimer), also clear any pending snap/restore timers by clearing
carouselSnapTimer and carouselRestoreTimer (and setting them to null) to prevent
them firing while hidden; update the same hidden-branch logic to call
clearTimeout/clearInterval on carouselSnapTimer and carouselRestoreTimer and
nullify those variables alongside the existing carouselTimer cleanup.
🧹 Nitpick comments (15)
packages/library/src/common/logify.class.ts (2)

103-113: Orphan severity Trace: no corresponding log method.

LOG.Trace = 5 is exported but there is no trace method on Logify, and no Level entry for it, so setting debug: LOG.Trace behaves identically to LOG.Debug for emission. If Trace is intended as “verbose Debug”, consider either adding a trace = (...msg) => this.#trap(Method.Trace, ...) (and a corresponding Method.Trace/Level entry) or dropping Trace from the enum until it's wired up, to avoid misleading public API.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/src/common/logify.class.ts` around lines 103 - 113, LOG
includes an orphaned Trace level (LOG.Trace) that has no corresponding
Logify.trace method or Method/Level entry, so enable it by adding a trace
handler or remove it; specifically either add a Logify.trace(...) that forwards
to this.#trap(Method.Trace, ...) and create matching Method.Trace and
Level.Trace entries so emission differs from Debug, or remove LOG.Trace from the
enum to avoid exposing an unsupported level; update any references to
LogLevel/Level to reflect the chosen change.

40-43: Severity gating semantics look correct; consider binding public LOG to internal Level.

The runtime Level map (lines 17-24) and the exported LOG enum (lines 103-111) independently assign numeric severities; they currently agree (Error=1, Warn=2, Info=3, Debug=4), but they can drift in future edits. Consider deriving one from the other so changes to public LOG levels stay in sync with the methodLevel <= currentLevel gate, e.g.:

♻️ Proposed refactor
-const Level = {
-	[Method.Error]: 1,
-	[Method.Warn]: 2,
-	[Method.Info]: 3,
-	[Method.Log]: 3,
-	[Method.Debug]: 4,
-} as const;
+const Level = {
+	[Method.Error]: LOG.Error,
+	[Method.Warn]: LOG.Warn,
+	[Method.Info]: LOG.Info,
+	[Method.Log]: LOG.Info,
+	[Method.Debug]: LOG.Debug,
+} as const;

(which requires declaring LOG above Level, or inlining the numbers consistently).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/src/common/logify.class.ts` around lines 40 - 43, The public
LOG enum and the internal Level map can drift; change the code so LOG is derived
from (or directly references) the internal Level values instead of duplicating
numeric assignments: ensure the exported LOG symbol reflects Level's severities
and update any checks using currentLevel and methodLevel (e.g., the calculation
of currentLevel from config.debug and the lookup Level[method] in methodLevel)
to use the unified source so methodLevel <= currentLevel gating remains correct.
Locate the Level object and exported LOG enum definitions and refactor so LOG is
bound to Level (or vice versa) to keep severities in sync across the class.
packages/library/src/common/assertion.library.ts (2)

57-84: Consider extracting the repeated Temporal isType / instanceof / toStringTag pattern.

Each Temporal guard duplicates the same four-branch shape (isTypeinstanceof Temporal.XSymbol.toStringTag → duck-typed heuristic). A small helper like hasTemporalTag(obj, 'Temporal.PlainDate') would collapse the boilerplate and make the duck-type heuristics easier to audit:

♻️ Sketch
const hasTemporalTag = (obj: any, tag: keyof typeof globalThis['Temporal'] | string) =>
	!!(globalThis as any).Temporal?.[tag] && obj instanceof (globalThis as any).Temporal[tag]
	|| (!!obj && obj[Symbol.toStringTag] === `Temporal.${tag}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/src/common/assertion.library.ts` around lines 57 - 84, Many
Temporal type-guards (isTemporal, isInstant, isZonedDateTime, isPlainDate,
isPlainTime, isPlainDateTime, isDuration, isPlainYearMonth, isPlainMonthDay, and
related *Like helpers) duplicate the same four-branch pattern (isType ->
instanceof globalThis.Temporal.X -> Symbol.toStringTag -> duck-typing), so
extract a small helper (e.g. hasTemporalTag) and refactor each guard to call it;
update isTemporal to use protoType + helper for specific Temporal classes and
replace repeated instanceof/toStringTag checks inside isInstant,
isZonedDateTime, isPlainDate, isPlainTime, isPlainDateTime, isDuration,
isPlainYearMonth, and isPlainMonthDay with calls to that helper and keep only
the duck-typed heuristics in each guard.

33-33: isDigit is a misleading name for a Number | BigInt check.

The guard accepts any finite or non-finite number and any BigInt, not a single digit. Consider renaming to something like isNumeric-adjacent (isNumberOrBigInt, isNumericType, or similar) to prevent misuse at call sites that actually want single-digit semantics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/src/common/assertion.library.ts` at line 33, The exported
type-guard isDigit misnames a check for Number|BigInt; rename the function to a
clearer name (e.g., isNumberOrBigInt or isNumericType) and update its export and
all internal imports/usages to that new identifier; keep the implementation
(isType(obj, 'Number', 'BigInt')) but change the function name and associated
JSDoc/comments, and optionally add a backward-compatible deprecated alias export
named isDigit that delegates to the new function to avoid breaking external
callers while you update call sites.
packages/library/test/common/logify.class.test.ts (2)

3-52: Consider extending coverage to debug: true and the Log method.

The new severity-gating table treats debug: true → LOG.Debug and Method.Log → 3. Neither path is currently exercised. A couple of extra cases would cement the contract and guard against future drift between the internal Level map and the public LOG enum:

♻️ Suggested additions
test('debug: true maps to Debug level (emits debug)', () => {
  const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
  try {
    new Logify('LogifyTestDebugTrue', { debug: true }).debug('debug-visible');
    expect(debugSpy).toHaveBeenCalledWith('LogifyTestDebugTrue: debug-visible');
  } finally { debugSpy.mockRestore(); }
});

test('log() emits at Info level by default', () => {
  const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
  try {
    new Logify('LogifyTestLog').log('log-visible');
    expect(logSpy).toHaveBeenCalledWith('LogifyTestLog: log-visible');
  } finally { logSpy.mockRestore(); }
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/test/common/logify.class.test.ts` around lines 3 - 52, Add
two tests to exercise the debug:true mapping and the Log method: create a test
that constructs new Logify('LogifyTestDebugTrue', { debug: true }) and calls
.debug('debug-visible'), asserting console.debug was called with
'LogifyTestDebugTrue: debug-visible' (restore spy afterward), and add a test
that constructs new Logify('LogifyTestLog') and calls .log('log-visible'),
asserting console.log was called with 'LogifyTestLog: log-visible' (restore spy
afterward); reference the Logify constructor and its .debug and .log methods and
ensure spies use mockImplementation to avoid real output.

40-51: Add a comment or test name capturing that debug: 0 intentionally suppresses console.error.

The assertion expect(errorSpy).not.toHaveBeenCalled() is the key behavioral contract from the severity-gating change (Error level is 1, so 1 <= 0 is false). Since this will look surprising to readers who expect errors to always emit, consider renaming the test or adding a short comment referencing LOG.Off:

♻️ Proposed tweak
-	test('rethrows errors even when log emission is gated off', () => {
+	test('rethrows errors even when log emission is gated off (debug: 0 / LOG.Off)', () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/test/common/logify.class.test.ts` around lines 40 - 51,
Rename the test or add a short inline comment to clarify that setting debug: 0
intentionally suppresses console.error (i.e., LOG.Off behavior) so the assertion
expect(errorSpy).not.toHaveBeenCalled() is deliberate; update the test name
'rethrows errors even when log emission is gated off' or add a comment next to
the Logify instantiation (new Logify('LogifyTestRethrow', { debug: 0, catch:
false })) referencing that debug: 0 corresponds to LOG.Off and therefore
error-level emissions are suppressed even though log.error rethrows the error.
packages/tempo/src/tempo.type.ts (1)

10-10: Make TempoBrand a type-only import.

TempoBrand is declared as export type TempoBrand = { ... } in packages/tempo/src/support/tempo.symbol.ts and is only used in a type position on line 32. Under verbatimModuleSyntax / isolatedModules, mixing it with the value import of sym without the type qualifier can trigger build errors. Use an inline type modifier:

♻️ Proposed fix
-import { sym, TempoBrand } from '#tempo/support/tempo.symbol.js';
+import { sym, type TempoBrand } from '#tempo/support/tempo.symbol.js';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/tempo.type.ts` at line 10, The import mixes a value import
(sym) with a type-only export (TempoBrand); change the import to import sym as a
value and TempoBrand as a type-only import (use the inline type modifier for
TempoBrand) so TempoBrand is imported with the type qualifier and avoids
isolatedModules/verbatimModuleSyntax build errors when used in the type position
(refer to the TempoBrand symbol and the sym value import in this file).
packages/tempo/test/term_unified.test.ts (1)

91-108: LGTM — tests cover granular and case-insensitive sphere matching paths.

Assertions on both {#qtr} formatting and { start: '#quarter' } mutation give good coverage of the new getRange/resolveCycleWindow sphere fallback. One optional follow-up: add a negative test (e.g. sphere: 'unknownzone') to verify the fallback does not resolve to an arbitrary hemisphere — this would lock in the “no match → empty/error” path for the TermError dispatch that term.util.ts was refactored to use.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/test/term_unified.test.ts` around lines 91 - 108, Add a
negative test that constructs a Tempo instance with an unknown sphere (e.g. new
Tempo(testDate, { catch: true, sphere: 'unknownzone' })) and assert that queries
relying on getRange/resolveCycleWindow do not resolve to a hemisphere value but
instead trigger the intended failure path (e.g. produce an empty result or cause
the TermError dispatch from term.util.ts); use the same pattern as existing
tests (format('{`#qtr`}') and set({ start: '#quarter' })) to confirm it does not
return Q4 or a valid date and that TermError handling is invoked.
packages/tempo/src/support/tempo.default.ts (1)

30-30: Minor: \- duplicated inside the guard character class.

\- appears twice (…\-\.\:T\/Z\+\-\(\)…). Harmless but redundant; one occurrence is enough.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/support/tempo.default.ts` at line 30, The guard regular
expression on the exported property named "guard" contains a duplicated escaped
hyphen (\-) inside the character class; edit the guard value in tempo.default.ts
(the guard property) to remove the redundant \- so the character class only
includes a single hyphen escape, leaving the rest of the tokens unchanged.
packages/tempo/src/engine/engine.layout.ts (2)

69-95: Reference‑preservation contract reads correctly; consider memoizing description lookups.

The delegation to resolveLayoutClassificationOrder first, plus returning ordered when no pair swap occurs, preserves the resolvedLayout === originalLayout invariant that Tempo.#swapLayout relies on at tempo.class.ts:238. For larger layout registries, the repeated layouts.findIndex([key]) => key.description === …) inside the mdyLayouts.forEach is O(N·M); a one‑shot Map<description, index> built after ownEntries(ordered) would keep the intent but drop it to O(N+M). Purely optional for current sizes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/engine/engine.layout.ts` around lines 69 - 95, The current
resolveLayoutOrder uses repeated layouts.findIndex calls inside
mdyLayouts.forEach which is O(N·M); to speed this up, after creating layouts =
ownEntries(ordered) build a Map from key.description to index (e.g.,
descIndexMap) and use that map to look up idx1/idx2 instead of findIndex; when
you swap entries in layouts update both the map entries for the two descriptions
and set changed = true as before, and keep the existing return behavior (return
ordered when !changed to preserve the reference-preservation contract used by
Tempo.#swapLayout).

45-58: Duplicate names in a controller entry can silently suppress reordering.

The first forEach in resolveLayoutClassificationOrder pushes to next without checking seen, so if a classification's preferred array contains the same name twice (e.g. through a misconfigured controller), next.length > entries.length, causing the next.length === entries.length guard on line 56 to fail and the function to return the original layout — masking the intended reorder entirely rather than surfacing the misconfiguration.

Defensive dedup in the preferred loop would prevent that silent no‑op:

♻️ Proposed hardening
 	preferred.forEach(name => {
 		const entry = byName.get(name);
-		if (!entry) return;
+		if (!entry || seen.has(entry[0])) return;
 		seen.add(entry[0]);
 		next.push(entry);
 	});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/engine/engine.layout.ts` around lines 45 - 58, The
preferred-names loop in resolveLayoutClassificationOrder can push the same entry
multiple times (if preferred contains duplicates) which makes next.length >
entries.length and prevents detection of reordering; fix by deduplicating when
iterating preferred (use seen to skip if entry[0] already in seen) so you only
push unique entries from preferred into next (references:
resolveLayoutClassificationOrder, preferred, byName, seen, next, entries),
ensuring the changed check correctly detects reorderings instead of silently
returning the original layout.
packages/tempo/src/support/tempo.init.ts (1)

48-80: Substantial duplication between global and fallback branches.

Lines 51‑60 and 68‑77 define the same 7 descriptors (calendar, timeZone, locale, discovery, formats, sphere, get) with identical values. Only scope and catch differ. A small helper would remove the drift risk:

♻️ Suggested helper extraction
-	if (isGlobal) {
-		markConfig(Object.assign(state.config, Default));
-		const { timeZone, calendar } = getDateTimeFormat();
-		Object.defineProperties(state.config, {
-			calendar: { value: calendar, enumerable: true, writable: true, configurable: true },
-			timeZone: { value: timeZone, enumerable: true, writable: true, configurable: true },
-			locale: { value: (getDateTimeFormat() as any).locale ?? 'en-US', enumerable: true, writable: true, configurable: true },
-			discovery: { value: Symbol.keyFor(sym.$Tempo) as string, enumerable: true, writable: true, configurable: true },
-			formats: { value: enumify(STATE.FORMAT, false), enumerable: true, writable: true, configurable: true },
-			sphere: { value: getHemisphere(timeZone), enumerable: true, writable: true, configurable: true },
-			get: { value: function (key: string) { return this[key] }, enumerable: false, writable: true, configurable: true },
-			scope: { value: 'global', enumerable: true, writable: true, configurable: true },
-			catch: { value: options.catch ?? false, enumerable: true, writable: true, configurable: true }
-		});
-	} else if (baseState) {
+	const defineBaseConfig = (scope: 'global' | 'local') => {
+		markConfig(Object.assign(state.config, Default));
+		Object.defineProperties(state.config, {
+			calendar:  { value: calendar, enumerable: true, writable: true, configurable: true },
+			timeZone:  { value: timeZone, enumerable: true, writable: true, configurable: true },
+			locale:    { value: (getDateTimeFormat() as any).locale ?? 'en-US', enumerable: true, writable: true, configurable: true },
+			discovery: { value: Symbol.keyFor(sym.$Tempo) as string, enumerable: true, writable: true, configurable: true },
+			formats:   { value: enumify(STATE.FORMAT, false), enumerable: true, writable: true, configurable: true },
+			sphere:    { value: getHemisphere(timeZone), enumerable: true, writable: true, configurable: true },
+			get:       { value: function (k: string) { return this[k] }, enumerable: false, writable: true, configurable: true },
+			scope:     { value: scope, enumerable: true, writable: true, configurable: true },
+			catch:     { value: options.catch ?? false, enumerable: true, writable: true, configurable: true },
+		});
+	};
+
+	if (isGlobal) {
+		defineBaseConfig('global');
+	} else if (baseState) {
 		state.config = markConfig(Object.create(baseState.config));
 		setProperty(state.config, 'scope', 'local');
 		if (isDefined(options.catch)) setProperty(state.config, 'catch', options.catch);
 	} else {
-		markConfig(Object.assign(state.config, Default));
-		Object.defineProperties(state.config, { /* …same 7 descriptors… */ });
-		if (isDefined(options.catch))
-			setProperty(state.config, 'catch', options.catch);
+		defineBaseConfig('local');
 	}

(Line 50's re‑destructuring of timeZone/calendar can be dropped since line 23 already provides them.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/support/tempo.init.ts` around lines 48 - 80, There is
duplicated Object.defineProperties descriptor setup for state.config in the
isGlobal and fallback branches; extract a small helper (e.g.,
buildConfigDescriptors or applyCommonConfigDescriptors) that returns/applies the
shared descriptors for calendar, timeZone, locale, discovery (using
Symbol.keyFor(sym.$Tempo)), formats (enumify(STATE.FORMAT, false)), sphere
(getHemisphere(timeZone)), and get (function), then call that helper from both
branches and only set scope/catch separately (use setProperty for catch when
needed); also remove the redundant re-destructuring of timeZone/calendar since
getDateTimeFormat() was already called earlier.
packages/library/src/common/proxy.library.ts (1)

97-109: Discovery side‑effect: get mutates the target to cache misses.

When onGet returns undefined and the target is extensible, the trap writes { value: undefined, enumerable: false, configurable: true } onto the real target. This is a useful "don't look it up again" hint, but has a few consequences worth documenting:

  • The raw target accretes hidden properties over the lifetime of the proxy (no eviction).
  • If onGet's answer for a key ever becomes defined after a prior miss, the cached descriptor will shadow further discovery attempts (since Reflect.has(t, k) is now true).
  • Reflect.ownKeys(t) will include these marker keys, which leaks into ownKeys on line 56 (they're non‑enumerable so for…in/Object.keys skip them, but Reflect.ownKeys/Object.getOwnPropertyNames expose them).

Not a bug for the current callers, but a deliberate contract worth a comment, or a WeakMap<target, Set<key>> side‑cache to avoid mutating user data.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/src/common/proxy.library.ts` around lines 97 - 109, The get
trap currently writes a hidden undefined property onto the real target (see
onGet, pending, t, k and the Object.defineProperty call guarded by
Reflect.isExtensible) to mark misses, which mutates user data and can shadow
later valid discoveries and leak into Reflect.ownKeys; fix it by replacing that
mutation with a side-cache: introduce a WeakMap (e.g., missCache:
WeakMap<object, Set<PropertyKey>>) keyed by target and store k in the Set
instead of defining a property, check missCache.has(target) &&
missCache.get(target).has(k) before calling onGet, and remove the
Object.defineProperty call and related Reflect.has checks that rely on the
synthetic descriptor; alternatively if mutation is acceptable, add a clear
explanatory comment at the Object.defineProperty site documenting the tradeoffs
(shadowing, no eviction, visibility via Reflect.ownKeys) so reviewers know it is
intentional.
packages/library/test/common/pledge.class.test.ts (1)

68-111: Good coverage of the numeric debug gating contract.

Both tests cover the key behaviors of the new Logify.Constructor["debug"] typing: the .then(runSafely) chain is given time to flush (await p.promise + await Promise.resolve()) before assertions, and the debug: 0 case correctly exercises the methodLevel (2) <= currentLevel (0) === false gate in Logify#trap.

Minor note: line 84's assertion on the exact string 'Pledge: Pledge callback failed' is tightly coupled to Logify's ${name}: ${output} format. If that prefix contract is deliberate, consider promoting it to a shared constant; otherwise be prepared for cross‑module churn if Logify's framing ever changes. Not blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/test/common/pledge.class.test.ts` around lines 68 - 111, The
assertion that the warn message equals the exact prefixed string is too tightly
coupled to Logify's framing; update the test in the 'callback failures warn at
default debug level (indirect Logify integration)' case to either (a) assert
only on the variable part of the message (e.g., that the warn output contains
'Pledge callback failed' rather than the full 'Pledge: Pledge callback failed'
prefix) or (b) import/use a shared constant from Logify (eg. a published
MESSAGE_PREFIX or format helper) and build the expected string from that
constant so the test uses Pledge and Logify's canonical prefix rather than a
hardcoded literal; keep the existing warnSpy/debugSpy setup and the await flush
sequence unchanged.
packages/library/src/common/primitive.library.ts (1)

44-73: ownEntries(all=true) edge case with prototype shadowing.

Walking the prototype chain into levels then iterating with levels.reverse() goes base → derived, and values are resolved from tgt (line 68). If a derived prototype shadows a base key with a non‑enumerable descriptor, the loop still emits the base level's (enumerable) entry even though tgt[k] would pick up the derived value through prototype resolution. The enumerable check is applied per‑level rather than at the effective (topmost) descriptor.

Unlikely to hit in practice for the way this is used today, but worth a comment noting the invariant, or checking enumerability of the effective property (via Object.getOwnPropertyDescriptor walking from tgt down).

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/library/src/common/assertion.library.ts`:
- Around line 70-72: isPlainDate/isPlainDateTime duck-typing branches
incorrectly conflate PlainDate and PlainDateTime and make PlainDateTime
impossible to match; update the fallbacks so isPlainDate explicitly ensures the
absence of time fields and isPlainDateTime requires the presence of time fields.
Concretely, in isPlainDate add a check that hour/minute/second/nanosecond are
undefined (so objects with time parts don't match), and in isPlainDateTime
remove the isUndefined(daysInMonth) requirement and instead require at least one
of the time-part properties (hour, minute, second, nanosecond) to be defined;
keep the existing isType/instanceof/@@toStringTag branches and the
toZonedDateTime/timeZoneId checks but use these time-field tests to disambiguate
the fallback paths in isPlainDate and isPlainDateTime.

In `@packages/library/src/common/logify.class.ts`:
- Around line 40-43: Add a JSDoc note to the Logify constructor's debug option
(Logify.Constructor.debug) documenting that debug accepts boolean | number
mapped to LOG levels and that numeric 0 (LOG.Off) will suppress all logging
including error output (so console.error will not be called even if errors are
rethrown when catch:false); update the comment near the existing JSDoc at the
constructor declaration to explicitly call out this behavior and recommend using
a higher numeric level or true to ensure errors are logged.

In `@packages/library/src/common/primitive.library.ts`:
- Around line 16-27: The loop in unwrap uses
Object.prototype.hasOwnProperty.call(curr, sym.$Target) and (curr as
any).$Target which trigger getOwnPropertyDescriptor traps and thus cannot detect
proxy-fabricated $Target; fix by driving traversal via direct property access
instead: in unwrap(), repeatedly read next = curr[sym.$Target] ?? (curr as
any).$Target and if next is falsy or depth >= maxDepth break, otherwise set curr
= next and increment depth (keep sym.$Target and string "$Target" checks and the
maxDepth guard). This preserves proxy get-trap behavior and correctly unwraps
chained proxies.

In `@packages/library/src/common/proxy.library.ts`:
- Around line 146-149: The delegator function currently uses Object.keys(keys)
which drops symbol keys when a Record with symbol keys is passed; change the key
extraction to use Reflect.ownKeys(keys) so both string and symbol keys are
preserved (e.g., use const keyList = Array.isArray(keys) ? keys :
Reflect.ownKeys(keys) as K[]), and ensure the resulting keyList is passed into
factory({} as any, { keys: keyList, onGet: fn as any }) so
has/getOwnPropertyDescriptor/ownKeys behave correctly for symbol keys.
- Around line 44-48: The getOwnPropertyDescriptor implementation currently
returns a generic data descriptor when keys is fixed, which can disagree with
the get trap (get -> onGet) by showing value: undefined; update
getOwnPropertyDescriptor to, when keys is present and includes k, attempt to
obtain the actual value via onGet(k, t) and return a descriptor that
materializes that value (enumerable: true, configurable: true, value:
<materialized>), but guard this call with the same recursion/pending check used
elsewhere (the pending set/flag referenced in the get trap) so you skip calling
onGet if recursion is already in flight and fall back to the existing generic
descriptor in that case. Ensure you reference and reuse the existing onGet
function and the pending recursion guard used around the get trap.

In `@packages/tempo/doc/migration-guide.md`:
- Around line 70-78: The fenced JSON example in the migration guide is indented
as an indented code block and triggers MD046; replace the indented/fenced mix
with a plain fenced block by removing the leading indentation and surrounding
the JSON with a proper triple-backtick fence labeled json (i.e., change the
current indented lines to: ```json on its own line, the JSON object unindented,
then closing ``` on its own line) so the Example fenced code block is valid and
lint-free.

In `@packages/tempo/doc/releases/v2.x.md`:
- Line 9: Update the release note bullet that reads "Added fortnight event and
half hour period aliases." to hyphenate the compound modifier by changing "half
hour" to "half-hour" so the line becomes "Added fortnight event and half-hour
period aliases."

In `@packages/tempo/index.md`:
- Around line 234-243: onSwipeStart can be interrupted by a second pointer which
overwrites swipePointerId; update onSwipeStart to early-return if a drag is
already active (check isDragging.value) and to only start a new drag for the
primary pointer (check e.isPrimary or prefer primary pointer for touch). In
practice, at the top of onSwipeStart guard with something like: return if
isDragging.value is true, and also return if the incoming event is not the
primary pointer (e.isPrimary === false) so multi-touch won't override
swipePointerId; keep the rest of the logic (setting swipePointerId, swipeStartX,
swipeLastX, dragOffsetX.value, transitionEnabled.value, isDragging.value, and
calling setPointerCapture) unchanged.

In `@packages/tempo/src/plugin/term.util.ts`:
- Around line 273-276: The code currently invokes the TermError symbol on the
constructor directly ((t.constructor as any)[TermError](...)) which can throw a
TypeError if the symbol/method isn't installed; change these calls to use
optional chaining so the symbol is only invoked if present (e.g., (t.constructor
as any)[TermError]?.(t.config, 'template')), preserving the fallback return []
behavior; apply the same optional-chaining fix to the other occurrence around
the block referenced (the second dispatch at the later lines) so
unresolved-template cases degrade gracefully.
- Around line 164-171: The sphere-matching logic is inconsistent: in getRange's
list filter (vars valA/valB) it checks config includes range
(valB.includes(valA)), but resolveCycleWindow uses mutual containment
(targetSphere.includes(sphereLower) || sphereLower.includes(targetSphere)); make
both use the same asymmetric rule where the config may be more specific than the
range key (i.e., check that config.includes(range)). Update resolveCycleWindow
to replace the bidirectional check with the single-direction check consistent
with getRange, and ensure the variable names (targetSphere, sphereLower) are
used so the logic reads targetSphere.includes(sphereLower).
- Around line 164-171: The current filter inside list.filter / keys.every treats
missing sphere values as '' so ''.includes('') or any.includes('') returns true
and unconditionally matches; change the sphere branch so it only matches when
the range's sphere is present and non-empty and the term's config sphere
contains it — i.e., compute valA = String(r[key] ?? '').toLowerCase() and valB =
String((t.config as any)[key] ?? '').toLowerCase() as before but require valA
!== '' (or both valA and valB non-empty) before using valB.includes(valA),
otherwise return false; update the sphere conditional in the list.filter block
accordingly.

In `@packages/tempo/src/support/tempo.init.ts`:
- Around line 62-80: The baseState branch currently creates state.config via
Object.create(baseState.config) which leaves core keys (timeZone, calendar,
locale, formats, sphere, discovery, get) on the prototype and causes
inconsistent own-key shape vs the global/fallback branches; change the baseState
branch to materialize the same own, enumerable properties as the other branches
(i.e. copy or define own properties for calendar, timeZone, locale, discovery,
formats, sphere, get, scope) after calling markConfig(state.config) so
Object.keys/Reflect.ownKeys/spread see the same surface, and explicitly set
'catch' with setProperty(state.config, 'catch', options.catch) (or
defineProperty) so 'catch' is always an own property regardless of Default.

---

Outside diff comments:
In `@packages/tempo/index.md`:
- Around line 305-313: When handling the hidden-page branch that pauses autoplay
(the else block managing isResuming.value, tickerActive.value, ticker?.stop(),
fallbackIntervalId, and carouselTimer), also clear any pending snap/restore
timers by clearing carouselSnapTimer and carouselRestoreTimer (and setting them
to null) to prevent them firing while hidden; update the same hidden-branch
logic to call clearTimeout/clearInterval on carouselSnapTimer and
carouselRestoreTimer and nullify those variables alongside the existing
carouselTimer cleanup.

---

Nitpick comments:
In `@packages/library/src/common/assertion.library.ts`:
- Around line 57-84: Many Temporal type-guards (isTemporal, isInstant,
isZonedDateTime, isPlainDate, isPlainTime, isPlainDateTime, isDuration,
isPlainYearMonth, isPlainMonthDay, and related *Like helpers) duplicate the same
four-branch pattern (isType -> instanceof globalThis.Temporal.X ->
Symbol.toStringTag -> duck-typing), so extract a small helper (e.g.
hasTemporalTag) and refactor each guard to call it; update isTemporal to use
protoType + helper for specific Temporal classes and replace repeated
instanceof/toStringTag checks inside isInstant, isZonedDateTime, isPlainDate,
isPlainTime, isPlainDateTime, isDuration, isPlainYearMonth, and isPlainMonthDay
with calls to that helper and keep only the duck-typed heuristics in each guard.
- Line 33: The exported type-guard isDigit misnames a check for Number|BigInt;
rename the function to a clearer name (e.g., isNumberOrBigInt or isNumericType)
and update its export and all internal imports/usages to that new identifier;
keep the implementation (isType(obj, 'Number', 'BigInt')) but change the
function name and associated JSDoc/comments, and optionally add a
backward-compatible deprecated alias export named isDigit that delegates to the
new function to avoid breaking external callers while you update call sites.

In `@packages/library/src/common/logify.class.ts`:
- Around line 103-113: LOG includes an orphaned Trace level (LOG.Trace) that has
no corresponding Logify.trace method or Method/Level entry, so enable it by
adding a trace handler or remove it; specifically either add a Logify.trace(...)
that forwards to this.#trap(Method.Trace, ...) and create matching Method.Trace
and Level.Trace entries so emission differs from Debug, or remove LOG.Trace from
the enum to avoid exposing an unsupported level; update any references to
LogLevel/Level to reflect the chosen change.
- Around line 40-43: The public LOG enum and the internal Level map can drift;
change the code so LOG is derived from (or directly references) the internal
Level values instead of duplicating numeric assignments: ensure the exported LOG
symbol reflects Level's severities and update any checks using currentLevel and
methodLevel (e.g., the calculation of currentLevel from config.debug and the
lookup Level[method] in methodLevel) to use the unified source so methodLevel <=
currentLevel gating remains correct. Locate the Level object and exported LOG
enum definitions and refactor so LOG is bound to Level (or vice versa) to keep
severities in sync across the class.

In `@packages/library/src/common/proxy.library.ts`:
- Around line 97-109: The get trap currently writes a hidden undefined property
onto the real target (see onGet, pending, t, k and the Object.defineProperty
call guarded by Reflect.isExtensible) to mark misses, which mutates user data
and can shadow later valid discoveries and leak into Reflect.ownKeys; fix it by
replacing that mutation with a side-cache: introduce a WeakMap (e.g., missCache:
WeakMap<object, Set<PropertyKey>>) keyed by target and store k in the Set
instead of defining a property, check missCache.has(target) &&
missCache.get(target).has(k) before calling onGet, and remove the
Object.defineProperty call and related Reflect.has checks that rely on the
synthetic descriptor; alternatively if mutation is acceptable, add a clear
explanatory comment at the Object.defineProperty site documenting the tradeoffs
(shadowing, no eviction, visibility via Reflect.ownKeys) so reviewers know it is
intentional.

In `@packages/library/test/common/logify.class.test.ts`:
- Around line 3-52: Add two tests to exercise the debug:true mapping and the Log
method: create a test that constructs new Logify('LogifyTestDebugTrue', { debug:
true }) and calls .debug('debug-visible'), asserting console.debug was called
with 'LogifyTestDebugTrue: debug-visible' (restore spy afterward), and add a
test that constructs new Logify('LogifyTestLog') and calls .log('log-visible'),
asserting console.log was called with 'LogifyTestLog: log-visible' (restore spy
afterward); reference the Logify constructor and its .debug and .log methods and
ensure spies use mockImplementation to avoid real output.
- Around line 40-51: Rename the test or add a short inline comment to clarify
that setting debug: 0 intentionally suppresses console.error (i.e., LOG.Off
behavior) so the assertion expect(errorSpy).not.toHaveBeenCalled() is
deliberate; update the test name 'rethrows errors even when log emission is
gated off' or add a comment next to the Logify instantiation (new
Logify('LogifyTestRethrow', { debug: 0, catch: false })) referencing that debug:
0 corresponds to LOG.Off and therefore error-level emissions are suppressed even
though log.error rethrows the error.

In `@packages/library/test/common/pledge.class.test.ts`:
- Around line 68-111: The assertion that the warn message equals the exact
prefixed string is too tightly coupled to Logify's framing; update the test in
the 'callback failures warn at default debug level (indirect Logify
integration)' case to either (a) assert only on the variable part of the message
(e.g., that the warn output contains 'Pledge callback failed' rather than the
full 'Pledge: Pledge callback failed' prefix) or (b) import/use a shared
constant from Logify (eg. a published MESSAGE_PREFIX or format helper) and build
the expected string from that constant so the test uses Pledge and Logify's
canonical prefix rather than a hardcoded literal; keep the existing
warnSpy/debugSpy setup and the await flush sequence unchanged.

In `@packages/tempo/src/engine/engine.layout.ts`:
- Around line 69-95: The current resolveLayoutOrder uses repeated
layouts.findIndex calls inside mdyLayouts.forEach which is O(N·M); to speed this
up, after creating layouts = ownEntries(ordered) build a Map from
key.description to index (e.g., descIndexMap) and use that map to look up
idx1/idx2 instead of findIndex; when you swap entries in layouts update both the
map entries for the two descriptions and set changed = true as before, and keep
the existing return behavior (return ordered when !changed to preserve the
reference-preservation contract used by Tempo.#swapLayout).
- Around line 45-58: The preferred-names loop in
resolveLayoutClassificationOrder can push the same entry multiple times (if
preferred contains duplicates) which makes next.length > entries.length and
prevents detection of reordering; fix by deduplicating when iterating preferred
(use seen to skip if entry[0] already in seen) so you only push unique entries
from preferred into next (references: resolveLayoutClassificationOrder,
preferred, byName, seen, next, entries), ensuring the changed check correctly
detects reorderings instead of silently returning the original layout.

In `@packages/tempo/src/support/tempo.default.ts`:
- Line 30: The guard regular expression on the exported property named "guard"
contains a duplicated escaped hyphen (\-) inside the character class; edit the
guard value in tempo.default.ts (the guard property) to remove the redundant \-
so the character class only includes a single hyphen escape, leaving the rest of
the tokens unchanged.

In `@packages/tempo/src/support/tempo.init.ts`:
- Around line 48-80: There is duplicated Object.defineProperties descriptor
setup for state.config in the isGlobal and fallback branches; extract a small
helper (e.g., buildConfigDescriptors or applyCommonConfigDescriptors) that
returns/applies the shared descriptors for calendar, timeZone, locale, discovery
(using Symbol.keyFor(sym.$Tempo)), formats (enumify(STATE.FORMAT, false)),
sphere (getHemisphere(timeZone)), and get (function), then call that helper from
both branches and only set scope/catch separately (use setProperty for catch
when needed); also remove the redundant re-destructuring of timeZone/calendar
since getDateTimeFormat() was already called earlier.

In `@packages/tempo/src/tempo.type.ts`:
- Line 10: The import mixes a value import (sym) with a type-only export
(TempoBrand); change the import to import sym as a value and TempoBrand as a
type-only import (use the inline type modifier for TempoBrand) so TempoBrand is
imported with the type qualifier and avoids isolatedModules/verbatimModuleSyntax
build errors when used in the type position (refer to the TempoBrand symbol and
the sym value import in this file).

In `@packages/tempo/test/term_unified.test.ts`:
- Around line 91-108: Add a negative test that constructs a Tempo instance with
an unknown sphere (e.g. new Tempo(testDate, { catch: true, sphere: 'unknownzone'
})) and assert that queries relying on getRange/resolveCycleWindow do not
resolve to a hemisphere value but instead trigger the intended failure path
(e.g. produce an empty result or cause the TermError dispatch from
term.util.ts); use the same pattern as existing tests (format('{`#qtr`}') and
set({ start: '#quarter' })) to confirm it does not return Q4 or a valid date and
that TermError handling is invoked.
🪄 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: 8e12ea4a-29d9-4120-8ff7-835529fdb43f

📥 Commits

Reviewing files that changed from the base of the PR and between 8331545 and e106407.

📒 Files selected for processing (37)
  • CHANGELOG.md
  • packages/library/src/common/array.library.ts
  • packages/library/src/common/assertion.library.ts
  • packages/library/src/common/coercion.library.ts
  • packages/library/src/common/enumerate.library.ts
  • packages/library/src/common/logify.class.ts
  • packages/library/src/common/pledge.class.ts
  • packages/library/src/common/primitive.library.ts
  • packages/library/src/common/proxy.library.ts
  • packages/library/src/common/string.library.ts
  • packages/library/src/common/temporal.library.ts
  • packages/library/src/common/utility.library.ts
  • packages/library/test/common/logify.class.test.ts
  • packages/library/test/common/pledge.class.test.ts
  • packages/tempo/CHANGELOG.md
  • packages/tempo/bin/tsconfig.json
  • packages/tempo/doc/installation.md
  • packages/tempo/doc/migration-guide.md
  • packages/tempo/doc/releases/v2.x.md
  • packages/tempo/doc/sandbox-factory.md
  • packages/tempo/doc/tempo.cookbook.md
  • packages/tempo/index.md
  • packages/tempo/plan/release-process.md
  • packages/tempo/src/discrete/discrete.parse.ts
  • packages/tempo/src/engine/engine.layout.ts
  • packages/tempo/src/engine/engine.term.ts
  • packages/tempo/src/plugin/extend/extend.ticker.ts
  • packages/tempo/src/plugin/term.util.ts
  • packages/tempo/src/support/tempo.default.ts
  • packages/tempo/src/support/tempo.enum.ts
  • packages/tempo/src/support/tempo.init.ts
  • packages/tempo/src/tempo.class.ts
  • packages/tempo/src/tempo.index.ts
  • packages/tempo/src/tempo.type.ts
  • packages/tempo/test/sandbox-factory.test.ts
  • packages/tempo/test/term_unified.test.ts
  • packages/tempo/test/tsconfig.json
✅ Files skipped from review due to trivial changes (6)
  • packages/tempo/src/tempo.index.ts
  • packages/tempo/doc/tempo.cookbook.md
  • packages/tempo/plan/release-process.md
  • packages/tempo/doc/sandbox-factory.md
  • CHANGELOG.md
  • packages/tempo/src/engine/engine.term.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • packages/tempo/CHANGELOG.md
  • packages/tempo/test/tsconfig.json
  • packages/library/src/common/enumerate.library.ts
  • packages/tempo/test/sandbox-factory.test.ts
  • packages/library/src/common/array.library.ts
  • packages/tempo/bin/tsconfig.json
  • packages/library/src/common/temporal.library.ts
  • packages/tempo/src/plugin/extend/extend.ticker.ts
  • packages/library/src/common/utility.library.ts
  • packages/tempo/src/support/tempo.enum.ts
  • packages/tempo/src/discrete/discrete.parse.ts
  • packages/tempo/src/tempo.class.ts

Comment thread packages/library/src/common/assertion.library.ts Outdated
Comment thread packages/library/src/common/primitive.library.ts
Comment thread packages/tempo/doc/migration-guide.md Outdated
Comment thread packages/tempo/doc/releases/v2.x.md Outdated
Comment thread packages/tempo/index.md
Comment thread packages/tempo/src/plugin/term.util.ts
Comment thread packages/tempo/src/plugin/term.util.ts
Comment thread packages/tempo/src/support/tempo.init.ts
@magmacomputing
Copy link
Copy Markdown
Owner Author

@coderabbitai please review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

@magmacomputing: Sure! I'll start reviewing the changes in PR #20 right away.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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: 4

🧹 Nitpick comments (2)
packages/library/src/common/logify.class.ts (1)

19-26: Dedupe severity numbers between Level and LOG.

Level (19–26) and LOG (107–114) independently encode the same error=1…trace=5 ladder. If someone later renumbers LOG (e.g., inserts a new level) without updating Level, gating silently goes wrong. Derive one from the other so there’s a single source of truth.

♻️ Proposed refactor — move `LOG` above and derive `Level` from it
-/** `@internal` severity levels mapped to Method names for gating logic (not exported; see LOG enum for public API) */
-const Level = {
-	[Method.Error]: 1,
-	[Method.Warn]: 2,
-	[Method.Info]: 3,
-	[Method.Log]: 3,
-	[Method.Debug]: 4,
-	[Method.Trace]: 5,
-} as const;
+/** logging severity levels for Logify output control */
+export const LOG = enumify({
+	Off: 0,
+	Error: 1,
+	Warn: 2,
+	Info: 3,
+	Debug: 4,
+	Trace: 5,
+}, false);
+export type LOG = ValueOf<typeof LOG>
+export type LogLevel = KeyOf<typeof LOG>
+
+/** `@internal` severity levels mapped to Method names for gating logic */
+const Level = {
+	[Method.Error]: LOG.Error,
+	[Method.Warn]:  LOG.Warn,
+	[Method.Info]:  LOG.Info,
+	[Method.Log]:   LOG.Info,
+	[Method.Debug]: LOG.Debug,
+	[Method.Trace]: LOG.Trace,
+} as const;

And remove the duplicate LOG/LogLevel block at lines 106–116.

Also applies to: 107-114

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/src/common/logify.class.ts` around lines 19 - 26, Level and
LOG currently duplicate the same severity mappings (error=1…trace=5), which
risks divergence; move the LOG/LogLevel definition above the Level block and
derive Level from LOG (e.g., map Method.* keys to LOG values) so there's a
single source of truth, then remove the duplicate LOG/LogLevel block later in
the file; update any references to use the canonical LOG/LogLevel symbol and
keep Method, Level, and LogLevel names intact.
packages/tempo/src/plugin/term.util.ts (1)

285-316: Sphere-aware fallback: specificity + deterministic tie-break are aligned with getRange; minor index-safety nit.

The scan-all/longest-sphere-wins logic with localeCompare tie-break is a clear improvement over the previous find(...) behaviour, and the containment direction (targetSphere.includes(sphereLower)) now matches the getRange filter (config-as-more-specific).

Small nit — Line 288: targetParts[sphereIdx].toLowerCase() assumes groupKey.split('.') yields at least sphereIdx + 1 entries. It will, unless any group value (e.g. options[key] / anchor?.[key] / t.config[key]) contains a literal . (e.g. a calendar-like id). In that pathological case targetParts[sphereIdx] can be undefined and .toLowerCase() throws. Defensive fallback:

🛡️ Suggested guard
-			const sphereIdx = groupBy.indexOf('sphere');
-			const targetParts = groupKey.split('.');
-			const targetSphere = targetParts[sphereIdx].toLowerCase();
+			const sphereIdx = groupBy.indexOf('sphere');
+			const targetParts = groupKey.split('.');
+			const targetSphere = (targetParts[sphereIdx] ?? '').toLowerCase();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/plugin/term.util.ts` around lines 285 - 316, The code in
the sphere-aware fallback uses targetParts[sphereIdx].toLowerCase() which can
throw if groupKey.split('.') produced fewer segments; update the guard in the
block inside the if (list.length === 0 && groupBy.includes('sphere')) so
targetSphere is computed safely (e.g. derive targetParts then use a defensive
fallback like (targetParts[sphereIdx] ?? '') or check targetParts.length >
sphereIdx before calling toLowerCase), and ensure subsequent logic that depends
on targetSphere handles the empty-string case (skip matching or continue) so
bestKey selection remains safe; change only the targetSphere extraction in the
block around targetParts/targetSphere in term.util.ts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/library/src/common/proxy.library.ts`:
- Around line 44-59: The getOwnPropertyDescriptor trap currently invokes onGet
for symbol keys while the get trap skips symbols (uses !isSymbol(k)), causing
inconsistent materialization; update getOwnPropertyDescriptor in the proxy
implementation to mirror the get trap's symbol handling by skipping onGet for
symbols (i.e., check isSymbol(k) before calling onGet) so both
getOwnPropertyDescriptor and get behave consistently when keys and onGet are
used together; apply the same alignment to the corresponding logic in the other
proxy block (the one around the get trap at ~lines 95–129).
- Around line 108-120: The sentinel property added during property discovery is
defined non-writable, which prevents future assignments when using
delegate(target, onGet, false); update the Object.defineProperty call inside the
Property Discovery block (where onGet, pending, isSymbol, isDefined are used) to
mark the cached property as writable (set writable: true) so non-frozen
delegates can later accept writes, or alternatively avoid mutating the target by
tracking missing keys in the existing pending/another Set; ensure the descriptor
includes writable: true along with enumerable: false and configurable: true.

In `@packages/tempo/src/tempo.type.ts`:
- Line 15: The import statement in tempo.type.ts has inconsistent double spaces
inside the destructured import; update the import for TermPlugin and Plugin (the
line importing TermPlugin and Plugin from '#tempo/plugin/plugin.type.js') to use
normalized single spacing between the braces and identifiers so it reads with
consistent spacing (e.g., single spaces after '{' and between items).
- Around line 191-193: The type declares non-optional fields matches and anchor
but several call sites create t.Internal.State with empty objects using
assertions; update the model to reflect actual usage by making matches and
anchor optional (matches?: Match[]; anchor?: Temporal.ZonedDateTime) OR ensure
they are always initialized in the init path (e.g., in init() and where new
state objects are created such as the constructors/creators used by tempo.init,
discrete.parse, and tempo.class) and remove the unsafe "as t.Internal.State"
assertions; reference the symbols matches, anchor and the t.Internal.State type
and fix either the type definition or the state creation sites so TypeScript
safety matches runtime guarantees.

---

Nitpick comments:
In `@packages/library/src/common/logify.class.ts`:
- Around line 19-26: Level and LOG currently duplicate the same severity
mappings (error=1…trace=5), which risks divergence; move the LOG/LogLevel
definition above the Level block and derive Level from LOG (e.g., map Method.*
keys to LOG values) so there's a single source of truth, then remove the
duplicate LOG/LogLevel block later in the file; update any references to use the
canonical LOG/LogLevel symbol and keep Method, Level, and LogLevel names intact.

In `@packages/tempo/src/plugin/term.util.ts`:
- Around line 285-316: The code in the sphere-aware fallback uses
targetParts[sphereIdx].toLowerCase() which can throw if groupKey.split('.')
produced fewer segments; update the guard in the block inside the if
(list.length === 0 && groupBy.includes('sphere')) so targetSphere is computed
safely (e.g. derive targetParts then use a defensive fallback like
(targetParts[sphereIdx] ?? '') or check targetParts.length > sphereIdx before
calling toLowerCase), and ensure subsequent logic that depends on targetSphere
handles the empty-string case (skip matching or continue) so bestKey selection
remains safe; change only the targetSphere extraction in the block around
targetParts/targetSphere in term.util.ts.
🪄 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: e8d1fa2e-af8b-461f-84eb-4449d6f82344

📥 Commits

Reviewing files that changed from the base of the PR and between e106407 and c790ac7.

📒 Files selected for processing (11)
  • packages/library/src/common/assertion.library.ts
  • packages/library/src/common/logify.class.ts
  • packages/library/src/common/primitive.library.ts
  • packages/library/src/common/proxy.library.ts
  • packages/library/test/common/pledge.class.test.ts
  • packages/tempo/doc/migration-guide.md
  • packages/tempo/doc/releases/v2.x.md
  • packages/tempo/index.md
  • packages/tempo/src/plugin/term.util.ts
  • packages/tempo/src/support/tempo.init.ts
  • packages/tempo/src/tempo.type.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/tempo/doc/releases/v2.x.md
  • packages/tempo/doc/migration-guide.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/library/src/common/primitive.library.ts
  • packages/tempo/src/support/tempo.init.ts
  • packages/tempo/index.md

Comment thread packages/library/src/common/proxy.library.ts
Comment thread packages/library/src/common/proxy.library.ts
Comment thread packages/tempo/src/tempo.type.ts Outdated
Comment thread packages/tempo/src/tempo.type.ts Outdated
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: 5

Caution

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

⚠️ Outside diff range comments (1)
packages/library/src/common/logify.class.ts (1)

45-80: ⚠️ Potential issue | 🟡 Minor

Trace output is hidden even with debug: true.

With debug: truecurrentLevel = LOG.Debug (4) and Method.Trace → LOG.Trace (5), so trace() calls never emit unless the caller passes a numeric debug >= 5 (or LOG.Trace). That's consistent with syslog semantics, but since the JSDoc on Logify.Constructor.debug (Lines 115-125) only documents true → LOG.Debug, consumers who enable debug will silently lose their trace() output.

Consider adding a brief note to the JSDoc that enabling trace() requires a numeric debug of LOG.Trace, or map true to LOG.Trace if that was the intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/src/common/logify.class.ts` around lines 45 - 80, The trap
currently maps config.debug === true to LOG.Debug, which hides Method.Trace
output; change the mapping in Logify.#trap so that (config.debug === true) maps
to LOG.Trace instead of LOG.Debug (i.e., set currentLevel = typeof config.debug
=== 'number' ? config.debug : (config.debug === true ? LOG.Trace : LOG.Info)),
and update the Logify.Constructor.debug JSDoc to state that debug: true enables
trace-level logging (or alternately document that numeric values control exact
levels) so callers understand trace behavior; reference symbols: `#trap`,
config.debug, Method.Trace, LOG.Debug, LOG.Trace, and Logify.Constructor.debug.
♻️ Duplicate comments (1)
packages/tempo/doc/tempo.modularity.md (1)

112-112: ⚠️ Potential issue | 🟠 Major

Documentation incorrectly suggests re-calling Tempo.init() to pick up late imports.

Line 112 states: "You would need to call Tempo.init() again or use Tempo.extend() to pick up the latecomers." However, as noted in the earlier comment on Line 11, the implementation returns early when state already exists, preventing re-initialization from picking up late imports.

The correct guidance should be to exclusively use Tempo.extend() for late-loaded modules, as init() will not re-process them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/doc/tempo.modularity.md` at line 112, Update the documentation
text that currently advises calling Tempo.init() again to pick up late-imported
side-effect plugins: clarify that because Tempo.init() short-circuits when state
already exists (see the init early-return behavior), re-calling init will not
load late modules; instead instruct users to use Tempo.extend() exclusively for
late-loaded plugins and remove the suggestion to re-run Tempo.init().
🧹 Nitpick comments (7)
packages/tempo/doc/tempo.config.md (1)

115-115: Clarify that plugins option is processed differently from core init options.

Line 115 documents plugins as a standard Tempo.init() option. However, the implementation shows that plugins is handled in packages/tempo/src/tempo.class.ts:411-413 via this.extend(p) rather than in the core extendState function (packages/tempo/src/support/tempo.init.ts:100-173) where other options like snippet, layout, event, period, etc. are processed.

While this is still a valid init option from a user perspective, it follows a different code path. Consider adding a note clarifying that plugins are extended onto the class rather than merged into the internal state, which distinguishes them from other configuration options.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/doc/tempo.config.md` at line 115, Update the plugins entry to
note that the `plugins` option is processed differently from core init options:
instead of being merged into the internal state via `extendState`, each plugin
is applied to the Tempo instance via `this.extend(p)` (see `Tempo.init()` flow
and the `extend` call), so plugins are extended onto the class instance rather
than merged into the internal state like `snippet`, `layout`, `event`, `period`,
etc.; add a short sentence describing this behavior and its implication for
plugin authors (they augment the instance, not the internal state).
packages/tempo/doc/architecture.md (1)

22-22: Consider varying sentence structure for better readability.

The explanation contains three consecutive sentences beginning with "Scoped runtimes," which makes the text somewhat repetitive. Consider rephrasing to improve flow.

✍️ Suggested rewrite
-- **Scoped runtimes (Experimental)** — `TempoRuntime.createScoped()` returns a fresh, isolated runtime that is *not* stored on `globalThis`, enabling clean test isolation without globalThis manipulation. Scoped runtimes are currently an experimental internal feature and are not yet fully threaded through all core utilities. Scoped runtimes are not pinned to `globalThis`, lack the `defineProperty` descriptor protections of the primary instance, and instead rely solely on the lexical reference returned (contrasting with the hardened `getRuntime()` and `globalThis[BRIDGE]` behavior). Implementation examples of this test-scoping pattern can be found in [plugin_registration.test.ts](../test/plugin_registration.test.ts) and [duration.core.test.ts](../test/duration.core.test.ts).
+- **Scoped runtimes (Experimental)** — `TempoRuntime.createScoped()` returns a fresh, isolated runtime that is *not* stored on `globalThis`, enabling clean test isolation without globalThis manipulation. Currently an experimental internal feature, they are not yet fully threaded through all core utilities. Unlike the primary instance, they are not pinned to `globalThis`, lack the `defineProperty` descriptor protections, and instead rely solely on the lexical reference returned (contrasting with the hardened `getRuntime()` and `globalThis[BRIDGE]` behavior). Implementation examples of this test-scoping pattern can be found in [plugin_registration.test.ts](../test/plugin_registration.test.ts) and [duration.core.test.ts](../test/duration.core.test.ts).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/doc/architecture.md` at line 22, The paragraph describing
scoped runtimes is repetitive because three sentences begin with "Scoped
runtimes"; reword it to vary sentence structure and improve flow by starting
with the API name and effects (e.g., "TempoRuntime.createScoped() returns a
fresh, isolated runtime..."), follow with a separate sentence noting
experimental/internal status, then explain differences from the primary runtime
(not pinned to globalThis, lacking defineProperty protections and relying on the
lexical reference) and finish with the pointer to the example tests
(plugin_registration.test.ts and duration.core.test.ts); update the block that
mentions TempoRuntime.createScoped(), globalThis, defineProperty, getRuntime(),
and [BRIDGE] accordingly to reflect the new sentence order and reduce
repetition.
packages/library/src/common/proxy.library.ts (1)

122-127: Consider caching bound functions to avoid per-access allocation and identity breakage.

val.bind(t) runs on every property read when bind is enabled (all proxify(frozen=true) and secure(...) consumers), which allocates a new function each time and breaks reference equality (e.g., proxy.method === proxy.method yields false, which can subtly invalidate memoization, event-listener add/remove pairing, and React dependency arrays).

A small WeakMap<Function, Function> keyed by val gives a stable wrapper with negligible overhead.

♻️ Sketch
 function factory<T extends object>(target: T, options: ProxyOptions = {}): T {
 	const { frozen, lock, appendOnly, onGet, keys, bind, skip } = options;
 	const pending = new Set<PropertyKey>();
+	const boundCache = bind ? new WeakMap<Function, Function>() : undefined;
 	let cachedJSON: any;
 ...
 			const val = Reflect.get(t, k, r);
 			if (bind && isFunction(val)) {
 				const desc = Object.getOwnPropertyDescriptor(t, k);
 				if (desc && !desc.configurable && !desc.writable) return val;
-				return val.bind(t);
+				let bound = boundCache!.get(val);
+				if (!bound) { bound = val.bind(t); boundCache!.set(val, bound); }
+				return bound;
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/src/common/proxy.library.ts` around lines 122 - 127, The
property getter currently calls val.bind(t) on every access (see Reflect.get
usage and the bind branch) which allocates a new function and breaks identity;
fix by adding a WeakMap<Function, Function> cache keyed by the original val to
return a stable bound wrapper instead of calling val.bind(t) each time (create
the cache in the module scope or in proxify/secure initialization, use it when
bind && isFunction(val) to return cachedBound || (cachedBound = val.bind(t)) and
store it). Ensure you still return val for non-configurable/non-writable
descriptors as currently handled.
packages/tempo/src/tempo.type.ts (1)

199-204: Match.source is free-form — consider a typed literal set.

The JSDoc hints at 'default' | 'local' | 'plugin:X', and discrete.parse.ts Line 455 currently emits only 'global' or 'local'. A string-literal union (e.g., 'default' | 'global' | 'local' | \plugin:${string}``) would prevent drift between producer and consumer, and make sandbox-traceability assertions in tests self-documenting.

♻️ Suggested typing
-		/** where this match came from (e.g. 'default', 'local', 'plugin:X') */ source?: string;
+		/** where this match came from */
+		source?: 'default' | 'global' | 'local' | `plugin:${string}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/tempo.type.ts` around lines 199 - 204, The Match.type
currently declares source?: string which is free-form; change it to a
string-literal union to lock producers/consumers together (e.g., 'default' |
'global' | 'local' | `plugin:${string}`) and update the Match JSDoc accordingly
so callers and tests (and the producer in discrete.parse.ts) use the same
allowed set; locate the Match type declaration and replace the source property
type with the literal union and adjust any imports/types if needed.
packages/tempo/src/discrete/discrete.parse.ts (2)

133-135: isAnchoredVal is equivalent to isAnchored.

At Line 67 isAnchored = isDefined(val) where val = dateTime ?? state.anchor ?? …. By the time we reach Line 133, nothing between has mutated state.anchor or dateTime, so isAnchored || isDefined(dateTime) || isDefined(state.anchor) collapses to isAnchored. Safe to drop the alias.

♻️ Suggested simplification
-			const isAnchoredVal = isAnchored || isDefined(dateTime) || isDefined(state.anchor);
 			const resolvingKeys = new Set<string>();
-			const res = _ParseEngine.conform(state, tempo, today, isAnchoredVal, resolvingKeys);
+			const res = _ParseEngine.conform(state, tempo, today, isAnchored, resolvingKeys);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/discrete/discrete.parse.ts` around lines 133 - 135, The
variable isAnchoredVal is redundant because isAnchored already equals
isDefined(dateTime ?? state.anchor ?? …); remove isAnchoredVal and pass
isAnchored directly to _ParseEngine.conform (update the call site where
isAnchoredVal is used and delete its declaration), keeping the rest of the logic
and resolvingKeys unchanged; specifically edit the block that defines
isAnchoredVal and calls _ParseEngine.conform to use isAnchored instead.

451-469: Reuse isFn instead of recomputing isFunction(definition).

isFn is already computed at Line 397 for the same definition. Minor consistency nit.

♻️ Suggested
-					const resolveVal = isFunction(definition) ? res : definition;
+					const resolveVal = isFn ? res : definition;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/discrete/discrete.parse.ts` around lines 451 - 469, The
code recomputes isFunction(definition) when isFn was already determined earlier;
update the assignment of resolveVal to use the existing isFn variable
(resolveVal = isFn ? res : definition) inside the try block where type, pat,
resolveVal, source are set so you reuse the precomputed predicate and maintain
consistency with the earlier computation of isFn for definition.
packages/tempo/package.json (1)

25-39: sideEffects globs are broad — double-check they don't over-include.

**/engine.*.js, **/module.*.js, and **/discrete.*.js match any file named engine.foo.js, module.foo.js, or discrete.foo.js anywhere in the package (e.g., in test/ or nested bundles). Given that files whitelists only dist/ and img/, that's fine in practice, but future-you may add other engine.*/module.* utility files that are pure and should be tree-shaken. Consider scoping these to the directories that actually host side-effect modules, e.g. **/engine/engine.*.js, **/module/module.*.js, **/discrete/discrete.*.js.

♻️ Suggested tightening
-    "**/plugin/extend/extend.*.js",
-    "**/plugin/extend/extend.*.ts",
-    "**/engine.*.js",
-    "**/engine.*.ts",
-    "**/module.*.js",
-    "**/module.*.ts",
-    "**/discrete.*.js",
-    "**/discrete.*.ts"
+    "**/plugin/extend/extend.*.js",
+    "**/plugin/extend/extend.*.ts",
+    "**/engine/engine.*.js",
+    "**/engine/engine.*.ts",
+    "**/module/module.*.js",
+    "**/module/module.*.ts",
+    "**/discrete/discrete.*.js",
+    "**/discrete/discrete.*.ts"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/package.json` around lines 25 - 39, The sideEffects array
contains broad globs ("**/engine.*.js", "**/module.*.js", "**/discrete.*.js" and
their .ts variants) that may over-include unrelated files; narrow them to the
actual directories that contain side-effectful modules by updating those entries
in package.json's sideEffects to target the specific folders that host those
runtime modules (e.g., scope to engine/, module/, discrete/ subfolders or the
dist/ layout you publish), and update the matching .ts entries as well; keep the
existing files whitelist behavior (dist/ and img/) in mind and run a quick
packaging/build smoke test to confirm nothing intended for side-effects is
accidentally excluded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/library/src/common/proxy.library.ts`:
- Around line 157-160: delegator currently returns a writable proxy so
assignments like proxy[key] = value mutate the empty target and break future
delegation; fix this by making the proxy read-only when calling factory inside
delegator: pass the appropriate immutability flag (e.g., frozen: true) in the
second argument to factory (alongside keys: keyList and onGet: fn) so writes
cannot overwrite the virtual forwarding behavior of delegator.

In `@packages/tempo/doc/tempo.modularity.md`:
- Line 11: The docs claim Tempo.init() will pick up side-effect modules if
called again but the implementation in tempo.init.ts short-circuits when
runtime.state exists (the if (isGlobal && runtime.state && !baseState) return
runtime.state;), so reconcile by choosing one approach: either remove/adjust
that early-return in Tempo.init (and ensure re-init re-processes imports and
merges state) or update documentation to state re-calling Tempo.init() will NOT
discover new side-effect modules and instruct users to call Tempo.extend() to
add modules; update tempo.modularity.md to remove the misleading line about
re-calling init and add clear guidance to use Tempo.extend(), and add a comment
near the runtime.state check in tempo.init.ts explaining the chosen behavior.

In `@packages/tempo/doc/tempo.plugin.md`:
- Around line 77-79: Remove or populate the empty admonition block in the
Markdown: locate the standalone "::: warning" / closing "::: " callout in
tempo.plugin.md and either delete both the opening and closing markers or add
the intended warning text between them so the warning box is not rendered empty.

In `@packages/tempo/doc/tempo.shorthand.md`:
- Around line 89-97: Update the Docusaurus admonitions to use the correct syntax
by replacing both occurrences of "::: warning" with ":::warning" so the blocks
render as intended; then reconcile the reserved-characters list for Range-Keys
by either adding the hyphen character "-" to the listed reserved characters (so
the Range-Keys warning matches the documented Navigation Modifier "Relative
Past" / "-" alias for "<") or add a short sentence next to the Range-Keys
section explaining why "-" is not reserved when used in range-keys, referencing
the "Range-Keys" heading and the "Relative Past" (alias "<") / "-" modifier.

In `@packages/tempo/src/discrete/discrete.parse.ts`:
- Around line 427-446: The HH:MM check using /^\d{1,2}:\d{2}$/ should be
tightened to only accept valid hours and minutes (e.g. hours 00–23 and minutes
00–59) before calling dateTime.with({ hour, minute, ... })—update the regex used
in the isString(result) branch to enforce that range so invalid times like
"25:70" are treated as non-matches and fall through to the stringify branch;
also remove the redundant try/catch that simply rethrows (the catch block around
the handler-return logic that does "catch (e: any) { throw e; }"), letting
exceptions propagate naturally while keeping assignment to state.zdt and the
other branches (isTempo, isZonedDateTime, object with toDateTime) intact.

---

Outside diff comments:
In `@packages/library/src/common/logify.class.ts`:
- Around line 45-80: The trap currently maps config.debug === true to LOG.Debug,
which hides Method.Trace output; change the mapping in Logify.#trap so that
(config.debug === true) maps to LOG.Trace instead of LOG.Debug (i.e., set
currentLevel = typeof config.debug === 'number' ? config.debug : (config.debug
=== true ? LOG.Trace : LOG.Info)), and update the Logify.Constructor.debug JSDoc
to state that debug: true enables trace-level logging (or alternately document
that numeric values control exact levels) so callers understand trace behavior;
reference symbols: `#trap`, config.debug, Method.Trace, LOG.Debug, LOG.Trace, and
Logify.Constructor.debug.

---

Duplicate comments:
In `@packages/tempo/doc/tempo.modularity.md`:
- Line 112: Update the documentation text that currently advises calling
Tempo.init() again to pick up late-imported side-effect plugins: clarify that
because Tempo.init() short-circuits when state already exists (see the init
early-return behavior), re-calling init will not load late modules; instead
instruct users to use Tempo.extend() exclusively for late-loaded plugins and
remove the suggestion to re-run Tempo.init().

---

Nitpick comments:
In `@packages/library/src/common/proxy.library.ts`:
- Around line 122-127: The property getter currently calls val.bind(t) on every
access (see Reflect.get usage and the bind branch) which allocates a new
function and breaks identity; fix by adding a WeakMap<Function, Function> cache
keyed by the original val to return a stable bound wrapper instead of calling
val.bind(t) each time (create the cache in the module scope or in proxify/secure
initialization, use it when bind && isFunction(val) to return cachedBound ||
(cachedBound = val.bind(t)) and store it). Ensure you still return val for
non-configurable/non-writable descriptors as currently handled.

In `@packages/tempo/doc/architecture.md`:
- Line 22: The paragraph describing scoped runtimes is repetitive because three
sentences begin with "Scoped runtimes"; reword it to vary sentence structure and
improve flow by starting with the API name and effects (e.g.,
"TempoRuntime.createScoped() returns a fresh, isolated runtime..."), follow with
a separate sentence noting experimental/internal status, then explain
differences from the primary runtime (not pinned to globalThis, lacking
defineProperty protections and relying on the lexical reference) and finish with
the pointer to the example tests (plugin_registration.test.ts and
duration.core.test.ts); update the block that mentions
TempoRuntime.createScoped(), globalThis, defineProperty, getRuntime(), and
[BRIDGE] accordingly to reflect the new sentence order and reduce repetition.

In `@packages/tempo/doc/tempo.config.md`:
- Line 115: Update the plugins entry to note that the `plugins` option is
processed differently from core init options: instead of being merged into the
internal state via `extendState`, each plugin is applied to the Tempo instance
via `this.extend(p)` (see `Tempo.init()` flow and the `extend` call), so plugins
are extended onto the class instance rather than merged into the internal state
like `snippet`, `layout`, `event`, `period`, etc.; add a short sentence
describing this behavior and its implication for plugin authors (they augment
the instance, not the internal state).

In `@packages/tempo/package.json`:
- Around line 25-39: The sideEffects array contains broad globs
("**/engine.*.js", "**/module.*.js", "**/discrete.*.js" and their .ts variants)
that may over-include unrelated files; narrow them to the actual directories
that contain side-effectful modules by updating those entries in package.json's
sideEffects to target the specific folders that host those runtime modules
(e.g., scope to engine/, module/, discrete/ subfolders or the dist/ layout you
publish), and update the matching .ts entries as well; keep the existing files
whitelist behavior (dist/ and img/) in mind and run a quick packaging/build
smoke test to confirm nothing intended for side-effects is accidentally
excluded.

In `@packages/tempo/src/discrete/discrete.parse.ts`:
- Around line 133-135: The variable isAnchoredVal is redundant because
isAnchored already equals isDefined(dateTime ?? state.anchor ?? …); remove
isAnchoredVal and pass isAnchored directly to _ParseEngine.conform (update the
call site where isAnchoredVal is used and delete its declaration), keeping the
rest of the logic and resolvingKeys unchanged; specifically edit the block that
defines isAnchoredVal and calls _ParseEngine.conform to use isAnchored instead.
- Around line 451-469: The code recomputes isFunction(definition) when isFn was
already determined earlier; update the assignment of resolveVal to use the
existing isFn variable (resolveVal = isFn ? res : definition) inside the try
block where type, pat, resolveVal, source are set so you reuse the precomputed
predicate and maintain consistency with the earlier computation of isFn for
definition.

In `@packages/tempo/src/tempo.type.ts`:
- Around line 199-204: The Match.type currently declares source?: string which
is free-form; change it to a string-literal union to lock producers/consumers
together (e.g., 'default' | 'global' | 'local' | `plugin:${string}`) and update
the Match JSDoc accordingly so callers and tests (and the producer in
discrete.parse.ts) use the same allowed set; locate the Match type declaration
and replace the source property type with the literal union and adjust any
imports/types if needed.
🪄 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: a11838de-47d4-4278-b8b1-d91efbe710c4

📥 Commits

Reviewing files that changed from the base of the PR and between c790ac7 and d0c549c.

📒 Files selected for processing (23)
  • packages/library/src/common/logify.class.ts
  • packages/library/src/common/proxy.library.ts
  • packages/tempo/doc/Tempo.md
  • packages/tempo/doc/architecture.md
  • packages/tempo/doc/commercial.md
  • packages/tempo/doc/installation.md
  • packages/tempo/doc/soft_freeze_strategy.md
  • packages/tempo/doc/tempo.api.md
  • packages/tempo/doc/tempo.benchmarks.md
  • packages/tempo/doc/tempo.config.md
  • packages/tempo/doc/tempo.cookbook.md
  • packages/tempo/doc/tempo.enumerators.md
  • packages/tempo/doc/tempo.format.md
  • packages/tempo/doc/tempo.modularity.md
  • packages/tempo/doc/tempo.parse.md
  • packages/tempo/doc/tempo.plugin.md
  • packages/tempo/doc/tempo.shorthand.md
  • packages/tempo/doc/tempo.term.md
  • packages/tempo/doc/tempo.ticker.md
  • packages/tempo/package.json
  • packages/tempo/src/discrete/discrete.parse.ts
  • packages/tempo/src/plugin/term.util.ts
  • packages/tempo/src/tempo.type.ts
💤 Files with no reviewable changes (1)
  • packages/tempo/doc/Tempo.md
✅ Files skipped from review due to trivial changes (8)
  • packages/tempo/doc/commercial.md
  • packages/tempo/doc/tempo.benchmarks.md
  • packages/tempo/doc/tempo.api.md
  • packages/tempo/doc/soft_freeze_strategy.md
  • packages/tempo/doc/tempo.format.md
  • packages/tempo/doc/tempo.parse.md
  • packages/tempo/doc/tempo.enumerators.md
  • packages/tempo/doc/installation.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/tempo/doc/tempo.cookbook.md

Comment thread packages/library/src/common/proxy.library.ts
Comment thread packages/tempo/doc/tempo.modularity.md Outdated
Comment thread packages/tempo/doc/tempo.plugin.md Outdated
Comment thread packages/tempo/doc/tempo.shorthand.md Outdated
Comment thread packages/tempo/src/discrete/discrete.parse.ts Outdated
Copy link
Copy Markdown
Owner Author

@magmacomputing magmacomputing left a comment

Choose a reason for hiding this comment

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

Code meets Project standards, and test have 100% pass rate.

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: 4

🧹 Nitpick comments (4)
packages/library/test/common/proxy.library.test.ts (1)

1-1: Consider adding a delegate test for parity with the AI summary / module surface.

The PR summary advertises coverage of delegate, but only proxify and delegator are imported here. Since delegate shares the same factory plumbing but with a different surface (callback-driven discovery on a real target, optional readonly), a small smoke test for it would round out the suite and protect against future regressions in the onGet pathway when keys are not fixed.

-import { delegator, proxify } from '#library/proxy.library.js';
+import { delegate, delegator, proxify } from '#library/proxy.library.js';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/test/common/proxy.library.test.ts` at line 1, Add a small
smoke test that imports delegate from '#library/proxy.library.js' alongside
proxify/delegator and exercises the callback-driven discovery path: create a
plain target object (e.g. {foo: 'bar'}), call delegate(target,
discoveryCallback, { readonly: true/false } as needed) where discoveryCallback
is invoked on property access and returns the underlying value, then assert that
reading proxy.foo yields 'bar' and that readonly mode blocks writes (or allows
writes when not readonly). Place the new test next to existing proxify/delegator
tests and reference the delegate symbol so future changes to the onGet pathway
are covered.
packages/tempo/doc/tempo.config.md (1)

126-128: Note numeric 0 (LOG.Off) also silences error logs.

While this info block correctly clarifies that string labels aren't supported, consider also mentioning the subtle trap where debug: 0 silences console.error output (the error is still thrown when catch: false, but nothing is logged). This matches the warning already present in Logify.Constructor.debug's JSDoc and will save users a debugging detour.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/doc/tempo.config.md` around lines 126 - 128, Add a short
sentence to the info block explaining that setting debug to numeric 0 (LOG.Off)
will also silence console.error messages (errors are still thrown when catch:
false but won’t be logged), and reference the existing JSDoc identifier
Logify.Constructor.debug to make the behavior consistent with its warning; keep
the note concise and mention LOG.Off so users recognize the numeric value.
packages/tempo/src/support/tempo.init.ts (2)

49-62: Redundant getDateTimeFormat() call.

Line 51 re-invokes getDateTimeFormat() and re-destructures { timeZone, calendar } which are already computed and in scope from Line 24. Remove the duplicate call; locale can still be fetched on Line 55 if needed, or pulled from the same initial call.

♻️ Proposed fix
 	if (isGlobal) {
 		markConfig(Object.assign(state.config, Default));
-		const { timeZone, calendar } = getDateTimeFormat();
 		Object.defineProperties(state.config, {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/support/tempo.init.ts` around lines 49 - 62, The code
makes a redundant call to getDateTimeFormat() inside the Object.defineProperties
block; reuse the previously computed { timeZone, calendar, locale } (from the
earlier getDateTimeFormat() call) instead of calling getDateTimeFormat()
again—remove the duplicate invocation, reference the existing timeZone and
calendar variables for their properties, and read locale from the same earlier
result (or from the already-in-scope variable) when defining the locale property
on state.config; keep the other properties (discovery, formats, sphere, get,
scope, catch) unchanged.

49-90: Consider consolidating the three config branches.

Global (49–62) and local-without-baseState (76–90) are nearly identical except for scope and the conditional catch. The baseState branch (63–75) duplicates the list of defined property names. Extracting a shared descriptor builder would reduce drift risk (e.g., adding a new config field today requires editing three places and failing to keep them in sync is exactly what the past review flagged about shape differences across branches).

No behavior change intended; purely a maintainability suggestion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/support/tempo.init.ts` around lines 49 - 90, The three
branches in tempo.init.ts duplicate Object.defineProperties for state.config
(seen around markConfig, Object.defineProperties, and setProperty) — extract a
shared descriptor builder function (e.g., buildConfigDescriptors or
getConfigDescriptors) that accepts overrides (scope value and whether to
include/set 'catch') and uses getDateTimeFormat(), enumify(STATE.FORMAT),
getHemisphere(timeZone), Symbol.keyFor(sym.$Tempo), etc., then call markConfig
and Object.defineProperties(state.config, buildConfigDescriptors(...)) in each
branch, and only apply setProperty(state.config, 'catch', options.catch) or a
different scope ('global' vs 'local') via the overrides so behavior is unchanged
but the descriptor list is defined once.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/library/src/common/logify.class.ts`:
- Around line 47-50: The tempo.config.md doc incorrectly states that debug: true
maps to LOG.Debug; update that doc to match the implementation in Logify where
config.debug === true maps to LOG.Trace (refer to the Logify class and the
config.debug runtime mapping that uses LOG.Trace), ensuring the doc text and any
examples mention LOG.Trace instead of LOG.Debug so docs and runtime are
consistent.

In `@packages/library/test/common/proxy.library.test.ts`:
- Line 52: The test passes a readonly tuple literal to delegator (proxy test
uses delegator(['alpha'] as const, key => `${key}-value`) cast to
Record<'alpha', string>) which fails under strict types because delegator's
first parameter is typed as K[] | Record<K, any>; update the delegator signature
in proxy.library.ts (the delegator function) to accept readonly K[] (e.g.,
change K[] to readonly K[]) or alternatively remove the as const in the test so
a mutable array is passed; pick one fix and update the corresponding declaration
or test accordingly so the readonly array is assignable.

In `@packages/tempo/doc/tempo.config.md`:
- Line 118: The docs table entry for the `debug` option is wrong: update the
description in tempo.config.md so it reflects Logify's actual behavior by
stating that `debug: true` maps to `LOG.Trace` (per the Logify implementation
and JSDoc in Logify class) and that numeric values map directly to `LOG` levels;
reference the `Logify` symbol and `LOG.Trace` when making the change so readers
aren't misled.

In `@packages/tempo/src/support/tempo.init.ts`:
- Around line 63-75: In the baseState sandbox branch, avoid unconditionally
calling setProperty(state.config, 'catch', options.catch) because assigning
undefined will shadow baseState.config.catch; update the else if (baseState)
branch to only call setProperty for 'catch' when options.catch is defined (e.g.,
wrap with isDefined(options.catch) or options.catch !== undefined) so the
sandbox inherits the parent's catch handler unless explicitly overridden.

---

Nitpick comments:
In `@packages/library/test/common/proxy.library.test.ts`:
- Line 1: Add a small smoke test that imports delegate from
'#library/proxy.library.js' alongside proxify/delegator and exercises the
callback-driven discovery path: create a plain target object (e.g. {foo:
'bar'}), call delegate(target, discoveryCallback, { readonly: true/false } as
needed) where discoveryCallback is invoked on property access and returns the
underlying value, then assert that reading proxy.foo yields 'bar' and that
readonly mode blocks writes (or allows writes when not readonly). Place the new
test next to existing proxify/delegator tests and reference the delegate symbol
so future changes to the onGet pathway are covered.

In `@packages/tempo/doc/tempo.config.md`:
- Around line 126-128: Add a short sentence to the info block explaining that
setting debug to numeric 0 (LOG.Off) will also silence console.error messages
(errors are still thrown when catch: false but won’t be logged), and reference
the existing JSDoc identifier Logify.Constructor.debug to make the behavior
consistent with its warning; keep the note concise and mention LOG.Off so users
recognize the numeric value.

In `@packages/tempo/src/support/tempo.init.ts`:
- Around line 49-62: The code makes a redundant call to getDateTimeFormat()
inside the Object.defineProperties block; reuse the previously computed {
timeZone, calendar, locale } (from the earlier getDateTimeFormat() call) instead
of calling getDateTimeFormat() again—remove the duplicate invocation, reference
the existing timeZone and calendar variables for their properties, and read
locale from the same earlier result (or from the already-in-scope variable) when
defining the locale property on state.config; keep the other properties
(discovery, formats, sphere, get, scope, catch) unchanged.
- Around line 49-90: The three branches in tempo.init.ts duplicate
Object.defineProperties for state.config (seen around markConfig,
Object.defineProperties, and setProperty) — extract a shared descriptor builder
function (e.g., buildConfigDescriptors or getConfigDescriptors) that accepts
overrides (scope value and whether to include/set 'catch') and uses
getDateTimeFormat(), enumify(STATE.FORMAT), getHemisphere(timeZone),
Symbol.keyFor(sym.$Tempo), etc., then call markConfig and
Object.defineProperties(state.config, buildConfigDescriptors(...)) in each
branch, and only apply setProperty(state.config, 'catch', options.catch) or a
different scope ('global' vs 'local') via the overrides so behavior is unchanged
but the descriptor list is defined once.
🪄 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: c7e7ca34-1526-4517-ba5f-b2a30d9f8bb4

📥 Commits

Reviewing files that changed from the base of the PR and between d0c549c and 09e282d.

📒 Files selected for processing (13)
  • packages/library/src/common/logify.class.ts
  • packages/library/src/common/proxy.library.ts
  • packages/library/test/common/logify.class.test.ts
  • packages/library/test/common/proxy.library.test.ts
  • packages/tempo/doc/architecture.md
  • packages/tempo/doc/tempo.config.md
  • packages/tempo/doc/tempo.modularity.md
  • packages/tempo/doc/tempo.plugin.md
  • packages/tempo/doc/tempo.shorthand.md
  • packages/tempo/package.json
  • packages/tempo/src/discrete/discrete.parse.ts
  • packages/tempo/src/support/tempo.init.ts
  • packages/tempo/src/tempo.type.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/tempo/doc/tempo.plugin.md
  • packages/library/test/common/logify.class.test.ts
  • packages/tempo/doc/architecture.md
  • packages/tempo/doc/tempo.shorthand.md
  • packages/library/src/common/proxy.library.ts

Comment on lines +47 to +50
const currentLevel = (typeof config.debug === 'number')
? config.debug
: (config.debug === true ? LOG.Trace : LOG.Info);
const methodLevel = Level[method] ?? 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Doc mismatch between Logify and tempo.config.md regarding debug: true mapping.

The JSDoc here (Line 117) and the runtime mapping (Line 49) both say debug === true maps to LOG.Trace. However, packages/tempo/doc/tempo.config.md Line 118 states true maps to LOG.Debug. One of the docs is wrong — the implementation is the source of truth, so the Tempo config doc needs updating (flagged in that file).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/src/common/logify.class.ts` around lines 47 - 50, The
tempo.config.md doc incorrectly states that debug: true maps to LOG.Debug;
update that doc to match the implementation in Logify where config.debug ===
true maps to LOG.Trace (refer to the Logify class and the config.debug runtime
mapping that uses LOG.Trace), ensuring the doc text and any examples mention
LOG.Trace instead of LOG.Debug so docs and runtime are consistent.


describe('delegator', () => {
test('does not allow writes to overwrite delegated values', () => {
const proxy = delegator(['alpha'] as const, key => `${key}-value`) as Record<'alpha', string>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm delegator's parameter typing in the source file.
fd -t f 'proxy.library.ts' packages/library/src
rg -nP -C2 '\bexport function delegator\b' --type=ts
# Check tsconfig strictness applied to tests.
fd -t f 'tsconfig*.json' | xargs -I{} sh -c 'echo "=== {} ==="; cat "{}"'

Repository: magmacomputing/magma

Length of output: 8326


🏁 Script executed:

cat packages/library/test/common/proxy.library.test.ts | head -60 | tail -20

Repository: magmacomputing/magma

Length of output: 688


🏁 Script executed:

cat tsconfig.base.json

Repository: magmacomputing/magma

Length of output: 10657


🏁 Script executed:

rg -n 'delegator\(\[' packages/library/test/common/proxy.library.test.ts -A 2 -B 2

Repository: magmacomputing/magma

Length of output: 319


🏁 Script executed:

# Try to understand the full context around the delegator call
cat packages/library/test/common/proxy.library.test.ts | sed -n '45,65p'

Repository: magmacomputing/magma

Length of output: 550


Fix TypeScript type incompatibility: readonly array to mutable parameter

Line 52 passes ['alpha'] as const (a readonly ['alpha']) to the delegator function's first parameter, which is typed as K[] | Record<K, any> (mutable array). Under strict TypeScript settings ("strict": true in tsconfig.base.json), readonly arrays are not assignable to mutable array parameters.

Either widen the parameter to readonly K[] in packages/library/src/common/proxy.library.ts (line 169), or remove as const from the test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/library/test/common/proxy.library.test.ts` at line 52, The test
passes a readonly tuple literal to delegator (proxy test uses
delegator(['alpha'] as const, key => `${key}-value`) cast to Record<'alpha',
string>) which fails under strict types because delegator's first parameter is
typed as K[] | Record<K, any>; update the delegator signature in
proxy.library.ts (the delegator function) to accept readonly K[] (e.g., change
K[] to readonly K[]) or alternatively remove the as const in the test so a
mutable array is passed; pick one fix and update the corresponding declaration
or test accordingly so the readonly array is assignable.

Comment thread packages/tempo/doc/tempo.config.md
Comment on lines +63 to +75
} else if (baseState) {
state.config = markConfig(Object.create(baseState.config));
Object.defineProperties(state.config, {
calendar: { value: (state.config as any).calendar, enumerable: true, writable: true, configurable: true },
timeZone: { value: (state.config as any).timeZone, enumerable: true, writable: true, configurable: true },
locale: { value: (state.config as any).locale, enumerable: true, writable: true, configurable: true },
discovery: { value: (state.config as any).discovery, enumerable: true, writable: true, configurable: true },
formats: { value: (state.config as any).formats, enumerable: true, writable: true, configurable: true },
sphere: { value: (state.config as any).sphere, enumerable: true, writable: true, configurable: true },
get: { value: (state.config as any).get, enumerable: false, writable: true, configurable: true },
scope: { value: 'local', enumerable: true, writable: true, configurable: true },
});
setProperty(state.config, 'catch', options.catch);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unconditional catch write in the sandbox branch breaks inheritance from baseState.

In the baseState branch, Line 75 calls setProperty(state.config, 'catch', options.catch) unconditionally. When a sandbox is created without an explicit options.catch, this assigns undefined as an own property and shadows the prototype-inherited value from baseState.config.catch. Contrast with the fallback branch (Lines 88–89), which gates the same call with isDefined(options.catch).

This means Tempo.create() sandboxes silently lose the parent's catch setting even when the caller didn't ask to change it.

🛠️ Proposed fix
-		setProperty(state.config, 'catch', options.catch);
+		if (isDefined(options.catch))
+			setProperty(state.config, 'catch', options.catch);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tempo/src/support/tempo.init.ts` around lines 63 - 75, In the
baseState sandbox branch, avoid unconditionally calling
setProperty(state.config, 'catch', options.catch) because assigning undefined
will shadow baseState.config.catch; update the else if (baseState) branch to
only call setProperty for 'catch' when options.catch is defined (e.g., wrap with
isDefined(options.catch) or options.catch !== undefined) so the sandbox inherits
the parent's catch handler unless explicitly overridden.

@magmacomputing
Copy link
Copy Markdown
Owner Author

prepare Tempo for future features (multiple layout paths)

@magmacomputing magmacomputing merged commit 2503ba0 into main Apr 25, 2026
1 check was pending
@magmacomputing magmacomputing deleted the code-standards branch April 25, 2026 03:13
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