Skip to content

release: v1.2.0 — TIME columns, WEEK()/DATEADD(), BROWSE cell validation, Overtime demo#57

Merged
DDecoene merged 17 commits into
mainfrom
release/v1.2.0
Jul 9, 2026
Merged

release: v1.2.0 — TIME columns, WEEK()/DATEADD(), BROWSE cell validation, Overtime demo#57
DDecoene merged 17 commits into
mainfrom
release/v1.2.0

Conversation

@DDecoene

@DDecoene DDecoene commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Ships the v1.2.0 milestone (#43, #44, #45, #46, #50, #52 — all closed). package.json is already at 1.2.0; v1.2.0 gets tagged on this merge commit.

Highlights

TIME columns (#43) — TIME / TIME(n), storing canonical HH:MM. The TIME(15) qualifier requires quarter-hour minutes and is enforced on write.

WEEK() (#44) and DATEADD() (#52) — ISO-8601 week numbers, and the day arithmetic W3Script simply didn't have. Both computed in UTC, so month/year/leap-day boundaries are exact.

BROWSE per-cell validation (#45) — an invalid edit keeps the cell in edit mode, outlined red, explaining why; the error clears as you fix it. Rules live in one shared module and run on the client for instant feedback and on the server as the authoritative check. grid-edit previously bypassed the Executor entirely and wrote to SQLite unvalidated.

demos/overtime.prg (#46) — an Overtime Tracker that exercises all of the above: per-employee weekly schedules, TIME(15) timesheets edited in the validated grid, a live overtime balance (banked minus leave), a grouped report, CSV export.

Test hardening (#50) — after two bugs shipped through a 283-test suite, this closes the blind spots that let them: a strict CREATE TABLE grammar (it used to absorb unknown tokens and invent columns), golden pins on the demo schemas, coverage for every grid WS message, and npm run coverage.

Bugs found and fixed along the way

Several were pre-existing and user-visible:

  • NUM(8,2) created a phantom column literally named 2 — the shipped PRODUCTS, DEALS and SALES demo tables all carried it.
  • Index metadata leaked across databases: opening PEOPLE in one database silently activated an index defined on a different database's PEOPLE, pointing record order at a column that need not exist there. Same defect in the new column-type store. Both now keyed by (db, table, …), with a data-preserving migration.
  • A bare INPUT "…" TO var at the REPL silently discarded the typed value.
  • The BROWSE validation message was clipped away by overflow: hidden — users saw a red border and no reason. The tests passed because toContainText/toBeVisible don't account for ancestor clipping; they now use toBeInViewport().

Verification

  • npm test379/379 vitest (was 265 at v1.1.1).
  • npx playwright test94/94 (was 73).
  • npx tsc --noEmit clean. Both CI jobs green on every PR into this branch.
  • The overtime demo was also driven by hand end-to-end.

Screenshots and the README demo GIF are regenerated against v1.2.0, including a new screenshot-grid-validation.png.

Carried forward: #34 (Assistant: surface JOIN + work areas) moved to the new v1.3.0 milestone — it predates this line and was never in scope here.

DDecoene added 17 commits July 9, 2026 18:20
Bump version to 1.2.0 and add Unreleased changelog heading for
TIME columns, WEEK(), BROWSE cell validation, and the Overtime
Tracking demo.
Add TIME/TIME(n) as a first-class CREATE TABLE column type, stored
as HH:MM text. The optional TIME(n) qualifier requires minutes to be
a multiple of n (e.g. TIME(15) for quarter-hour increments).

A new ColumnMetaStore (data/system.sqlite3, same pattern as
IndexStore) tracks base type + qualifier per column since SQLite's
own type affinity can't distinguish TIME from CHAR/DATE (all TEXT).
REPLACE ... WITH validates against it and rejects malformed or
off-granularity values instead of silently coercing them; LIST
STRUCTURE prints the declared type. The New table wizard and Modify
structure wizard both offer TIME as a column type.
feat: TIME column type with granularity qualifier (#43)
WEEK(date) returns the ISO-8601 week number (1-53): Monday-start
weeks, week 1 being the week that contains the year's first Thursday.
Early-January dates therefore report the previous year's week 52/53,
and late-December dates week 1 of the next year.

Accepts ISO YYYY-MM-DD or MM/DD/YY. Computed in UTC so a local
timezone offset can't shift the day, and ISO-shaped but impossible
dates (2024-02-30, 2023-02-29) return 0 rather than silently rolling
over the way Date.UTC does.

Registered in Parser's BUILTIN_FUNCTIONS — implementing a built-in
without whitelisting it there is how the #4 built-ins shipped broken.
Covered directly (Builtins.test.ts), through the parser
(BuiltinsParse.test.ts), and end-to-end in the REPL
(parity-commands.spec.ts), asserting the printed value.

Also refreshes the doc test counts and adds ColumnMetaStore.ts /
TimeType.test.ts to the CLAUDE.md trees, both missed in #43.
feat: WEEK() ISO-8601 week-number built-in (#44)
Grid edits are now checked against the column's declared type before
they commit. An invalid edit keeps the cell in edit mode, outlined in
red, showing why; the error clears as soon as the value becomes valid,
and Esc abandons the edit leaving the original value intact.

The rules live in src/shared/cellValidation.ts and run on both sides:
Grid.ts for instant feedback, and Session's grid-edit handler as the
authoritative check. grid-edit previously bypassed the Executor and
wrote straight to SQLite with no validation at all, so a WS message
could plant any value in any column.

Serving that needed the declared type of every column, not just TIME:
SQLite's affinity cannot tell TIME/DATE/CHAR apart (all TEXT), nor
LOGICAL from INT (both INTEGER), nor recover a NUM(p,s) qualifier.
CREATE TABLE now records all of them, grid-open ships them to the
client, and LIST STRUCTURE prints them as declared.

Two bugs surfaced and are fixed here:

- CREATE TABLE t (price NUM(8,2)) created a phantom column named "2"
  of type ")" — the parser read the precision and then treated the
  scale as the next column definition. The PRODUCTS, DEALS and SALES
  demo tables all carried the stray column.

- Column metadata was keyed by (table, column) with no database
  scope, so same-named tables in different databases overwrote each
  other's declared types. Now keyed by (db, table, column), with a
  migration for the pre-#45 schema.
feat: BROWSE per-cell validation by declared column type (#45)
Two bugs shipped through a 283-test suite. Both were structural blind
spots in how the tests were written, not bad luck:

- Nearly every assertion is `toContain` on rendered text, which can
  prove a thing is present but never that something extra is absent.
  A phantom column named "2" therefore sailed through every LIST and
  LIST STRUCTURE check.
- Four of twelve ClientMessage types had no test at all. grid-edit
  wrote straight to SQLite unvalidated because the grid tests only
  opened the grid and pressed Escape.

Root cause first: parseCreate used to absorb any token it did not
understand and invent a column from it. It now rejects a malformed
column list — missing comma, missing type, unclosed paren, a third
type argument — naming the offending column and creating nothing.

Then the coverage those bugs needed:

- DemoSchemas.test.ts pins the exact column list of every table the
  demos create, and asserts no column name is a bare number.
- GridMessages.test.ts drives grid-edit, grid-delete, grid-new-row and
  grid-refresh, asserting the effect on the database.
- CreateTableParse.test.ts covers the strict grammar both ways.

Writing those tests immediately found two more bugs, fixed here:

- Index metadata was keyed by table name alone, so opening PEOPLE in
  one database silently activated an index defined on a different
  database's PEOPLE — pointing record order at a column that need not
  exist there. Now keyed by (db, table, tag). Existing definitions are
  adopted into the database that owns the table; ambiguous ones are
  dropped and must be recreated with INDEX ON.
- A bare `INPUT "..." TO var` at the REPL discarded the typed value:
  form-submit only stored values when a continuation existed, which
  never happens for a single statement.

Also removes the input-request/input-response message types (declared
but never sent or handled), teaches the New table wizard to emit
NUM(p,s), and adds `npm run coverage` so untested modules stop hiding.
test: strict CREATE TABLE, demo schema pins, grid message coverage (#50)
DATEADD(date, n) returns the ISO date n days later; n may be negative.
W3Script had no date arithmetic at all — CTOD/DTOC only reformat and
YEAR/MONTH/DAY only decompose — so there was no way to say "the day
after this one". demos/overtime.prg (#46) needs it to derive each
TIMESHEET.WORKDATE from the week's Monday.

Computed in UTC, so month, year and leap-day boundaries are exact and
no local timezone offset can shift the day. Impossible ISO dates return
'' rather than rolling over, matching WEEK().

WEEK() already parsed dates exactly this way, so that logic is
extracted into a shared parseDateUTC() rather than duplicated.

Registered in Parser's BUILTIN_FUNCTIONS, and covered directly, through
the parser, and end-to-end in the REPL — the three-way coverage the #4
built-ins lacked when they shipped broken.
feat: DATEADD() date-arithmetic built-in (#52)
An overtime tracker, and the showcase for this release's engine work:
TIME(15) columns validated per-cell as you type in BROWSE, WEEK() for
the ISO week number, and DATEADD() to walk a week's Monday to Friday.

Five linked tables (EMPLOYEES, SCHEDULEDAYS, TIMESHEET, WEEKSUMMARY,
LEAVETAKEN) across five work areas with relations into EMPLOYEES.
Each employee has their own weekly schedule, so standard hours are a
real per-employee sum rather than a flat 40. Overtime is banked per
week and drawn down as leave; the balance is computed live from the
source rows, so re-editing a week cannot leave a stale running total.

Two engine constraints shaped the implementation, both verified before
writing the program:

- SUM ... FOR and SET FILTER are spliced raw into SQL and cannot see a
  memory variable, so the per-employee balance accumulates in a
  DO WHILE / IF loop rather than the crm.prg SUM ... FOR ... TO idiom,
  whose condition is a literal.
- Field references only resolve where the row cache is primed (IF,
  DO WHILE, @ SAY), which is why the time arithmetic lives inside the
  loop body.

Times are HH:MM text, so worked hours convert to minutes first:
(TIMEOUT - TIMEIN) - (BEND - BSTART), rounded to two decimals.

Seeds a grouped report and is reachable from the splash screen, HELP,
and the Assistant (Programs -> Run Overtime demo). The DemoSchemas
golden pins from #50 caught the new tables immediately and now cover
them.
feat: demos/overtime.prg — Overtime Tracking demo (#46)
…nshots

The per-cell validation error (#45) was never visible. Grid cells set
overflow:hidden for the ellipsis on long values, which clipped the
absolutely-positioned tooltip to nothing — an invalid edit showed a red
border and no explanation of what was wrong.

The e2e tests passed the whole time: toContainText and toBeVisible do
not account for clipping by an ancestor's overflow. The assertion now
uses toBeInViewport(), which is backed by an IntersectionObserver and
does. Verified it fails against the unfixed CSS (viewport ratio 0).

Found by looking at a screenshot of the feature rather than trusting a
green test — now recorded in CLAUDE.md's test-discipline notes.

Also regenerates every screenshot and the README demo GIF against
v1.2.0 (they still showed the v1.1.1 banner), and adds a new
screenshot-grid-validation.png plus its capture step and a README
section for it.
fix: BROWSE validation message was clipped away; refresh docs + screenshots
Date the v1.2.0 heading, drop the "in progress" marker from the
roadmap, and record the screenshot/GIF refresh.
release: finalize CHANGELOG and roadmap for v1.2.0
@DDecoene DDecoene merged commit 532799d into main Jul 9, 2026
4 checks passed
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.

1 participant