DEV Community

Ali Gürtuna | AGProLabs
Ali Gürtuna | AGProLabs

Posted on Fully Autonomous

A review cursor should not invent completion

A resource list needs a way to resume without pretending that selection means completion. A numerical index can become confusing when the list changes order. A stable item ID gives the next review a clearer starting point, but it still needs a documented rule for missing IDs, excluded items and an exhausted candidate list.

I maintain AGProLabs, AliGurtuna.com and Babamın Şiir Defteri. Their resources include TradingView analysis guides, a health-tool directory and a poet directory. The example below is a standalone in-memory review selector for such a resource list. It is not an indexing API, automated publishing tool or deployed change to these projects.

A cursor is a position, not an outcome

Use three separate records:

  • A stable item identity supplied by your catalog.
  • A cursor identifying the item visited most recently.
  • Outcome or exclusion records with their own reasons and dates.

An item can be selected and never successfully reviewed. Persisting the cursor immediately after selection can skip an interrupted visit. Updating it after a completed visit still does not mean the resource was approved, published or indexed. Those outcomes need their own evidence.

This example calls its suggested cursor cursorAfterVisit to make that boundary visible. The function does not save it for you and does not manufacture a completion flag.

State the fallback rules

For this small contract, start just after the saved item in the current order. If it was removed, start at the first item. Wrap at most once. Skip ineligible items and explicitly excluded identities. Return null when no candidate remains under those rules.

A reorder preserves the cursor's identity but changes what comes after it. That is deliberate here. If your application needs a frozen traversal order across sessions, keep a catalog version or an explicit ordered queue as well. A stable ID alone does not freeze the list.

Save this as review-cursor.mjs:

export function nextReview(items, lastId = null, excluded = new Set()) {
  if (!Array.isArray(items) || !(excluded instanceof Set))
    throw new TypeError('items must be an array and excluded must be a Set');
  if (lastId !== null && (typeof lastId !== 'string' || !lastId.trim()))
    throw new TypeError('lastId must be null or a nonempty string');
  const seen = new Set();
  for (const item of items) {
    if (!item || typeof item.id !== 'string' || !item.id.trim() ||
        typeof item.eligible !== 'boolean' || seen.has(item.id))
      throw new TypeError('Each item needs a unique nonempty id and boolean eligible');
    seen.add(item.id);
  }
  if (!items.length) return null;
  const previous = items.findIndex(item => item.id === lastId);
  const start = previous < 0 ? 0 : (previous + 1) % items.length;
  for (let offset = 0; offset < items.length; offset++) {
    const item = items[(start + offset) % items.length];
    if (item.eligible && !excluded.has(item.id))
      return { item, cursorAfterVisit: item.id };
  }
  return null;
}
Enter fullscreen mode Exit fullscreen mode

The selector validates identity uniqueness before choosing an item. Duplicate IDs should not quietly collapse into one review because you would lose the ability to say which entry was visited. Eligibility is a required boolean, not a truthy value such as "yes".

The returned item is the original object reference. The function does not mutate it, but it also does not deep-clone or freeze your catalog. Keep catalog mutation in your application's owning layer. The exclusion set is read-only from the selector's perspective; add a reviewed or temporarily held ID only under your stated policy.

Eight deterministic scenarios

Save as review-cursor.test.mjs beside the module:

import test from 'node:test';
import assert from 'node:assert/strict';
import { nextReview } from './review-cursor.mjs';
const items = ['a', 'b', 'c'].map(id => ({ id, eligible: true }));
test('first visit starts at the first eligible item', () => {
  assert.equal(nextReview(items).item.id, 'a');
});
test('cursor follows identity after a reorder', () => {
  assert.equal(nextReview([items[2], items[0], items[1]], 'a').item.id, 'b');
});
test('last item wraps once to the first', () => {
  assert.equal(nextReview(items, 'c').item.id, 'a');
});
test('eligibility and temporary exclusion are separate', () => {
  const input = [{ id: 'a', eligible: false }, items[1], items[2]];
  assert.equal(nextReview(input, null, new Set(['b'])).item.id, 'c');
});
test('no candidate does not manufacture a result', () => {
  assert.equal(nextReview([]), null);
  assert.equal(nextReview(items, null, new Set(['a','b','c'])), null);
});
test('a removed cursor uses the documented first-item fallback', () => {
  assert.equal(nextReview(items, 'removed').item.id, 'a');
});
test('invalid or duplicated identities fail explicitly', () => {
  assert.throws(() => nextReview([items[0], items[0]]), TypeError);
  assert.throws(() => nextReview([{ id: '', eligible: true }]), TypeError);
  assert.throws(() => nextReview([{ id: 'a', eligible: 'yes' }]), TypeError);
  assert.throws(() => nextReview({}, null), TypeError);
  assert.throws(() => nextReview(items, 2), TypeError);
  assert.throws(() => nextReview(items, null, []), TypeError);
});
test('selection does not mutate items, exclusions or completion state', () => {
  const input = Object.freeze(items.map(x => Object.freeze({ ...x })));
  const excluded = new Set(['b']);
  const result = nextReview(input, 'a', excluded);
  assert.equal(result.cursorAfterVisit, 'c');
  assert.deepEqual([...excluded], ['b']);
  assert.deepEqual(input, items);
  assert.equal(Object.hasOwn(result.item, 'completed'), false);
});
Enter fullscreen mode Exit fullscreen mode

Run node --test review-cursor.test.mjs. The Node test-runner documentation explains the built-in runner. All eight scenarios passed in this article's local check. The fixtures cover initial selection, reordering, wrapping, eligibility, temporary exclusion, exhaustion, a removed cursor, invalid identities and non-mutation. The final count is eight test scenarios, not a benchmark or exhaustive proof about every catalog.

Keep the outcome vocabulary relevant to the resource

A screened-candidate journal and a weekly process review show why an observation and its later interpretation need separate records. A cursor can help select the next record to examine, but it cannot establish data quality, execution or trading performance.

For health categories or psychological-awareness resources, “visited” is a browsing status, not a medical conclusion. In the literary-review archive, opening a review does not mean you have read the original work or verified its attribution.

These are examples of different outcome vocabularies, not a reason to force every project into one approval state. Store the right outcome separately from the universal navigation position.

What a real application still needs

Persist the cursor and outcome records atomically where your storage requires it. Refresh the catalog under a clear version policy. Handle interrupted visits and unavailable resources without calling them successful. A live system may also need concurrency control, migrations, access checks and its own error handling; this pure selector supplies none of those automatically.

The useful result is modest: you can explain where review resumes, why an item was skipped and what a selected item does not prove. That makes a resource workflow easier to inspect without inflating its completion claims.

Disclosure: I maintain the linked projects. This article and code were prepared with AI assistance and locally tested. No production workflow, search-engine request or main-site code was changed. Trading and health references are resource context, not financial or clinical advice.

Try the browser fixtures

Open the Review Cursor Lab. It imports the same independent module and holds synthetic state only in memory. Choose a last-visited identity, then try default, reordered, excluded, exhausted and removed-ID fixtures. Observe the selection and effective cursor separately.

Five selected fixtures plus synthetic-visit advancement and reset were checked in Chrome. With market-journal saved, excluding health selects poetry-directory; the exhausted fixture shows NO CANDIDATE and disables the visit button. Reset restores the initial selection and clears the simulated visit history. The page never creates a completion flag or submits a request. Reloading discards state; durable storage, concurrency and catalog versioning remain separate application work.

Browse the module and reproducible tests. No live feed, analytics, account, clinical input or proprietary Pine source is included. These focused fixtures are not a cross-browser or exhaustive application certification.

Top comments (0)