back to workTypeScript · MIT · React 19

[ data ]taskflow-calendarsha 7d45d4cmeasured-in-repo

Free text in, structured task out, with the conflicts settled on the record.

taskflow-calendar is a full-stack TypeScript calendar and task platform. The standout is the smart input: type a sentence and a pipeline of parsers turns it into a date, a priority, and entities in real time, scores each one, and resolves the parsers that claim the same words. The frontend suite runs green at the pinned commit.

[ taskflow-calendar ], noun
1.a full-stack TypeScript calendar and task platform
2.a priority-ordered NLP smart-input pipeline
3.confidence-weighted conflict resolution on a pure-SQL backend
frontend suite
634vitest tests across 58 files, all green on a clean checkout
parsers in the pipeline
3ChronoDate, Priority, Compromise, behind one Parser interface
strategic db indexes
10counted as CREATE INDEX in add-performance-indexes.sql
lang
TypeScript
license
MIT
stack
React 19 · Vercel · pg
baseline
Todoist smart input
harness
vitest
verdict
measured-in-repo

the algorithm, made physical

Three parsers claim spans of the same line. The resolver settles every overlap by priority, then confidence, and emits clean text with one weighted score, on the record.

ChronoDatepriority 10Prioritypriority 8Compromisepriority 6date0.85priority0.90person0.75noun?weak claiminputMeet John tomorrow at 3pm for review #high-priorityresolveConflictspriority → conf.dropped · lower priority (6 < 10)resolved tagspersonJohn0.75date2026-06-22 15:000.85priorityhigh0.90clean text"Meet for review"overall conf 0.842
resolveConflicts settles each overlap by parser priority first, per-tag confidence second. The contested tomorrow span is dropped because ChronoDate (10) outranks Compromise (6); the kept tags emit clean text and a priority-weighted 0.842 overall confidence.
problem

One line of free text has to become structured data while the user watches

A user types Meet John tomorrow at 3pm for review #high-priority into one field. I have to pull out the date, the priority, and the people, show confidence-scored highlights as they type, and leave behind clean text for the title. None of that is the hard part.

The hard part is that more than one parser will claim the same words. A date parser reads tomorrow at 3pm as a timestamp. A general NLP parser also sees tomorrow as a bare noun and wants to tag it too. Both are right about the characters. Only one can win the span. Without a rule for that, the same text gets double-tagged and the title comes out wrong.

approach

A pluggable parser pipeline with one rule for who wins a contested span

Each parser implements one interface: test, parse, priority. SmartParser runs them in priority order, collects every claimed span, and detects the overlaps. Three ship today: ChronoDate at priority 10, Priority at 8, and Compromise at 6.

The resolver settles each overlap by parser priority first, then by per-tag confidence (SmartParser.ts:152-188). It strips the winning spans out of the input, returns the clean remainder as the task title, and folds the surviving tags into one priority-weighted overall confidence (:223-239). Parsers register and unregister at runtime through addParser and removeParser, so adding a new tag kind is one object, not a rewrite. The figure above traces this on the worked example.

architecture

React on the front, two parallel backends behind it

The frontend is React 19 and Vite. Client and UI state live in Zustand. Server state lives in TanStack Query with optimistic updates. The NLP pipeline sits in src/components/smart-input/parsers/ and runs entirely in the browser, which is what makes the demo below the real code rather than a mock. The backend is split in two: the serverless lib/ path (api/ Vercel functions over a pure-SQL service layer) and the auth-server path (a separate Express + Prisma server in packages/backend/).

taskflow-calendar architecture: a React 19 + Vite frontend with the SmartParser NLP pipeline (ChronoDate priority 10, Priority 8, Compromise 6, resolving to clean text plus a weighted confidence) and a hybrid Zustand client-state plus TanStack Query server-state split, fanning into two parallel backends. The serverless path runs api/ Vercel functions through a composable middleware chain (cors, requestId, rateLimit, auth, validate) into a pure-SQL service layer over node-postgres, with an in-memory cache and ten strategic indexes. It migrated off Prisma to raw pg. The dimmed auth-server path is a packages/backend Express server with Prisma still shipping a full schema, plus a packages/shared Zod layer. Both reach one PostgreSQL.
The committed architecture diagram from the repo. The amber serverless lib/ path is the one that dropped Prisma for raw pg. The dashed packages/backend/ Express + Prisma stack runs alongside it. Both reach one PostgreSQL.
tradeoffs · road not taken

What I changed, what it cost, and the wrinkle I will not paper over

The README's Technical Decisions section records two moves I stand behind and one consequence that does not go away. Each card names what I chose, what it cost, and why.

  • decisiontechnical decision

    Dropped Prisma for raw pg in the serverless layer

    cost
    Lost the ORM's migrations and type-safe query builder, so lib/services/ owns its SQL and its mapping by hand.
    why
    Smaller cold starts, smaller bundles, and full control over the query plan in a function where every kilobyte and millisecond of init shows up.
  • decisiontechnical decision

    Built a custom composable middleware chain instead of Express

    cost
    Reimplemented cors, request IDs, rate limiting, auth, and validation that Express middleware would hand me for free.
    why
    The serverless path has no long-lived server to hang Express off. composeMiddleware keeps the per-function pipeline explicit and tree-shakeable.
  • decisiontechnical decision

    Caching is process-local InMemoryCache, not Redis

    cost
    A multi-instance deployment gets per-instance caches that can disagree, which an in-code comment calls out directly.
    why
    One dependency fewer to run, and the cache has TTL, LRU eviction, and pattern invalidation. The swap to Redis is a documented later step, not a rewrite.
  • kept inroad not taken

    The honest wrinkle: two backend stacks still ship

    cost
    packages/backend/ still carries a full Prisma schema and Express server, so the repo holds two parallel backends at once.
    why
    The 'we dropped Prisma' story is true only for the serverless lib/ path. I keep both because the auth server predates the migration; I do not pretend the repo is one clean stack.
benchmark · vs todoist

The baseline is Todoist's smart input. There is no speed number in the repo.

The named baseline is Todoist. PriorityParser implements Todoist's p1/p2/p3 syntax on purpose, so the smart input lines up with a tool people already know.

There is no quantitative benchmark against Todoist in the repo, so the comparison stays at the level of the syntax, not a stopwatch. The README also claims indexes give 40-60% faster queries. I checked: no benchmark script and no EXPLAIN ANALYZE output in the repo back it. The verified parser output and the test record are below.

real parser confidences · PriorityParser

The falsifiable evidence is the parser output. These are the base confidences PriorityParser assigns by pattern specificity, with the explicit Todoist p1/p2/p3 rung at the top, boosted to 0.98.

  1. p1 / p2 / p3 (explicit, Todoist)0.95 0.98
  2. top / highest priority, must do0.90
  3. urgent / critical / asap / high priority0.85
  4. low priority / someday / optional0.80
  5. high (bare keyword)0.75
  6. medium (bare keyword)0.70
  7. low (bare keyword)0.65

Source: PriorityParser.ts:16-39, real base confidences before context adjustment. The explicit Todoist p1/p2/p3 rung is boosted to 0.98 in adjustConfidence.

the test record at sha 7d45d4c

The README headline is 738 tests passing. That is the collected count across three configs. Here is what actually runs on a clean checkout, and what needs a database.

Frontend suite (vitest, 58 files)
634 / 634 passing
Shared package (Zod schemas, 4 files)
22 / 22 passing
lib/ service + middleware unit (8 files)
220 / 220 passing
Backend workspace (packages/backend)
57 passing · 25 skipped
api/ + lib integration / e2e
needs live Postgres

So the honest read is 634 plus 22 plus 220 pass on a clean checkout. The integration and e2e suites need docker:up to bring up Postgres before they run.

proof · the hard parts

The code that carries the engineering

demo · in the repo

Run the smart-input pipeline in your own clone

taskflow-calendar repository on GitHub