[ 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
- 634
- parsers in the pipeline
- 3
- strategic db indexes
- 10
- 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.
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.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.
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.
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/).
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.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
- decisiontechnical decision
Built a custom composable middleware chain instead of Express
- decisiontechnical decision
Caching is process-local InMemoryCache, not Redis
- kept inroad not taken
The honest wrinkle: two backend stacks still ship
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
- p1 / p2 / p3 (explicit, Todoist)0.95 → 0.98
- top / highest priority, must do0.90
- urgent / critical / asap / high priority0.85
- low priority / someday / optional0.80
- high (bare keyword)0.75
- medium (bare keyword)0.70
- 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 code that carries the engineering
Six places where the platform does the work that matters: the resolver, the weighted-confidence pass, the pure-SQL service base, the cache, and the composable JWT middleware. Each links into the source at the pinned SHA.
Run the smart-input pipeline in your own clone
The smart-input pipeline is pure browser TypeScript, so SmartParser runs entirely client side: parsed tags, per-tag confidence bars, the detected conflicts, and the resolved clean text.