Skip to content

Table lens: derived expr columns + author sort, computed at read time#23

Open
snudurupati wants to merge 4 commits into
mainfrom
feat/table-derived-columns
Open

Table lens: derived expr columns + author sort, computed at read time#23
snudurupati wants to merge 4 commits into
mainfrom
feat/table-derived-columns

Conversation

@snudurupati

@snudurupati snudurupati commented Jul 9, 2026

Copy link
Copy Markdown

Why

Some column values are derived facts that decay with time — the motivating case is a lead's freshness score in the crm Daily-leads table. Freshness is a function of a stored post date and now; storing the score in the graph means rewriting every row every day (1000 leads → 1000 daily touches). The right shape: the graph stores the durable fact (the date), and the notebook derives the current value on every render.

What

column.expr — an optional per-column expression evaluated against each row at query-refresh time; the result is injected under the column's key (so numeric right-align etc. just work). column.precision rounds it.

The expression language is deliberately tiny and safe — arithmetic, parens, and three builtins; no ambient environment, no property access, not eval:

  • num("col") — row field as a number
  • days_since("col") — fractional days from a date/Z-less-datetime field to now
  • tier(x, t1,v1, t2,v2, ..., default) — step function for decay tiers

Worked example (current lead rank from a stored post date + stored component scores):

- key: current_rank
  label: Score
  precision: 2
  expr: >
    0.35 * tier(days_since("s.observed_at"), 1,1.0, 3,0.75, 7,0.55, 14,0.35, 30,0.2, 0.05)
    + 0.25 * num("eq.summary") + 0.40 * num("ea.summary")

table.sort{key, dir} pre-render sort, needed when display order depends on a derived column the source query cannot order by. Numeric-aware, stable, nulls last; string comparison is pinned to a fixed locale (Intl.Collator("en")) so order is identical in the Node TUI and any browser.

Where

Evaluation happens once in core's buildRuntimeProps (not in the renderers), so web and TUI show identical values and the components stay presentation-only. Failure mode: a bad expr, unknown function, or missing field yields null (blank cell) — never NaN, never a render-time throw.

Tests

New expr.test.ts covers precedence, the builtins, decay-tier selection, null-on-garbage (including window.alert(1) rejected), derived+sort integration, and no-op passthrough when unauthored. Full workspace: 5 packages build, all suites pass (core 131, web 13 files).

🤖 Generated with Claude Code

Greptile Summary

This PR adds read-time derived columns and author sorting for table lenses.

  • Adds a small expression evaluator for table column formulas.
  • Injects derived values into table rows during core runtime prop building.
  • Adds precision handling for derived numeric values.
  • Adds stable, nulls-last table sorting with fixed-locale string comparison.
  • Adds tests for expressions, invalid inputs, derivation, and sorting.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
packages/core/src/catalog/expr.ts Adds the expression parser and builtins, with invalid numeric results returning null.
packages/core/src/catalog/lenses/table.ts Adds table expr, precision, and sort props, then materializes derived rows with stable sorting.
packages/core/src/catalog/index.ts Runs table derivation and sorting while building Table runtime props.
packages/core/src/catalog/expr.test.ts Adds tests for expression evaluation, bad inputs, derived values, and table sorting.

Reviews (4): Last reviewed commit: "expr: tier() rejects non-finite argument..." | Re-trigger Greptile

Context used:

  • Context used - CLAUDE.md (source)

Some column values are derived facts that decay with time — a lead's
freshness, an SLA countdown, an age. Storing the decayed value in the
graph means rewriting every row every day; the durable fact is the
date it derives from. This adds:

- column.expr: a tiny safe expression (arithmetic + num()/days_since()/
  tier() builtins, no ambient environment) evaluated per row against
  the query result at refresh time; result injected under column.key.
  column.precision rounds it.
- table.sort {key, dir}: pre-render sort, needed when display order
  depends on a derived column the source query cannot order by.
  Numeric-aware, stable, nulls last.

Evaluation lives in core (buildRuntimeProps), so web and TUI render
identical values; bad exprs/missing fields blank the cell (null) rather
than rendering NaN or throwing mid-render.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/core/src/catalog/expr.ts
Comment thread packages/core/src/catalog/lenses/table.ts Outdated
Number(true)/Number(false) coerce to 1/0, letting a boolean field leak
into derived scores and sort order. Only pass numbers through and only
coerce non-empty strings; everything else is NaN (surfaced as null).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@snudurupati

Copy link
Copy Markdown
Author

Addressed the boolean-coercion review comment in bccc7fe: num() now only passes numbers through and coerces non-empty strings; booleans and whitespace-only strings evaluate to null like other bad input. Added test coverage for both cases.

localeCompare() with no locale uses the host default, so author-sorted
string columns could order differently in the Node TUI vs a browser.
Use a shared Intl.Collator("en") for deterministic cross-host order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/core/src/catalog/expr.ts Outdated
NaN from num() on a boolean/blank field passed tier()'s typeof check,
skipped every threshold, and returned the finite default — which then
survived evaluateExpr's isFinite guard and rendered instead of blanking
the cell. Reject non-finite args so the throw becomes null as intended.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

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.

2 participants