Skip to content

fix(core): throw RangeError on unparseable evaluation instant (#1166) - #1176

Merged
plur9 merged 1 commit into
plur-ai:mainfrom
amasen02:fix/evaluation-instant-unparseable-now-1166
Sep 11, 2026
Merged

plur9 merged 1 commit into
plur-ai:mainfrom
amasen02:fix/evaluation-instant-unparseable-now-1166

Conversation

@amasen02

@amasen02 amasen02 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Resolves #1166.

In packages/core/src/validity.ts, evaluationInstant(now) previously fell back to Date.now() when closesAt(now) returned null:

const ms = closesAt(now)
return ms === null ? Date.now() : ms

As noted in #1166, while stored engram bounds (isExpired, isNotYetValid) treat unparseable timestamps as absent to avoid hiding valid content over data errors, a caller-provided evaluation instant (now in options.now across getCandidatePairsDetailed / scanForTensions) is an explicit argument. Falling back to the current time silently answered queries as of the present moment with no notification that the instant parameter was unparseable.

Proposed Changes

  1. packages/core/src/validity.ts:
    • Updated docstring to document the RangeError contract for caller-supplied evaluation instants.
    • Throws RangeError('Unparseable evaluation instant: ... Expected YYYY-MM-DD or an RFC 3339 timestamp.') when now is non-empty but cannot be parsed as a date or instant.
  2. packages/core/test/validity-instants.test.ts:
    • Added evaluationInstant (#1166) unit test suite asserting:
      • Omitted or empty now returns current timestamp.
      • YYYY-MM-DD date resolves to end-of-day UTC (23:59:59.999Z).
      • RFC 3339 instants resolve accurately with zone offset.
      • Naive timestamps pin to UTC.
      • Unparseable strings throw RangeError with descriptive message.
  3. packages/core/test/tensions.test.ts:
    • Added regression test asserting getCandidatePairs fails fast with RangeError when options.now is unparseable.

Verification

  • npm test -- packages/core/test/validity-instants.test.ts packages/core/test/tensions.test.ts: 122/122 passed.
  • packages/core builds cleanly via npm run build (tsup).

…i#1166)

evaluationInstant(now) in packages/core/src/validity.ts returned Date.now() when closesAt(now) was null, causing callers that pass an unparseable date/instant to be silently answered as of the present moment instead. getCandidatePairsDetailed exposes options.now publicly so callers can evaluate temporal states at a specific instant; a typo produced a plausible-looking answer for the current time with nothing reported.

Unlike stored bounds (where an unparseable value is treated as absent to avoid hiding valid content over data errors), a caller-supplied evaluation instant is an explicit argument. An unparseable now string now throws a RangeError so bad inputs fail fast rather than silently corrupting temporal evaluations.

- Update evaluationInstant docstring and add RangeError throw on unparseable now.
- Add unit test suite in packages/core/test/validity-instants.test.ts asserting empty default, date-only end-of-day UTC, explicit instant, naive UTC-pinning, and RangeError throw.
- Add regression test in packages/core/test/tensions.test.ts verifying getCandidatePairs fails fast when options.now is unparseable.

Closes plur-ai#1166

Signed-off-by: amasen02 <amasen02@users.noreply.github.com>
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

ℹ️ Could not auto-assign #1166 to @amasen02 — GitHub only allows assigning users with repo access or prior interaction on the issue. Maintainer: please assign it manually so the claim is recorded. This is not a problem with the PR.

@miles-on-nightshift miles-on-nightshift left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The RangeError is correct here — a caller-supplied now is an explicit argument, not a stored field, so silent fallback to the current time is worse than a loud failure. The asymmetry with isExpired/isNotYetValid (which treat unparseable bounds as absent) is real but intentional and the docstring now explains why. Test covers the new throw path and the unchanged no-now path. CI passing. @plur9 ready for merge.

@amasen02

amasen02 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Gentle ping on throwing RangeError for unparseable evaluation instants (122 tests green). Ready to adjust the error surface per feedback.

@crtahlin crtahlin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved. Two documentation gaps are worth closing, neither blocking — filed as follow-ups.

The fix is correct and minimal. evaluationInstant now throws a RangeError on a caller-supplied now it cannot parse, instead of silently answering as of the present moment, and the docstring explains why this diverges from isExpired and isNotYetValid rather than merely stating that it does — a stored bound is schema-validated data where hiding content is the worse failure, a caller-supplied argument is an explicit instruction. That reasoning is the part a future reader would otherwise have to reconstruct.

The regression is asserted twice, once on the helper and once through the public getCandidatePairs surface a real caller uses. Both bite: reverting the throw to return ms === null ? Date.now() : ms turns exactly those two assertions red and leaves everything else green. The control tests for the forms that must keep working are there too — date-only resolving to end-of-day UTC, an explicit Z, a +02:00 offset, and a zone-less timestamp pinned to UTC — so the end-of-day semantics that preserve the pre-#1150 answer are protected rather than assumed.

RangeError matches local practice: packages/core/src/expiry.ts:127 throws the same type for an out-of-order valid_from/valid_until, also a well-typed string carrying an unacceptable value.

Follow-ups: #1179, #1180

Notes

  • packages/core/src/tensions.ts:110 — TemporalGateOptions.now is the doc a caller actually reads, and it still describes only the old contract: an "ISO date treated as 'today'" that "defaults to the current date". Nothing there says a malformed value is now fatal. #1166 asked for the behaviour to be documented, and it is — thoroughly, on the internal helper, but not on the option the caller sets. Filed as #1179.
  • packages/core/src/validity.ts:177,180 — the @throws clause says the function throws when now is "provided but cannot be parsed", but the empty string is provided, cannot be parsed, and does not throw, because line 180 uses a falsy check. The new test pins that, so it is deliberate — but now: '' from a blank config field is the same silent-wrong-answer this change exists to stop. Filed as #1180, together with the accepted-forms list being narrower than the behaviour: pinToUtc also takes a zone-less timestamp, which is not valid RFC 3339.
  • packages/core/test/validity-instants.test.ts:231 — the empty-string assertion checks only the lower bound, unlike the omitted-argument case two lines above. A value far in the future would still pass.

Blast radius, since a new throw deserves the check

Effectively nil on shipped surfaces. Neither the CLI (packages/cli/src/commands/tensions.ts:173) nor the MCP tool (packages/mcp/src/tools.ts:3925) ever sets now; there is no --now flag and no now field in the tool schema; and evaluationInstant is not re-exported from packages/core/src/index.ts. The throw is reachable only from library embedders and tests, so this does not turn a quiet wrong answer into a user-facing crash anywhere today.

Tests: PASS — full packages/core suite, 3679 passed. One unrelated failure, test/injection-dedup.test.ts ("four concurrent processes write ONE event"), reproduces identically on the base commit d005139e and is a concurrency-sensitive test flaking under local parallel load; CI is green on that same commit. The targeted files are 122/122.

Verified: the throw was reverted to the old fallback to confirm both new tests fail; every accepted now form was exercised; every caller of evaluationInstant was traced to confirm reachability.

@amasen02

amasen02 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Gentle nudge — this one is approved by both @miles-on-nightshift and @crtahlin with all checks green. Ready to merge whenever convenient; happy to make any final adjustments if needed. Thanks!

@plur9
plur9 merged commit dc1e212 into plur-ai:main Sep 11, 2026
8 checks passed
plur9 added a commit that referenced this pull request Sep 14, 2026
GitHub appends the PR number to a squash-merge subject, so this PR lands on
main as "... (#1190) (#1191)" — the repo's own history shows the shape, e.g.
"fix(core): throw RangeError on unparseable evaluation instant (#1166) (#1176)".

The manifest gate parses EVERY number in a squash subject, not just the last,
so #1191 counts as a shipped user-facing PR the moment this merges and must be
declared or Step 3.6 aborts the release. Declaring it here, before the merge,
because the CHANGELOG it would need to appear in is inside this PR.

Simulated post-merge: 61 PR numbers shipped, 61 declared.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4or5TZEfYoreFm7GLJwRJ
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.

evaluationInstant silently evaluates at the present when its now parameter will not parse

4 participants