Skip to content

feat(data list): rich task-grouped listing (task / size / records / format)#376

Merged
LukasWodka merged 13 commits into
developfrom
feat/data-list-table
Jul 21, 2026
Merged

feat(data list): rich task-grouped listing (task / size / records / format)#376
LukasWodka merged 13 commits into
developfrom
feat/data-list-table

Conversation

@LukasWodka

@LukasWodka LukasWodka commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

What

tracebloc data list was a bare bullet list of table names ("looks very empty").
It now renders a rich, task-grouped table — each dataset's real task,
split (train/test), record count, size, format, classes, and freshness:

  Datasets in hospital-workspace — 5 · 1.38 GiB

  Image classification · 2
  ✔ chest_xray_train     train   5232 images     1.10 GiB   jpg · 2 classes           2h ago
  ✔ chest_xray_test      test     624 images   138.50 MiB   jpg · 2 classes           2h ago

  Text classification · 1
  ✔ imdb_reviews_train   train  100000 documents  127.00 MiB  txt · 2 classes          3h ago

  Tabular regression · 1
  ✔ housing_train        train   20640 rows        2.10 MiB   csv · 8 cols             1d ago

  Time-series classification · 1
  ✔ sepsis_vitals_train  train   40336 rows       20.50 MiB   csv · 6 cols · 2 classes  5h ago

Per dataset: status · split · records (with the right noun) · size · format ·
classes · ingested
. Datasets are grouped by their real task (the canonical
registry label — "Image classification", "Time-series classification", …),
ordered by family. Framework tables (the ingest-run journal) are hidden unless
--all. --output-json emits per-dataset objects. Validated against the real
test0721 (32 datasets, every task).

How (read-only, no backend round-trip)

  • push.ListDatasetsDetailed queries the mysql pod: an information_schema pass
    (create-time, data_length, columns), a per-table UNION (data_intent,
    COUNT(*), COUNT(DISTINCT label), extension), and the ingest-run journal
    for each dataset's task. SQL goes over stdin so backtick-quoted names need
    no shell escaping; a one-shot retry survives a table dropped mid-listing.
  • Task is read from tracebloc_ingest_runs.task — only when that column
    exists (guarded), so it degrades gracefully. The group label and the modality
    (family / record noun / format) both come from the CLI's category registry
    (push/category.go, the same source data ingest uses), so they never drift
    as tasks are added. Datasets ingested before task-persistence fall back to
    inferred modality (extension / column shape).
  • Size comes from where the data actually lives: a du of the shared PVC for
    file datasets (images/text), and the table's data_length for
    row-based datasets (tabular / time-series). The file-vs-row signal is the
    per-row extension — filename/extension are framework columns on every
    table, so only a non-empty extension marks staged files. Empty datasets stay
    sizeless (), matching their ⚠ flag.

Addressed the two known limits from the first pass

The initial version could only infer modality and left row-based size blank.
Both are now solved:

Kept intentionally: N classes shows only when the label repeats
(classes < records), so a continuous regression target isn't mislabelled.

Tests

Parsers (schema / data / du / task), size selection (du vs data_length, empty →
, stray-PVC-dir guard), registry-driven task label + family, modality-inference
fallback, column alignment (rune-width), the grouped render, --all, and the JSON
shape. gofmt / vet / staticcheck / deadcode / file-budget green; e2e (kind) green;
Bugbot clean.

🤖 Generated with Claude Code

`tracebloc data list` was a bare list of table names. It now groups datasets by
modality (Image / Text / Tabular / Time-series) and shows, per dataset: split
(train/test), record count with the right noun (images / documents / rows), real
byte size, format (jpg/txt, or csv · N cols), class count, and how long ago it
was ingested — plus a summary line and --all to reveal the framework tables
(the ingest-run journal) hidden by default.

Data sources (read-only, no backend round-trip):
- push.ListDatasetsDetailed queries the mysql pod: an information_schema pass
  (create-time + columns), then a per-table UNION for data_intent / COUNT(*) /
  COUNT(DISTINCT label) / extension. Real datasets are tables with a data_id
  column; framework tables (no data_id) are flagged System.
- Size comes from a du of the shared PVC on the jobs-manager pod (where files
  live). The DB size is metadata-only for file datasets and InnoDB-padded for
  tiny tables — both misleading — so it is deliberately not used. Best-effort:
  if the du is unreachable, size shows "—".

Notes / limits (validated against a real 32-dataset cluster):
- The specific task (image_classification vs object_detection, …) is NOT stored
  in the cluster DB, so grouping is by inferred modality (extension + time/
  sequence columns), not the 16 exact tasks. Exact-task grouping needs the
  ingestor to persist the task.
- Tabular/time-series show size "—": their data is DB rows, not PVC files.
- "N classes" shows only when the label repeats (classes < records), so a
  continuous regression target isn't mislabelled as classes.

--output-json emits per-dataset objects (modality/intent/records/classes/format/
size_bytes/ingested); --all includes system tables. Tests cover the parsers
(schema/data/du), modality inference, humanBytes, the grouped render, --all, and
the JSON shape. gofmt/vet/lint/deadcode/file-budget green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka LukasWodka self-assigned this Jul 21, 2026
@LukasWodka

Copy link
Copy Markdown
Contributor Author

@BugBot run

Comment thread internal/cli/data_list.go
Comment thread internal/push/list_detailed.go Outdated
Two Bugbot findings on #376:

- Use the existing push.SharedRoot for the du path instead of a new
  SharedDataPath const, so a future mount-path change can't leave the size du
  pointed at a stale path while staging/teardown move.

- datasetModality only keyed Image/Text off a populated extension, so an
  ingested-but-empty (0-row) file dataset — NULL extension — fell through to
  Tabular/"Other" with a misleading "csv · 0 cols". Now the Tabular branch
  requires records (an empty table's modality is genuinely unknowable → "Other"),
  and formatCell renders "—" for an undetermined modality instead of implying
  csv. Tests cover the empty-file case + the neutral format.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

@BugBot run

Comment thread internal/push/list_detailed.go Outdated
Comment thread internal/cli/data_list.go Outdated
Comment thread internal/push/list_detailed.go Outdated
Comment thread internal/cli/data_list.go Outdated
Three Bugbot findings on #376:

- HIGH: the per-table data pass hard-coded COUNT(DISTINCT label), but label is
  optional (self-supervised text tasks omit it) — one such table would fail the
  whole UNION ALL and exit 7 for the entire listing. Each per-table SELECT now
  references only the columns that table actually has (label / data_intent /
  extension), falling back to a constant otherwise.
- Reuse exported push.HumanBytes instead of a private humanBytes that had
  drifted (%.1f vs %.2f, extra TiB), so data list matches ingest/over-cap sizes.
- --output-json keeps `datasets` as the []string of names (the additive-only
  contract in docs/json-output.md forbids re-typing existing fields) and moves
  the rich per-dataset objects to a new `details` array; doc updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

@BugBot run

Comment thread internal/push/list_detailed.go
Comment thread internal/cli/data_list.go
Comment thread internal/cli/data_list.go
Comment thread internal/cli/data_list.go
…y-state polish

Four Bugbot findings on #376:

- Medium: relativeTime parsed the MySQL create_time wall clock as UTC, skewing
  every "Xh ago" by the server's tz offset. Now the schema pass also selects
  UNIX_TIMESTAMP(create_time) (a tz-safe epoch) and the "ago" math uses that;
  the ISO string is kept for display/JSON.
- Medium: the size du treated any non-zero exit as total failure, blanking every
  dataset's size if one shared-PVC entry was unreadable (jobs-manager can hit
  EACCES). Appended `|| true` so readable entries still yield sizes.
- Low: under --all, system tables rendered "0 B" (they aren't du-sized); now they
  show "—" like other unknown sizes.
- Low: when every table is a system table and --all is off, the empty-state
  branch now still prints the "N system table(s) hidden — show with --all" hint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

@BugBot run

Comment thread internal/cli/data_list.go
Comment thread internal/push/list_detailed.go
… lists

Two Bugbot findings on #376:

- Medium: datasetModality only matched "txt" for Text, but the ingestor accepts
  both .txt and .text (preflight accept-set / textExtensions). A .text dataset
  was grouped as "Other" with the wrong noun/format. Now matches txt + text.
- Low: the schema query's GROUP_CONCAT of column names could truncate at MySQL's
  default group_concat_max_len (1024), under-counting feature columns / dropping
  modality markers on wide tables. Raise it to 1 MiB via a leading SET SESSION.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

@BugBot run

Resolve internal/cli/data_list.go: keep the rich modality-grouped
renderer (--all, per-dataset size/records/format/split, details JSON)
and adopt #370's cleaner-output conventions on the shared surface —
no banner, honest empty state via invokedName() + Para.

Fold #370's reservedTables set into ListDatasetsDetailed's System-table
detection so the ingest-run journal and ingest-meta store are hidden by
default (alongside the existing data_id heuristic), consistent with how
ListDatasets now filters them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

@BugBot run

Comment thread internal/cli/data_list.go Outdated
Bugbot (commit 1cfdcc3): datasetRow pinned size to %9s and records to
%-12s, but HumanBytes emits 10-char values ("100.00 KiB") and text
counts reach "100 documents" (13). Go treats those widths as minimums,
so a common large dataset printed its full value and pushed size /
format / freshness right on that row alone — breaking the aligned table.

Size the records and size columns to their widest cell, like name and
format already are. Measure and pad by display width (runes) via
padRight/padLeft, not fmt's byte-based %*s, so the multi-byte em dash
and middot don't skew alignment either. Add TestRenderDataList_
ColumnsAlign asserting the format column starts at the same offset on
a small row and a wide (100000-document / 100 KiB) row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

@BugBot run

Comment thread internal/cli/data_list.go
Comment thread internal/cli/data_list.go
Two follow-ups from Bugbot on 5c0b0b4:

- Format-width cap without truncate (Low): fmtW was capped at 28 but
  format cells are never truncated, so a wide "csv · N cols · M classes"
  would overflow and shift freshness — reintroducing the overflow the
  dynamic sizing just fixed. Drop the cap: format cells are
  system-generated and bounded, so sizing to content stays aligned and
  keeps the full text. Names keep cap + truncate (user-controlled).

- Unreachable "files" fallback (Medium premise): the base="files" arm in
  formatCell's Image/Text branch was dead — modality is extension-driven,
  so the extension is always set there. Move "files" to the reachable
  default branch, gated on a filename column, so a populated file table
  whose extension wasn't recorded reads "files" instead of "—" (it stays
  "Other" — image vs text is genuinely undeterminable without the
  extension). Extract hasCol and reuse it in datasetModality.

Add TestFormatCell_FileWithoutExtension.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

@BugBot run

Comment thread internal/push/list_detailed.go
Comment thread internal/push/list_detailed.go Outdated
Comment thread internal/cli/data_list.go
…ame cap

Three follow-ups from Bugbot on 8418436:

- JSON ingested time lacked a timezone (Medium): the `ingested` field
  used a session-tz DATE_FORMAT string that looked like ISO-8601, while
  the human "ago" text used the UTC epoch — they disagree when the MySQL
  session tz isn't UTC. Make CreatedUnix (the UTC epoch) the sole time
  source and emit `ingested` as explicit UTC RFC3339 (Z-suffixed) via
  ingestedISO. Drop the naive DATE_FORMAT column + the CreatedAt field.

- Listing failed if a table vanished (Medium): the data pass is one
  UNION ALL over the schema snapshot, so a table dropped by a concurrent
  `data delete` between the two queries failed the whole listing (exit
  7). Retry the schema+data pair once — a fresh snapshot drops the
  vanished table and the rebuilt query succeeds; a broken mysql still
  fails the retry and surfaces the error.

- System names capped without truncate (Low): the --all System section
  capped sysNameW at 24 but padRight never truncates, so a >24-char
  system name would overflow — the same false-cap class as the format
  column. Drop the cap; system names are framework-generated and short.

Tests: schema fixtures move to the 3-column layout; add the vanished-
table retry test and a UTC-explicit `ingested` assertion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

@BugBot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 70b07e2. Configure here.

…asets

Read each dataset's task from the run journal (tracebloc_ingest_runs.task,
persisted by data-ingestors) and group by it, instead of only inferring
modality:

- list_detailed.go reads task via a guarded query — only when the journal
  actually has the column (known from the schema pass), so a cluster whose
  ingestor predates it is never queried for it. DatasetInfo gains Task.
- datasetModality is now authoritative when a task is recorded (task→modality
  map), falling back to shape inference for datasets ingested before the
  column shipped. Groups are labelled by the humanized task ("Image
  classification"), ordered by modality family; JSON gains `task`.

Fill the size column for row-based datasets:

- fetch information_schema.data_length and use it as the size for tabular /
  time-series datasets (whose data lives in the table, not the PVC). File
  datasets keep the du-of-PVC size. The file-vs-row signal is the per-row
  Extension, NOT a filename column — filename/extension/annotation are
  framework columns on every table, so only a non-empty extension marks
  staged files (caught against the live cluster: tabular tables carry a
  filename column yet are row-based).

Verified against test0721 (32 datasets): pre-persistence datasets fall back
to inferred modality and now show a DB size instead of "—".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

@BugBot run

Comment thread internal/push/list_detailed.go
Bugbot on 0832e30: an empty dataset (0 rows) has an empty extension too, so
it fell into the row-based branch and took data_length — InnoDB's one-page
(16 KiB) allocation, not real data — implying an empty, ⚠-flagged dataset
holds a page of content. Only use DBBytes for row-based datasets that
actually have rows (Extension == "" && Records > 0); leave an empty dataset
sizeless ("—"), consistent with its empty flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

@BugBot run

Comment thread internal/cli/data_list.go Outdated
…istry

Bugbot on 7f75028: taskModality + humanizeTask re-enumerated the ingest
tasks, so section headers diverged from the rest of the CLI ("Time series
classification" vs "Time-series classification", "Seq2seq" vs
"Sequence-to-sequence") and would drift as the registry gains a task —
exactly what category.go (cli#74) exists to prevent.

Drop both and use the registry (push):
- groupLabel → push.Lookup(task).Label for a known task (canonical,
  matches `data ingest`), the raw id for an unknown task, else the
  inferred modality.
- datasetModality → push.IsImage/IsText/IsTabular for a known task. The
  registry files time-series under FamilyTabular, so a known time-series
  task now reports the Tabular family (its distinct label still heads its
  own group); the "Time-series" bucket remains only for the inference
  fallback (datasets ingested before the task was recorded).

No behaviour change for pre-persistence datasets (verified against
test0721: same Image/Text/Tabular/Time-series fallback groups).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

@BugBot run

Comment thread internal/push/list_detailed.go
…t's size

applyDatasetSizes took any du hit as authoritative before checking
Extension, so a row-based table with a dest dir on the shared PVC got
that directory's size instead of its DBBytes — the documented
file-vs-row rule never ran. Gate the du lookup on Extension (the
file-bearing signal) and cover the stray-dir case in the test.

Bugbot finding on #376.

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

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 00dceda. Configure here.

@LukasWodka LukasWodka changed the title feat(data list): rich modality-grouped listing (size / records / format) feat(data list): rich task-grouped listing (task / size / records / format) Jul 21, 2026
@LukasWodka
LukasWodka merged commit 685cf0b into develop Jul 21, 2026
23 checks passed
@LukasWodka
LukasWodka deleted the feat/data-list-table branch July 21, 2026 14:27
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