Skip to content

feat(data-repo): runlog-index GitHub Action keeps runlogs/<folder>/index.json current - #187

Merged
mbreiser merged 2 commits into
mainfrom
feat/runlog-index-action
Sep 7, 2026
Merged

feat(data-repo): runlog-index GitHub Action keeps runlogs/<folder>/index.json current#187
mbreiser merged 2 commits into
mainfrom
feat/runlog-index-action

Conversation

@mbreiser

@mbreiser mbreiser commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Option 1 from today's review: the data repo keeps its own catalog index. A workflow template, scripts/data-repo-workflows/runlog-index.yml, runs on every push under runlogs/, rebuilds only the touched folders' index.json with scripts/build-runlog-index.py --github (64 KB head + 4 KB tail per file, never a full log), and commits them with GITHUB_TOKEN.

  • Loop guard: the paths filter excludes runlogs/**/index.json, so the bot's own commits don't retrigger.
  • Concurrency group: simultaneous pushes from two rigs queue instead of racing; the script also retries a stale-sha 409/422 on PUT.
  • workflow_dispatch rebuilds every folder by hand.
  • Sparse checkout of .github only — the workflow never downloads the multi-GB logs.
  • --folder is now repeatable.

Arena Studio does not write the index (option 2 dropped): one writer, no change to the run-log commit path while experiments are running, and hand-pushed large logs, migrations and deletions are all covered because each folder is rebuilt from what is present.

Installed into reiserlab/cshl-2026-course today (.github/workflows/runlog-index.yml + .github/scripts/build-runlog-index.py) and verified with a throwaway push (see PR comment). The seed-script install step and the setup-doc section go into #178 (those files live only on that branch).

🤖 Generated with Claude Code

mbreiser and others added 2 commits September 6, 2026 18:18
…<folder>/index.json

Option 1 from the review: the data repo itself keeps the catalog index current.
scripts/data-repo-workflows/runlog-index.yml (template installed into each data
repo) runs on every push under runlogs/ (ignoring its own index.json commits),
serializes concurrent runs, sparse-checks-out only .github, computes the touched
folders from the push payload (added/modified/removed), and rebuilds just those
via build-runlog-index.py --github with GITHUB_TOKEN. workflow_dispatch rebuilds
all folders. Arena Studio does NOT write the index (option 2 dropped: one writer,
no commit-path change on running rigs, covers hand pushes/migrations/deletions).

build-runlog-index.py: --folder is repeatable; index PUT retries on a stale-sha
409/422. Comments/README updated to name the Action as the writer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ange reads; skip unchanged index.json

First live run rebuilt ALL folders (push-payload parse found no paths) and hit
a transient connection reset on one of ~340 range reads. Detection now uses
GET /compare/{before}...{sha} (paginated); raw range reads retry 4x; an index
that is byte-identical is not re-committed (no no-op bot commits).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@mbreiser

mbreiser commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Live verification on reiserlab/cshl-2026-course (2026-09-06)

Installed .github/workflows/runlog-index.yml + .github/scripts/build-runlog-index.py (byte-identical to this branch), then pushed and deleted a 6-line throwaway log under runlogs/bench00/:

event run detection rebuilt bot commits
add throwaway success changed files: 1 → folders: bench00 bench00 only (4 runs) 1
delete throwaway success changed files: 1 → folders: bench00 bench00 only (3 runs) 1
  • Index after add contained idxtest3 with duration_s 123.456 / complete: true; after delete it is back to the 3 real bench00 runs. No re-trigger from the bot's own index commits (loop guard works).
  • Earlier iteration (before the compare-API change) fell back to rebuilding all 12 folders and hit one transient ConnectionResetError on ~340 range reads — hence the read retries and the unchanged-index skip in this PR.
  • Follow-ups on the feat(studio): data-repo registry — lab repo picker, "Rig id", no default repo (v0.72) #178 branch (files live only there): scripts/seed-data-repo.sh installs both files into new data repos; docs/development/data-repo-setup.md gets the index section. Arena Studio does not write the index.

@mbreiser
mbreiser merged commit 10c49c5 into main Sep 7, 2026
@mbreiser
mbreiser deleted the feat/runlog-index-action branch September 7, 2026 22:19
mbreiser added a commit that referenced this pull request Sep 7, 2026
…ex.json

isRunlogName accepted bare .json, and #188 applies it to directory listings
(data-repo browse, URL index) and the corpus parity script. Since #187 every
runlogs/<folder>/ carries an index.json catalog, so the dashboard would list
each folder's index as a run and the corpus gate failed on all 12 of them
(174/174 logs themselves pass). Narrow the shared filter to .jsonl|.ndjson
(+ .gz); the drop handler keeps accepting a user-dropped .json to match the
file input's accept list. Vendored copy synced; test updated.

Corpus gates on this branch (course clone @ 37d003e, 174 logs):
  scripts/runlog-v2-corpus.py             174/174 ok
  dashboard/data-browser/tests/corpus-v2-parity.js  174/174 ok (was 174 ok + 12 index.json FAIL)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mbreiser added a commit that referenced this pull request Sep 9, 2026
…ex.json

isRunlogName accepted bare .json, and #188 applies it to directory listings
(data-repo browse, URL index) and the corpus parity script. Since #187 every
runlogs/<folder>/ carries an index.json catalog, so the dashboard would list
each folder's index as a run and the corpus gate failed on all 12 of them
(174/174 logs themselves pass). Narrow the shared filter to .jsonl|.ndjson
(+ .gz); the drop handler keeps accepting a user-dropped .json to match the
file input's accept list. Vendored copy synced; test updated.

Corpus gates on this branch (course clone @ 37d003e, 174 logs):
  scripts/runlog-v2-corpus.py             174/174 ok
  dashboard/data-browser/tests/corpus-v2-parity.js  174/174 ok (was 174 ok + 12 index.json FAIL)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mbreiser added a commit that referenced this pull request Sep 10, 2026
…v0.73 readers (#186, #188) (#188)

* feat(studio): gzip run-log commits (.jsonl.gz), behavior_v2 default, bridge log-level ack (PR 2 of runlog-behavior-v2-plan) — Studio v0.72

Studio side of docs/development/runlog-behavior-v2-plan.md (Part 1 §5 + Part 2).
Stacks on PR #183 (bridge). Readers (dashboard / replay viewer) are PR 3 — until
it lands, .jsonl.gz files need gunzip or `bridge.py --convert`.

- js/fictrac-bridge-client.js: LOG_LEVELS [behavior_v2, behavior_v1, full]
  (v2 default); handles hello_ack (bridgeInfo, bridgeSupportsLevel) and
  log_control_ack (ackedLogLevel, cleared on every setLogging and on close);
  new 'loglevel' event + an err log line when the bridge cannot write the
  requested level; waitForLogLevelAck(ms) → acked level or null (old bridge).
- js/studio-github.js: gzipBytes (CompressionStream) + isGzip; Git Database
  builders (reqCreateBlob/reqGetCommit/reqCreateTree/reqCreateCommit/
  reqUpdateRef, allowlisted, fast-forward only); directCommitLarge (7-call
  blob→tree→commit→ref sequence, per-step errors); commitFile routes by size
  (LARGE_FILE_BYTES = 30 MiB) and reports via + bytes.
- arena_studio.html v0.72: File ▾ → Run logging offers behavior_v2 (default) /
  behavior_v1 (legacy) / full; Console mirror follows the bridge's ack (⚠ on
  mismatch); run start awaits the ack ≤ 800 ms, names the level in the banner
  + transcript and warns "bridge too old for behavior_v2 — logging behavior_v1"
  (or "did not confirm" for a pre-3.0 bridge); run_metadata gains log_format;
  commitRunLog gzips the export → runlogs/<bench>/<name>.jsonl.gz via
  GH.commitFile (raw .jsonl fallback without CompressionStream) and shows
  raw → gz size + the path used.
- Tests: test-fictrac-bridge-client.js 42 → 71 checks (levels, hello_ack,
  log_control_ack, mismatch, waiters, close reset); test-studio-github.js
  118 → 160 (gzip round trip via zlib, git-db builders + token containment,
  directCommitLarge sequence + failure step, commitFile routing, Studio wiring
  assertions on the HTML).
- Docs: release notes v0.72, cshl-pipeline-test-plan size-limits, CLAUDE.md
  Studio rule, plan doc status + PR 2 notes (merge with/after PR 3).

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

* feat(readers): open .jsonl.gz + behavior_v2 run logs everywhere — shared js/runlog-format.js (PR 3 of runlog-behavior-v2-plan) — Studio v0.73

Reader side of docs/development/runlog-behavior-v2-plan.md (Part 3). Stacks on
the Studio PR (v0.72). With this, everything that opens a run-log FILE inflates
gzip on the magic bytes and expands behavior_v2's compact arena echoes back to
the v1 `arena_command` object at parse time, so every existing consumer
(p3LedEpochs, stall forensics, replay frame decoding) is unchanged.

- js/runlog-format.js (new; classic dual-export, vendored byte-identical at
  dashboard/data-browser/vendor/): isGzip, isRunlogName, stripGz, inflateIfGzip,
  readRunlogText (string|bytes|ArrayBuffer|Blob → text), readRunlogPrefixText
  (truncation-tolerant gunzip for the catalog's 64 KB metadata reads),
  isArenaArray, expandV2Line (exact v1 key order; timeout ⇒ status/echo/ok all
  null), compactV1Line, detectFormat, createNormalizer() (per-file state
  machine), convertV1ToV2Text / convertV2ToV1Text (JS mirror of the bridge
  converter, for tests + parity).
- Dashboard: analysis-core.js parseJsonl normalizes every line, run.logFormat +
  run.rawBytes, parseFilename strips .gz; github-repo.js fetchRaw reads bytes and
  inflates (prefix mode inflates a truncated gz); app.js loaders (repo / URL /
  local server / dropped file) go through readRunlogText, catalog filters accept
  .jsonl.gz, new size column (compressed size; inflated size + format in the
  hover), ?v= cache busters bumped, file input accepts .gz.
- Replay: js/runlog-replay.js parseRunLog normalizes records (loads
  js/runlog-format.js first in arena_studio.html; degrades to "v2 echoes
  skipped" + console warning under a stale cache); js/arena-studio-alt.js replay
  picker lists .jsonl.gz and inflates the picked file. arena_replay_viewer.html
  (protocol-fed) and js/studio-runlog-adapter.js (no file reads) unchanged.
- Tests: tests/test-runlog-format.js (new, 86 checks, in pixi run test — also
  enforces the vendored copy); tests/test-runlog-replay.js v2 case identical to
  the equivalent v1 (76 checks); dashboard test-analysis.js re-reads the P3
  fixture as v2 + gz asserting identical frames, arena_command objects,
  preference indices, LED epochs and page CSV rows, plus metadata from a gz
  prefix; dashboard/data-browser/tests/corpus-v2-parity.js runs the same over a
  whole clone (164/164 on cshl-2026-course origin/main).
- Browser-verified: the dashboard opens a converted .jsonl.gz via ?data= (52,369
  frames / 29 steps, size column "1.6 MB gz", no console errors).
- Docs: dashboard README, release notes v0.73, plan doc status + PR 3 notes,
  CLAUDE.md reader rule.

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

* fix(readers): run-log name filter must not match runlogs/<folder>/index.json

isRunlogName accepted bare .json, and #188 applies it to directory listings
(data-repo browse, URL index) and the corpus parity script. Since #187 every
runlogs/<folder>/ carries an index.json catalog, so the dashboard would list
each folder's index as a run and the corpus gate failed on all 12 of them
(174/174 logs themselves pass). Narrow the shared filter to .jsonl|.ndjson
(+ .gz); the drop handler keeps accepting a user-dropped .json to match the
file input's accept list. Vendored copy synced; test updated.

Corpus gates on this branch (course clone @ 37d003e, 174 logs):
  scripts/runlog-v2-corpus.py             174/174 ok
  dashboard/data-browser/tests/corpus-v2-parity.js  174/174 ok (was 174 ok + 12 index.json FAIL)

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

* fix(readers): restore strict mode in runlog-replay.js; mark rawBytes hover approximate

Codex cross-review 2026-09-09 (all three reviewers): the new `const Fmt` /
`let warnedNoFmt` declarations displaced 'use strict' so it became a
parenthesised expression after executable code — no longer a directive, the
replay IIFE silently ran in sloppy mode. Directive first again.

rawBytes is text.length (UTF-16 code units), not bytes; the catalog hover now
says ≈ and the field is annotated, rather than re-encoding a 50 MB string.

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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