Skip to content

feat(node): predicate recipes, decisions, and registry - #543

Merged
kartikeya-27 merged 11 commits into
ByteVeda:masterfrom
stromanni:feat/node-predicate-recipes
Jul 25, 2026
Merged

feat(node): predicate recipes, decisions, and registry#543
kartikeya-27 merged 11 commits into
ByteVeda:masterfrom
stromanni:feat/node-predicate-recipes

Conversation

@stromanni

@stromanni stromanni commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Closes #517.

Node's predicate surface was 28 lines — a boolean Predicate plus allOf/anyOf/not. A gate
could only allow or reject, so a policy like "hold this until business hours" was inexpressible.
Java already had EnqueueDecision plus five recipes; Python adds a registry, outcome metrics, and a
feature-flag provider seam. This closes the whole parity row rather than only the Java floor the
issue named as the minimum, since the issue title asks for the registry too.

src/predicates.ts becomes src/predicates/ — a package alongside locks/, resources/,
autoscale/. Existing from "./predicates" imports resolve unchanged.

Decisions

EnqueueDecision is allow | skip(reason) | defer(delayMs) | reject(reason), built with a
Decision factory object copying the Interception idiom. Evaluated where gates already ran, after
middleware onEnqueue; the first non-allow decision wins, as in Java.

  • rejectPredicateRejectedError (unchanged behaviour)
  • skipenqueue throws the new EnqueueSkippedError; the new tryEnqueue() returns null
  • defer → job created with the gate's delay, replacing any delayMs the caller passed

Booleans keep working: toDecision normalizes true/false, and a bare false carries no reason
so the thrown message and the predicate.rejected payload are byte-identical to before. A null-ish
return (a JavaScript caller returning nothing by accident) fails closed.

Recipes

Nine, validated at construction so a bad zone or window throws at wiring time:

From Java, same names From Python, camelCased
businessHours, timeWindow, dayOfWeek, payloadMatches, featureFlag isWeekend, after, before, envVarTruthy

Also exported as a Recipes bundle, so the Java examples transliterate. Zones resolve through
Intl — no new dependency.

Registry, metrics, events

registerPredicate(name, gate) plus queue.gate(task, "name"), resolved when the gate is
registered so an unknown name throws at startup rather than on the first enqueue. This is what makes
config-driven gating possible without a serializable AST — Node gates are closures, and Python's AST
exists only to serialize one.

queue.predicateStats() returns { allowed, skipped, deferred, rejected, errors }, one count per
gated enqueue. predicate.skipped and predicate.deferred join predicate.rejected on the event
bus and are webhook-subscribable.

Three choices worth reviewing

  1. allOf/anyOf are overloaded, not widened. (...Predicate[]) => Predicate and
    (...EnqueueGate[]) => EnqueueGate, and the body returns a real boolean when every member
    returned one. Widening the return type alone would have left const p: Predicate = allOf(a, b)
    compiling while if (p(ctx)) became always-true — a silent break. not stays boolean-only;
    inverting a defer has no meaning.
  2. PredicateContext gained now, read once per evaluation by the queue. Without it the time
    recipes would call new Date() internally and be untestable, which is the case for Java's
    Recipes today.
  3. Batches honour defer but refuse skip. Each entry carries its own options, so a per-entry
    delay works (Java can't — one shared options object). A skip throws EnqueueSkippedError,
    because the returned ids line up with the input and a dropped entry isn't expressible.

Closed sets follow the existing convention — literal union plus an as const array
(DECISION_KINDS, DAYS_OF_WEEK), and as const satisfies Record<…> for the decision→counter map,
mirroring OUTCOME_KIND_EVENTS. No TS enum, and the #505 contract that plain strings keep working
is preserved.

Time-zone math

tz.ts reads a wall clock with Intl.DateTimeFormat and resolves one back to an instant by
guessing with the offset at a reference instant, then re-reading the offset at the guess and
correcting once. That second pass is what makes a target across a DST boundary land correctly —
regression-tested against New York's 2026-03-08 spring-forward, where the naive single-pass answer
is an hour off. A target inside a spring-forward gap resolves to just after it, documented.

Tests

test/core/predicates.test.ts 7 → 19, plus predicate-recipes.test.ts (33) and
predicate-registry.test.ts (6). Recipes are driven through a pinned now, covering in/out of
window, midnight wrap, weekend rollover, the DST boundary, empty day sets, dotted-path misses, and
rejected zones. webhooks.test.ts's event count goes 26 → 28 for the two new names.

Node suite: 494 passed. The 10 test/dashboard/ files fail on master too — vite isn't installed
in the dashboard/ workspace locally, so build:dashboard fails in beforeAll.

Smoke on a real clock (Saturday 05:42 UTC): businessHours({ timeZone: "UTC" }) scheduled the job
at 2026-07-27T09:00:00.002Z, the next weekday opening, and featureFlag with the flag unset
returned null from tryEnqueue with no row written.

Docs

Rewrote the four <SdkOnly sdk="node"> blocks that said Node has no defer outcome, no recipe
library, no inspection API, and no predicate metrics or events, plus the batch semantics, the node
example page, and two api-reference tables. docs check:parity clean and the site builds.

Summary by CodeRabbit

  • New Features

    • Added decision-based enqueue gates (allow/reject/skip/defer) with deterministic enqueue-time context.
    • Introduced tryEnqueue (returns null on skipped), predicateStats(), and new predicate outcome events (predicate.skipped, predicate.deferred).
    • Added built-in gate “recipes” with timezone-aware scheduling plus a named predicate registry.
  • Bug Fixes

    • Worker shutdown is now awaitable after teardown completes.
    • Listener errors are logged at debug level without impacting other listeners.
  • Documentation

    • Expanded Node API references, guides, and examples for gate decisions, recipes, and enqueue behavior.
    • Updated changelog entries for the new gate/recipe system and bare-metal autoscaler.

A gate could only allow or reject, so a policy like "hold this until
business hours" was inexpressible. Gates may now return an
EnqueueDecision; booleans still mean allow/reject.
Nine ready-made gates matching the Java and Python surfaces. Time zones
resolve through Intl, so no new dependency; a wall-clock target re-reads
the zone offset at the candidate instant to survive DST shifts.
Gates can be registered by name and referenced as `gate(task, "name")`,
so gating is configurable without a serializable predicate AST.
predicateStats() reports what the gates decided.
Regenerating the docs page also picks up the autoscaler entry that
landed in CHANGELOG.md without a sync.
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bde91c35-6e3c-4d91-a92c-2ac2ff34b1e7

📥 Commits

Reviewing files that changed from the base of the PR and between b1709d2 and 8d1bbf8.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • docs/content/docs/node/api-reference/queue/index.mdx
  • docs/content/docs/resources/changelog.mdx
  • docs/content/docs/shared/guides/core/predicates.mdx
  • sdks/node/src/predicates/combinators.ts
  • sdks/node/src/predicates/decisions.ts
  • sdks/node/src/predicates/recipes.ts
  • sdks/node/src/predicates/tz.ts
  • sdks/node/test/core/predicate-recipes.test.ts
  • sdks/node/test/core/predicates.test.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • docs/content/docs/node/api-reference/queue/index.mdx
  • docs/content/docs/resources/changelog.mdx
  • CHANGELOG.md
  • sdks/node/src/predicates/combinators.ts
  • sdks/node/test/core/predicate-recipes.test.ts
  • sdks/node/src/predicates/tz.ts
  • sdks/node/test/core/predicates.test.ts
  • sdks/node/src/predicates/recipes.ts
  • docs/content/docs/shared/guides/core/predicates.mdx

📝 Walkthrough

Walkthrough

Node enqueue gates now support allow, reject, skip, and defer decisions, reusable recipes, named registration, predicate metrics, and predicate events. Queue APIs, tests, examples, changelogs, and reference documentation were updated.

Changes

Node predicate gates

Layer / File(s) Summary
Decision contracts and predicate APIs
sdks/node/src/errors.ts, sdks/node/src/events.ts, sdks/node/src/predicates/*, sdks/node/src/index.ts
Adds decision types and factories, combinators, providers, registries, metrics, validation errors, predicate events, and public exports.
Time-zone utilities and recipe implementations
sdks/node/src/predicates/tz.ts, sdks/node/src/predicates/recipes.ts
Adds time-zone arithmetic and validated business-hours, time-window, weekday, payload, feature-flag, environment, and instant-based recipes.
Queue gate evaluation and enqueue outcomes
sdks/node/src/queue.ts
Adds inline or named gates, first-blocking decision evaluation, tryEnqueue, skip handling, deferred delays, batch behavior, events, and statistics.
Predicate and queue validation
sdks/node/test/core/*predicate*.test.ts, sdks/node/test/core/webhooks.test.ts
Tests recipes, registries, decisions, skip/defer behavior, metrics, and predicate event names.
API and behavior documentation
CHANGELOG.md, docs/content/docs/node/..., docs/content/docs/shared/guides/core/predicates.mdx
Documents the updated Node gate API, recipes, registry, enqueue semantics, metrics, events, batches, and examples.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Producer
  participant Queue
  participant Registry
  participant Gate
  participant EventBus
  Producer->>Queue: enqueue or tryEnqueue
  Queue->>Registry: resolve named gate
  Queue->>Gate: evaluate with taskName, args, now
  Gate-->>Queue: allow, reject, skip, or defer
  Queue->>EventBus: emit predicate outcome
  Queue-->>Producer: job id, null, or error
Loading

Possibly related PRs

Suggested reviewers: pratyush618

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The changelog docs include an unrelated Node bare-metal autoscaler entry that is not part of the predicate recipes/registry scope. Remove or split the autoscaler changelog update into a separate PR unless it is intentionally part of this issue.
Docstring Coverage ⚠️ Warning Docstring coverage is 69.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: Node predicate recipes, decisions, and registry.
Linked Issues check ✅ Passed The PR delivers the requested Node predicate recipes, registry, feature-flag providers, metrics, and parity-related predicate capabilities.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (2)
sdks/node/src/queue.ts (1)

928-951: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Skip reason is discarded before it reaches enqueue()/enqueueMany()'s thrown error.

prepareEnqueue's case "skip" (Lines 934-936) emits decision.reason on the predicate.skipped event but then return null;, discarding the reason. Downstream, enqueue() (Lines 551-556) throws new EnqueueSkippedError(name) with no reason at all, and enqueueMany() (Line 632) substitutes a fixed generic string instead of the gate's actual reason. This is inconsistent with the reject path just above (Lines 931-933), which does thread decision.reason into PredicateRejectedError. Since EnqueueSkippedError's constructor already accepts an optional reason, callers using the primary enqueue() API lose the specific "why" unless they separately wire a predicate.skipped listener or poll predicateStats().

♻️ Proposed fix: thread the skip reason through `prepareEnqueue` → `submit` → `enqueue`
   private prepareEnqueue<Name extends keyof TTasks & string>(
     name: Name,
     args: Parameters<TTasks[Name]> | undefined,
     options: EnqueueOptions | undefined,
     mode?: { batch: boolean },
-  ): { taskName: string; payload: Buffer; options: NativeEnqueueOptions } | null {
+  ): { taskName: string; payload: Buffer; options: NativeEnqueueOptions } | { skipped: true; reason: string } {
     ...
       case "skip":
         this.emitter.emit("predicate.skipped", predicateEvent(taskName, decision.reason));
-        return null;
+        return { skipped: true, reason: decision.reason };
   private submit<Name extends keyof TTasks & string>(
     name: Name,
     args: Parameters<TTasks[Name]> | undefined,
     options: EnqueueOptions | undefined,
-  ): string | null {
+  ): { jobId: string | null; skipReason?: string } {
     this.rejectIfQueueFull(options?.queue ?? "default");
     const prepared = this.prepareEnqueue(name, args, options);
-    if (prepared === null) {
-      return null;
+    if ("skipped" in prepared) {
+      return { jobId: null, skipReason: prepared.reason };
     }
     const { taskName, payload, options: nativeOpts } = prepared;
     const jobId = this.native.enqueue(taskName, payload, nativeOpts);
     this.emitter.emit("job.enqueued", { jobId, taskName, queue: nativeOpts.queue ?? "default" });
-    return jobId;
+    return { jobId };
   }
   enqueue<Name extends keyof TTasks & string>(...): string {
-    const jobId = this.submit(name, args, options);
-    if (jobId === null) {
-      throw new EnqueueSkippedError(name);
+    const { jobId, skipReason } = this.submit(name, args, options);
+    if (jobId === null) {
+      throw new EnqueueSkippedError(name, skipReason || undefined);
     }
     return jobId;
   }

   tryEnqueue<Name extends keyof TTasks & string>(...): string | null {
-    return this.submit(name, args, options);
+    return this.submit(name, args, options).jobId;
   }

enqueueMany's guard would similarly check if ("skipped" in entry) and could append entry.reason to its message.

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

In `@sdks/node/src/queue.ts` around lines 928 - 951, Thread the skip reason from
prepareEnqueue through submit to the public enqueue and enqueueMany flows
instead of returning an uninformative null. Preserve decision.reason in the
skipped result, have enqueue pass it to EnqueueSkippedError, and have
enqueueMany detect skipped entries and include their reason rather than using a
fixed generic message.
docs/content/docs/node/more/examples/predicate-gated-jobs.mdx (1)

63-84: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reject demo can't actually reject; the gate that can (underQuota) is never shown.

sendPromo's gates are anyOf(urgent, duringBusinessHours)urgent is a plain boolean and duringBusinessHours only ever allows or defers, so this call can never throw PredicateRejectedError, even though the surrounding text says "A rejection throws synchronously." Meanwhile underQuota, the gate defined specifically to reject (line 33-38) and wired to exportData (line 60), is never enqueued anywhere in the doc.

Consider swapping the reject example to queue.enqueue("exportData", [tenantId]) so the catch block demonstrates a real rejection.

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

In `@docs/content/docs/node/more/examples/predicate-gated-jobs.mdx` around lines
63 - 84, Update the rejection example to enqueue the reject-capable exportData
task instead of sendPromo, passing the tenantId argument used by its underQuota
gate. Keep the PredicateRejectedError handling and surrounding explanation
intact, while leaving the tryEnqueue example unchanged.
🧹 Nitpick comments (1)
sdks/node/test/core/predicate-recipes.test.ts (1)

244-255: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard TASKITO_TEST_FLAG cleanup with try/finally.

If the assertion at Line 252 fails, delete process.env.TASKITO_TEST_FLAG on Line 253 never runs, leaking the env var into later tests. The sibling featureFlag env test (Lines 199-208) already guards its cleanup with try/finally — apply the same pattern here.

♻️ Proposed fix
     process.env.TASKITO_TEST_FLAG = "0";
-    expect(gate(at(WED_NOON_UTC))).toBe(false);
-    delete process.env.TASKITO_TEST_FLAG;
+    try {
+      expect(gate(at(WED_NOON_UTC))).toBe(false);
+    } finally {
+      delete process.env.TASKITO_TEST_FLAG;
+    }
     expect(() => envVarTruthy("")).toThrow(PredicateValidationError);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/node/test/core/predicate-recipes.test.ts` around lines 244 - 255, Wrap
the TASKITO_TEST_FLAG mutations and assertions in the envVarTruthy test with a
try/finally block, and move delete process.env.TASKITO_TEST_FLAG into the
finally block so cleanup always runs even when an expectation fails. Preserve
the existing truthy, false, and validation assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 34-42: Update the older “Full lifecycle event taxonomy” changelog
entry to remove predicate.deferred from the Python-only event list, while
retaining predicate.cancelled and the rest of the existing wording.

In `@docs/content/docs/node/api-reference/queue/index.mdx`:
- Around line 46-47: Update the tryEnqueue documentation row to state that gate
skips return null while gate rejects still throw PredicateRejectedError,
preserving the existing enqueue behavior description.

In `@docs/content/docs/resources/changelog.mdx`:
- Around line 41-49: The changelog contains a contradictory lifecycle-event
statement about predicate.deferred. Update the “Full lifecycle event taxonomy”
bullet to remove predicate.deferred from the Python-only list, while keeping
predicate.cancelled there and preserving the event-bus statement in the Node
predicate recipes bullet.

In `@docs/content/docs/shared/guides/core/predicates.mdx`:
- Around line 235-266: Update the Decision table’s `false` /
`Decision.reject(reason)` row to explicitly state that `tryEnqueue` also throws
`PredicateRejectedError`, matching the documented Java behavior. Leave the
existing `enqueue` behavior and other decision rows unchanged.
- Around line 382-413: Update the introductory behavior summary to distinguish
ordinary Boolean recipes— isWeekend, before, and envVarTruthy—which allow or
reject, from recipes that skip silently. State that payloadMatches and
featureFlag skip, while dayOfWeek skips only when days is empty; preserve the
existing time-based defer behavior.

In `@sdks/node/src/predicates/combinators.ts`:
- Around line 12-22: Update the composed-gate handling in allOf and the
corresponding anyOf path to treat null or undefined outcomes as false before
accessing outcome.kind or otherwise inspecting the result. Preserve existing
boolean and decision handling, ensuring missing gate returns fail closed and
produce rejection behavior.

In `@sdks/node/src/predicates/decisions.ts`:
- Around line 90-96: Extend the decision validation immediately before the
return in the gate outcome validation flow to enforce payload invariants, not
just DECISION_KINDS membership. For defer outcomes, require a valid non-negative
delayMs consistent with Decision.defer(); for skip and reject outcomes, validate
their reason payloads using the existing decision constructors or validation
symbols. Keep unknown-kind errors unchanged and only return outcome after all
kind-specific payload checks pass.

In `@sdks/node/src/predicates/recipes.ts`:
- Around line 340-348: Update the object-property lookup in step to use
Object.hasOwn(record, segment) instead of the in operator, ensuring only own
payload properties match while preserving the existing MISSING behavior for
absent paths.

In `@sdks/node/src/predicates/tz.ts`:
- Around line 122-133: The zonedInstant conversion must resolve nonexistent
spring-forward wall times forward to the first valid local instant instead of
applying a correction that moves backward across the gap. Update zonedInstant’s
offset-estimation logic to detect when the requested wall-clock target falls in
a timezone gap and advance the result to the gap’s end, while preserving normal
conversion behavior for valid times and fall-back ambiguities.

---

Outside diff comments:
In `@docs/content/docs/node/more/examples/predicate-gated-jobs.mdx`:
- Around line 63-84: Update the rejection example to enqueue the reject-capable
exportData task instead of sendPromo, passing the tenantId argument used by its
underQuota gate. Keep the PredicateRejectedError handling and surrounding
explanation intact, while leaving the tryEnqueue example unchanged.

In `@sdks/node/src/queue.ts`:
- Around line 928-951: Thread the skip reason from prepareEnqueue through submit
to the public enqueue and enqueueMany flows instead of returning an
uninformative null. Preserve decision.reason in the skipped result, have enqueue
pass it to EnqueueSkippedError, and have enqueueMany detect skipped entries and
include their reason rather than using a fixed generic message.

---

Nitpick comments:
In `@sdks/node/test/core/predicate-recipes.test.ts`:
- Around line 244-255: Wrap the TASKITO_TEST_FLAG mutations and assertions in
the envVarTruthy test with a try/finally block, and move delete
process.env.TASKITO_TEST_FLAG into the finally block so cleanup always runs even
when an expectation fails. Preserve the existing truthy, false, and validation
assertions.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 76064349-51ac-401f-ae43-0fc1cb2ae15d

📥 Commits

Reviewing files that changed from the base of the PR and between 2a22048 and b1709d2.

📒 Files selected for processing (24)
  • CHANGELOG.md
  • docs/content/docs/node/api-reference/queue/index.mdx
  • docs/content/docs/node/api-reference/queue/resources.mdx
  • docs/content/docs/node/more/examples/predicate-gated-jobs.mdx
  • docs/content/docs/resources/changelog.mdx
  • docs/content/docs/shared/guides/core/predicates.mdx
  • sdks/node/src/errors.ts
  • sdks/node/src/events.ts
  • sdks/node/src/index.ts
  • sdks/node/src/predicates.ts
  • sdks/node/src/predicates/combinators.ts
  • sdks/node/src/predicates/context.ts
  • sdks/node/src/predicates/decisions.ts
  • sdks/node/src/predicates/index.ts
  • sdks/node/src/predicates/metrics.ts
  • sdks/node/src/predicates/providers.ts
  • sdks/node/src/predicates/recipes.ts
  • sdks/node/src/predicates/registry.ts
  • sdks/node/src/predicates/tz.ts
  • sdks/node/src/queue.ts
  • sdks/node/test/core/predicate-recipes.test.ts
  • sdks/node/test/core/predicate-registry.test.ts
  • sdks/node/test/core/predicates.test.ts
  • sdks/node/test/core/webhooks.test.ts
💤 Files with no reviewable changes (1)
  • sdks/node/src/predicates.ts

Comment thread CHANGELOG.md
Comment thread docs/content/docs/node/api-reference/queue/index.mdx Outdated
Comment thread docs/content/docs/resources/changelog.mdx
Comment thread docs/content/docs/shared/guides/core/predicates.mdx
Comment thread docs/content/docs/shared/guides/core/predicates.mdx Outdated
Comment thread sdks/node/src/predicates/combinators.ts
Comment thread sdks/node/src/predicates/decisions.ts Outdated
Comment thread sdks/node/src/predicates/recipes.ts
Comment thread sdks/node/src/predicates/tz.ts
A gate returning nothing threw on `.kind` inside allOf/anyOf instead of
failing closed, and a hand-built `{ kind: "defer", delayMs: -1 }` — which
type-checks — bypassed the factory's invariant on its way to storage.
`segment in record` let a path resolve to an inherited member such as
`constructor`, which is not present in the payload.
A wall clock inside a DST gap (02:30 in New York on 2026-03-08) resolved
to 01:30 EST — an hour before the requested time — because the offset
correction moved the instant back across the transition. Now the
candidate the zone agrees with wins, and a gap target shifts forward
past the gap, as java.time does.
tryEnqueue only swallows a skip, and the recipes are not uniformly
skip-or-defer — three are plain booleans that reject.
@stromanni

Copy link
Copy Markdown
Contributor Author

CodeRabbit round 1 — all 9 findings addressed

Every one was valid; nothing skipped. Five commits, split by concern.

Finding Verdict Commit
Nullish gate return bypasses fail-closed in allOf/anyOf Real — threw TypeError on .kind 8057e6a
toDecision validates kind but not the payload Real, and reachable from TypeScript too — { kind: "defer", delayMs: -1 } type-checks 8057e6a
segment in record matches inherited properties Real 55e40ed
Spring-forward gap resolves backward Real — reproduced at 01:30 EST, an hour early d8c0a80
reject row omits tryEnqueue behaviour Real 54bb829
"filters skip silently" wrong for 3 of 9 recipes Real 54bb829
tryEnqueue row omits that reject throws Real 54bb829
CHANGELOG.md contradicts itself on predicate.deferred Real 8d1bbf8
Same in the generated changelog.mdx Real 8d1bbf8

The DST one was the most substantive. It also falsified this branch's own docstring, which claimed
gap targets resolved after the gap when they resolved an hour before it. zonedInstant now keeps
the candidate whose own offset reproduces it, and when neither does — the definition of a gap — the
later candidate wins, shifting the target past the gap as java.time does. Fall-back overlaps still
take the first occurrence.

Four new regression tests, each verified red on the preceding commit:
fails closed on a null-ish gate inside allOf / anyOf, re-validates a hand-built decision's
payload
, reads own properties only, not inherited ones, pushes a target inside a spring-forward
gap past the gap
— plus tolerates a decision with a missing or non-string reason as a contract
lock.

Node suite 270 passed in test/core/ (was 265), typecheck and biome clean, docs build and
check:parity clean.

@kartikeya-27
kartikeya-27 merged commit 2266554 into ByteVeda:master Jul 25, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Node: predicate recipes and registry

2 participants