Compare commits

..

77 Commits

Author SHA1 Message Date
Elijah Voigt dc7cfec897 feat(quotesdb): add admin verify endpoint, remove reset-auth-code UI
- Add GET /api/admin/verify — side-effect-free code check used by the
  admin unlock flow; registered before reset-auth-code in the router
- Remove "Reset auth code" section from admin panel (UI + dead API code);
  rotation is now CLI-only via `wrangler secret put ADMIN_AUTH_CODE`
- Add rotate-admin-code justfile recipe using pwgen for local key rotation
- Add pwgen to Nix dev shell
- Update OpenAPI spec with /api/admin/verify definition

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
Elijah Voigt 9418bd4b0b feat(quotesdb): support ADMIN_AUTH_CODE Cloudflare secret for admin auth
Merge qdb-api-d4a624 — ticket d4a624
4 months ago
Elijah Voigt dcbc659ec1 feat(quotesdb): support ADMIN_AUTH_CODE Cloudflare secret for admin auth
Add AppState to handlers, read ADMIN_AUTH_CODE from Worker env, gate
reset-auth-code with 409 when secret is active, update tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
Elijah Voigt bac6696c4b fix(quotesdb): make site footer stick to bottom of viewport
Yew's Renderer::new() renders into <body>, not #app, so the flex
column layout on #app had no effect. Move display:flex and
flex-direction:column to body so .main-content{flex:1} correctly
pushes .site-footer to the bottom of the viewport.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
Elijah Voigt deb3ec40f6 refactor(quotesdb): move db/handlers to lib modules, upgrade worker to 0.7, update infra
- Move src/bin/api/db/ and src/bin/api/handlers/ to src/db/ and
  src/handlers/ so they compile as library modules accessible to both
  the native binary and the Cloudflare Workers entry point
- Upgrade worker crate 0.5 → 0.7; add workers-api feature flag and
  cdylib/rlib crate-type to Cargo.toml
- Update flake.nix: add worker-build and just to the dev shell; bump
  flake.lock (nixpkgs + rust-overlay)
- Consolidate rate limit rules to one (Free plan allows only 1 rule
  per zone in the http_ratelimit phase)
- Update infra/worker.tf to deploy via wrangler rather than Terraform
  (Cloudflare provider v4 can't upload ES module + wasm bundles)
- Extend .gitignore to exclude *.wasm build artifacts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
Elijah Voigt b00f24ae85 fix(quotesdb): fix D1 exec() newline truncation in migrations, add justfile and migrate command
D1's exec() treats newlines as statement separators, causing multiline
CREATE TABLE statements to be truncated after the first line and return
"incomplete input: SQLITE_ERROR" on every request.

Fix: switch run_migrations() in D1Repository to use prepare(sql).run()
instead of exec(sql), which treats the full string as a single statement.

Also moves db and handlers modules from src/bin/api/ to src/ (library
modules), adds justfile with build/deploy/migrate recipes, adds
migrations/schema.sql for direct wrangler d1 execute usage, and adds
wrangler.toml for worker deployment configuration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
Elijah Voigt 66cbe67100 fix(quotesdb): fix report submit button always disabled, add auth code hint
- Remove captcha-gated disabled state on report submit button — Turnstile
  does not fire a DOM input event so captcha_solved never became true; rate
  limiting is handled at the WAF layer so the gate was incorrect
- Token is still read from the hidden Turnstile input at submit time if present
- Add explanatory hint below "Enter Auth Code" heading in AuthModal explaining
  the code was provided or generated when the quote was created
- Add .auth-modal__hint CSS class for the hint text

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 1728141517 fix(quotesdb): add PartialEq to report types, remove unused import
- Derive PartialEq on ReportSummary, ReportListResponse, ReportRow, and
  QuoteReports so they can be held in Yew state enums that require PartialEq
- Remove unused ApiError import from moderation_tab.rs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 5dadc23246 chore(quotesdb): close ticket 3f22f2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 25adf3897f feat(quotesdb): add admin moderation tab with report detail modal
Adds a Moderation tab to the admin page (visible after unlock) showing a
paginated list of reported quotes. Clicking a row opens a detail modal with
the full quote, all individual reports, and action buttons to delete the
quote, hide it, or dismiss the reports.

- api.rs: add ReportSummary, ReportListResponse, ReportRow, QuoteReports types
  and five admin_* async functions for the reports endpoints
- components/moderation_tab.rs: new ModerationTab function_component with
  paginated list, row-click loading state, and an inline detail modal
- components/mod.rs: expose moderation_tab module
- pages/admin.rs: introduce AdminTab enum and tab bar; wrap existing settings
  content in the Settings tab; add Moderation tab rendering ModerationTab
- style.css: add styles for admin tabs, moderation list table, and detail modal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 00a9a36510 chore(quotesdb): close ticket f4930e
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 77c131c08a feat(quotesdb): add hidden toggle on quote detail page
- Add HideAuth action variant to the Action enum
- Show yellow badge when quote.hidden is true
- Add Hide/Make Public toggle button in the actions bar
- Reuse AuthModal for the hide/unhide auth prompt
- Call POST /api/quotes/:id with { hidden: !current } and X-Auth-Code
- Update quote state on success; re-prompt on 403
- Add .page-quote__hidden-badge CSS styles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 01cddd6e95 chore(quotesdb): close ticket 354276
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt b6f03fd967 feat(quotesdb): add report button with modal on quote page
Adds a Report button to /quotes/:id that opens a modal containing:
- Optional reason textarea (max 256 chars) with live character counter
- Cloudflare Turnstile CAPTCHA widget (always-passes test sitekey as const)
- Submit button disabled until CAPTCHA token is non-empty
- Cancel button

On submit POSTs { reason?, captcha_token } to POST /api/quotes/:id/report.
Shows a success banner on 201 or an error via ErrorDisplay on failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 872ac9592c chore(quotesdb): close ticket a6e8ba
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt f90dc2dc5e feat(quotesdb): collapsible filter panel on browse page
Add a toggle button ("Filters ▼ / ▲") above the quote list on the
browse page. Filter controls are hidden by default and expand when
clicked. Each filter (Author, Tag, Date range) is on its own labelled
row with consistent styling. Existing API query logic is unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt e169d8b2cc chore(quotesdb): close ticket 6c5904
Mark admin moderation endpoints ticket as done following successful
implementation and test pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 684c58fdfe feat(quotesdb): add admin moderation endpoints
Implements ticket 6c5904 — five admin-authenticated endpoints for
moderation workflows:

  GET  /api/admin/reports              paginated list of reported quotes
  GET  /api/admin/reports/:id          full quote + all report rows
  DELETE /api/admin/reports/:id/quote  unconditionally delete a quote
  POST /api/admin/reports/:id/hide     set hidden=1 on a quote
  DELETE /api/admin/reports/:id/reports clear all reports for a quote

All endpoints require X-Admin-Code header; 403 on missing/wrong code.

DB layer additions:
- QuoteRepository trait gains list_reports, get_reports_for_quote,
  admin_delete_quote, hide_quote, and clear_reports methods
- New ReportRow, ReportSummary, ReportListResult, and QuoteReports
  types added to db/mod.rs
- Implementations in native.rs (rusqlite) and d1.rs (Cloudflare D1)

Tests added:
- 14 unit handler tests using MockRepo (3 per endpoint covering
  success, 404, and 403 cases)
- 5 integration tests using real SQLite via NativeRepository
- 10 DB-layer unit tests in native.rs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt df3a288c9f chore(quotesdb): add ticket for collapsible filter panel on browse page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt db71399b2f feat(quotesdb): add Cloudflare WAF rate limiting rules via OpenTofu
Adds infra/rate-limits.tf with a cloudflare_ruleset (phase: http_ratelimit)
implementing per-IP rate limits on all mutating API endpoints:
- PUT /api/quotes: 5 requests per 10 minutes (quote creation)
- POST /api/quotes/:id/report: 3 requests per hour (abuse reports)
- POST /api/quotes/🆔 10 requests per minute (quote updates)
- DELETE /api/quotes/🆔 10 requests per minute (quote deletes)

The report rule is ordered before the general update rule to ensure the
more-specific /report path matches before the broader /api/quotes/:id
pattern. Documents the approach, plan requirements, and layered protection
rationale in docs/ARCHITECTURE.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt ab76d35bd5 feat(quotesdb): add footer with contact email to all pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 4d4edae841 fix(quotesdb): remove spurious X-Auth-Code header, drop unused current param, fix error messages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 9827dcc5b9 feat(quotesdb): admin page auth-first flow, remove admin from nav
- Remove the Admin link from the top navigation bar; /admin remains
  reachable by direct URL but is no longer discoverable from normal
  browsing.
- Rework /admin to an auth-first flow: on load the page shows only a
  password input and an Unlock button. On success, the admin controls
  (submission lock/unlock, auth code reset) are revealed; on failure a
  clear error message is shown and the page stays locked. Refreshing
  always resets to locked state (code is in component state only).
- Add api::verify_admin_code() — calls POST /api/admin/reset-auth-code
  with new_code equal to the entered code, making the call idempotent
  on success (code unchanged) while still returning 403 on mismatch.
- Fix pre-existing wasm build breakage in quote.rs: UpdateQuoteInput
  gained a hidden field in an earlier ticket but quote.rs was never
  updated. Added hidden: None to the struct literal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 995fff4046 fix(quotesdb): use char count for reason validation, remove duplicate CountRow in d1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt eecdbba9d7 feat(quotesdb): add reports table and POST /api/quotes/:id/report endpoint
- Add CREATE_REPORTS migration constant (was unused — now wired in)
- Wire CREATE_REPORTS into run_migrations for both NativeRepository and D1Repository
- Add create_report to QuoteRepository trait with NotFound semantics
- Implement create_report in NativeRepository (two-step: existence check then insert)
- Implement create_report in D1Repository (two-step: COUNT check then insert)
- Add report_handler: POST /api/quotes/{id}/report, 201/400/404/500
- Register route before /{id} in router so static /report suffix wins
- Add create_report to MockRepo in handler tests
- Add handler tests: test_report_success, test_report_quote_not_found, test_report_reason_too_long
- Add DB tests: test_create_report_success, test_create_report_not_found
- Add ReportInput schema and /api/quotes/{id}/report path to openapi.yaml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 14cc879743 test(quotesdb): add hidden flag filter tests to native repository
Add three tests verifying hidden-quote filtering behaviour in
NativeRepository:

- list_quotes_excludes_hidden: hidden quotes do not appear in paginated
  listing results.
- get_random_quote_excludes_hidden: get_random_quote returns None when
  the only quote is hidden.
- get_quote_returns_hidden_quote: get_quote (direct ID lookup) still
  returns the quote when it is hidden.

Also refactor the inline row-mapping closure in list_quotes to use the
existing row_to_quote helper, eliminating duplicated column mapping
logic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 86c5e4990d feat(quotesdb): add hidden field to openapi spec
Add `hidden` (boolean, required) to the Quote response schema so all
GET responses reflect the field. Add `hidden` (boolean, optional) to
QuoteUpdateRequest so callers can toggle visibility via POST /api/quotes/:id.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt c59efdc373 feat(quotesdb): add hidden flag to quotes
- Add `hidden: bool` to the `Quote` struct and `hidden: Option<bool>` to
  `UpdateQuoteInput` in `src/lib.rs`
- Add `ALTER_QUOTES_ADD_HIDDEN` migration constant in `db/migrations.rs`
- Apply the ALTER TABLE migration in `NativeRepository::run_migrations` and
  `D1Repository::run_migrations` with try/ignore for idempotency
- Exclude hidden quotes from `list_quotes` (WHERE hidden = 0) and
  `get_random_quote` in both native and D1 implementations
- Update all SELECT queries to include the `hidden` column
- Handle `hidden` field in `update_quote` SET clause for both implementations
- Update `MockRepo` and `sample_quote` in handler tests to include `hidden`

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 549accded0 chore(quotesdb): add tickets for footer, hidden quotes, reporting, moderation, and rate limiting
New tickets:
- b2af7f: ui — footer with contact email
- 8a7fba: api — hidden flag for quotes (schema + endpoints)
- 77237f: api — reports table + POST /api/quotes/:id/report
- 6c5904: api — admin moderation endpoints
- f4930e: ui — hidden toggle on quote pages
- 354276: ui — report button with modal and Turnstile captcha
- 3f22f2: ui — admin moderation tab
- cb8de0: ui — admin auth-first flow, remove from default nav
- 06d304: infra — Cloudflare rate limiting

Reopened sub-project and root tickets; wired dependencies.
5 months ago
Elijah Voigt 00d195c86f chore(quotesdb): commit tickets, TODO, and infra README update
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt c4a59ec9ad feat(quotesdb): show locked banner on /submit when submissions are closed
Check GET /api/status on mount; if submissions_locked is true, hide the
form and show a .submissions-closed-banner instead. Fail-open: on error,
treat as unlocked and display the form normally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 511c9fbf54 fix(quotesdb): fix admin_reset_auth_code call, handle status fetch error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 5d2780a72a fix(quotesdb): fix admin_reset_auth_code call, handle status fetch error
Pass "" for the unused `current` param in admin_reset_auth_code (the
server only checks X-Admin-Code). Handle get_status() failure in the
on-mount effect with fail-open behaviour: set submissions_locked=Some(false)
and display "Could not fetch status." so the UI never hangs on
"Loading status...".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 49f70cc5e8 feat(quotesdb): /admin page component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt a4d59b4371 fix(quotesdb): atomic update_admin_auth_code, fix handler docstring
Replace the two-step read-check-write in update_admin_auth_code with a
single atomic UPDATE … WHERE key = 'admin_auth_code' AND value = ?current
in both NativeRepository and D1Repository. Rows-affected count is checked:
zero means the code was absent or mismatched → DbError::Forbidden; one
means success.

Also remove the now-unnecessary replacement2 clone binding in native.rs.

Fix the reset_auth_code handler doc comment to accurately describe that a
missing X-Admin-Code header is caught by the handler itself (before any DB
call), while a wrong-but-present code reaches the DB layer which returns
DbError::Forbidden.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt ab398b690c feat(quotesdb): POST /api/admin/reset-auth-code endpoint
Adds handler, route registration, request/response types, and five unit
tests for the admin auth-code rotation endpoint. Updates openapi.yaml
with the new path and a ResetAuthCodeResponse component schema.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 80b998c192 feat(quotesdb): enforce submission lock on PUT /api/quotes
Add a pre-flight check at the top of create_handler that calls
get_submissions_locked() before processing the request. Returns
423 Locked with {"error": "submissions are closed"} when locked.

Update openapi.yaml to document the 423 response on PUT /api/quotes.

Add three unit tests: locked → 423, unlocked → 201, unlock-then-create → 201.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 3684e196dd fix(quotesdb): fix verify_admin_code docstring, add 500 to OpenAPI, make handlers private
- Clarify verify_admin_code docstring to say "standard string equality"
  instead of leaving comparison method implicit
- Add missing "500" response entries to /api/admin/lock and
  /api/admin/unlock in openapi.yaml
- Remove pub from lock_submissions and unlock_submissions to match all
  other handlers in the file

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 401a4f45a5 feat(quotesdb): POST /api/admin/lock and /api/admin/unlock endpoints
Add two admin-protected endpoints that toggle the global submissions lock:
- POST /api/admin/lock  — sets submissions_locked = true
- POST /api/admin/unlock — sets submissions_locked = false

Both require the X-Admin-Code header and return { "submissions_locked": bool }
on success, or 403 on missing/wrong code. Operation is idempotent.

Shared helper verify_admin_code() fetches and compares the stored admin code.
Routes registered in the router() function. Five unit tests added covering
correct code, wrong code, missing header, and idempotent lock behaviour.

OpenAPI spec updated with AdminCode security scheme, LockResponse schema,
/api/admin/lock and /api/admin/unlock path entries, and an admin tag.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt f6f652ef3e docs(quotesdb): add /api/status to OpenAPI spec
Add GET /api/status path and StatusResponse schema. The endpoint
returns { "submissions_locked": bool } with 200 or 500, requires
no auth, and is tagged under the existing `meta` group.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 6b90f34ccf feat(quotesdb): GET /api/status public endpoint
Adds the GET /api/status handler that returns {"submissions_locked": bool}.
Registers the route in the router before the quotes routes.
Adds three unit tests covering unlocked state, locked state, and default false.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt b0cb813740 feat(quotesdb): admin API client functions in UI
Add ApiError::Forbidden variant, StatusResponse / ResetAuthCodeResponse /
LockResponse / ResetAuthCodeBody types, and four new async functions to
src/bin/ui/api.rs:
- get_status()           → GET /api/status
- admin_reset_auth_code() → POST /api/admin/reset-auth-code (X-Admin-Code + X-Auth-Code)
- admin_lock()           → POST /api/admin/lock (X-Admin-Code)
- admin_unlock()         → POST /api/admin/unlock (X-Admin-Code)

HTTP 403 responses map to ApiError::Forbidden in all three admin functions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 585f4b2f02 test(quotesdb): add handler-level tests for DB admin methods
Make MockRepo stateful for admin_auth_code and submissions_locked so
the new QuoteRepository methods can be exercised without a real DB.

Add four tests to src/bin/api/handlers/mod.rs:
- get_submissions_locked returns false by default
- set_submissions_locked(true) then get_submissions_locked returns true
- update_admin_auth_code with correct current succeeds and returns new code
- update_admin_auth_code with wrong current returns Forbidden

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt c9142edbbf feat(quotesdb): DB layer — add submissions_locked + update_admin_auth_code
Add three new QuoteRepository trait methods and a seed helper:
- update_admin_auth_code(current, new_code): replaces the admin code if
  `current` matches; generates a fresh passphrase when new_code is None;
  returns DbError::Forbidden on mismatch.
- get_submissions_locked(): reads the submissions_locked key from
  admin_config; returns false when the key is absent.
- set_submissions_locked(locked): upserts "1"/"0" into admin_config.
- seed_submissions_locked(): INSERT OR IGNORE "0" — safe to call on every
  startup without clobbering an active lock.

Implemented in both NativeRepository (rusqlite) and D1Repository (wasm32).
Updated startup seeding in main.rs (native and wasm32 paths) to call
seed_submissions_locked after the existing admin auth code seeding.

Added 7 unit tests in db/native.rs covering all four specified scenarios:
default false, set-then-get, seed does not overwrite, correct code succeeds,
None new_code generates passphrase, wrong code returns Forbidden, stored
code unchanged after Forbidden.

MockRepo in handlers/mod.rs updated with stub implementations of all four
new trait methods to satisfy the trait bound.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 15d9de3947 docs(quotesdb): admin features design doc 5 months ago
Elijah Voigt 5dcbb334fa feat(quotesdb): Cloudflare Turnstile CAPTCHA on submit
- infra/turnstile.tf: provision Turnstile widget (managed mode, quotes.elijah.run domain) with site_key and secret_key outputs
- infra/variables.tf: add var.domain (default: quotes.elijah.run)
- src/lib.rs: add cf_turnstile_token: Option<String> (#[serde(default)]) to CreateQuoteInput; update doctest
- Cargo.toml: add reqwest (0.12, rustls-tls) under native-only dependencies
- src/bin/api/handlers/mod.rs: add verify_turnstile() and CAPTCHA gate in create_handler, both gated on #[cfg(not(target_arch = "wasm32"))]
- src/bin/api/db/native.rs: add cf_turnstile_token: None to all CreateQuoteInput struct literals in tests
- api/openapi.yaml: document cf_turnstile_token field in QuoteCreateRequest schema
- index.html: add Turnstile JS script tag
- src/bin/ui/pages/submit.rs: add turnstile_token state, use_effect_with callback registration, widget div, token included in CreateQuoteInput
- docs/LOCAL_DEV.md: add Cloudflare Turnstile CAPTCHA section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 014dc39ea4 feat(quotesdb): date range filter for quotes list
Add 6 optional query parameters to GET /api/quotes:
  date_after_year/month/day and date_before_year/month/day

Changes:
- QuoteRepository::list_quotes gains date_after and date_before params
- NativeRepository and D1Repository build ISO date prefix WHERE clauses;
  quotes with NULL date are excluded when any bound is set
- list_handler validates component ordering (month requires year, etc.)
  and returns 400 on invalid combinations
- build_date_bound helper converts y/m/d components to ISO prefix strings
- UI api::list_quotes and browse page gain From/To year filter inputs
- author page call updated to pass None for the new date params
- openapi.yaml extended with 6 new query parameter entries
- 6 new integration tests covering after, before, range, and 400 cases
- 1 new native DB unit test covering all filter combinations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 267a95aa13 feat(quotesdb): admin super auth code for quote moderation
Add an admin_config table storing a single admin auth code that
bypasses per-quote auth checks for update and delete operations.
The code is auto-generated on first startup and printed to stderr.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt bdf99b32c4 feat(quotesdb): add workers-rs WASM entry point to api binary
- Gate native Tokio/Axum main() with #[cfg(not(target_arch = "wasm32"))]
- Add #![cfg_attr(target_arch = "wasm32", no_main)] to suppress missing-main error
- Add #[worker::event(fetch)] entry point using worker::HttpRequest / http::Response<axum::body::Body>
- Enable `http` feature on worker dep so fetch handler uses standard http types
- Add axum (json+query features), tower-service, and http to wasm32 deps
- Move async-trait to shared [dependencies] so both targets have it
- Make db::d1 module pub so main.rs can access D1Repository on wasm32
- Fix worker::d1::Database → D1Database and PreparedStatement → D1PreparedStatement
- Add #[cfg_attr(target_arch = "wasm32", worker::send)] to all 7 handler fns
  so their futures satisfy Axum's Handler<Send> bound on single-threaded wasm32
5 months ago
Elijah Voigt d9f14bfc53 fix(quotesdb): resolve compiler warnings in api and ui
- Remove redundant #![cfg(target_arch="wasm32")] from d1.rs (module
  declaration in mod.rs already gates it)
- Remove unused D1Repository re-export from db/mod.rs
- Drop unused page/total_count fields from UI ListResponse struct
  (only total_pages is consumed by the browse page)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 7e2f01e822 feat(quotesdb): implement D1Repository for Cloudflare Workers
Replace all 7 stub methods in src/bin/api/db/d1.rs with full working
implementations using the Cloudflare D1 API from workers-rs 0.5.

Implements:
- run_migrations: executes four DDL statements via db.exec()
- list_quotes: dynamic WHERE clause with positional params, COUNT query,
  paginated SELECT, per-quote tag fetch
- get_quote: prepared statement with first::<QuoteRow>()
- get_random_quote: ORDER BY RANDOM() LIMIT 1
- create_quote: INSERT + batch tag insert + read-back for timestamps
- update_quote: auth check, dynamic SET clause, optional tag replacement,
  read-back of updated row
- delete_quote: auth check, DELETE, returns DeleteResult enum

Also adds helper structs (QuoteRow, AuthRow, TagRow, CountRow),
fetch_tags() helper method, and unsafe Send/Sync impls required for
Arc<dyn QuoteRepository + Send + Sync> on single-threaded wasm32.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt b48f56712e fix(quotesdb): home page friendly empty state when no quotes
Detect 404 from random quote endpoint and render a friendly
'Nothing here yet — Submit a quote!' message with a link to
/submit instead of the generic error display.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt ad06c8befb fix(quotesdb): submit form UX fixes (6a4c61, dfd185, 5b3475)
- Replace 'Submit another' link with 'Browse all quotes' -> Route::Browse
- Change date input from type=text to type=date, remove placeholder
- Make author optional (defaults to Anonymous), update label
- Clarify auth code label to 'Edit/delete passphrase'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt e3fe2253f9 fix frontend proxy issue 5 months ago
Elijah Voigt d8560ef3d5 chore(quotesdb): close all tickets — implementation complete
All 69 implementation tickets done. Root ticket ec118c closed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 4f1aa5560a test(quotesdb): add integration test suite for all API endpoints
Adds 26 integration tests in handlers::integration_tests using a real
NativeRepository backed by a NamedTempFile SQLite database. Each test
gets an isolated database via test_router(). tempfile added to
[dev-dependencies]. Closes tickets: 5f5ba0, 9b581f, 789d0f, aa0eab,
f9f448, 4a4c26, 93f1b6, fae330, 8c87db, 893eba, e8f5cf, ce1e4f.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 7529b43845 fix(quotesdb): gate UI binary on wasm32, update UI ticket statuses
- main.rs: add #[cfg(target_arch = "wasm32")] guards on all wasm32-only
  code; add stub fn main() for native targets so cargo check passes
- Ticket statuses updated for all completed UI page/component tickets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt c436ba07c7 feat(quotesdb): merge full page implementations, tag filter, and stylesheet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt d317648d2f feat(quotesdb): add comprehensive BEM stylesheet for all UI components
Covers all component and page classes:
- Navigation bar (nav, nav__brand, nav__links, nav__link)
- QuoteCard (quote-card, quote-card__text, footer, author, source, tags, tag, link)
- ErrorDisplay (error-display, error-display__message)
- Pagination (pagination, pagination__btn, pagination__info)
- AuthModal (auth-modal__overlay, auth-modal, auth-modal__title/input/actions)
- All page containers (page-home, page-browse, page-quote, page-author, page-submit)
- Edit form, submit form, success state with auth code box
- Base styles: reset, typography, buttons, form inputs
- Responsive breakpoint at 640px

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 183994b3dc feat(quotesdb): implement all UI page components (Home, Browse, QuoteDetail, Author, Submit)
- HomePage: fetches random quote on mount, displays with QuoteCard and browse/submit links
- BrowsePage: paginated list with author and tag filter inputs, Pagination component
- QuotePage: view/edit/delete with AuthModal gating, 403/404 handling, sessionStorage auth
- AuthorPage: lists quotes by author with tag filter and pagination
- SubmitPage: full form with all fields, success state showing auth code prominently
- Tag filter (d3d502) integrated into Browse and Author pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt bc48924d16 feat(quotesdb): implement API DB layer and all HTTP handlers
DB layer (src/bin/api/db/):
- native.rs: NativeRepository (tokio-rusqlite) implementing all CRUD ops,
  dynamic WHERE for filters, two-phase auth check for update, 13 unit tests
- d1.rs: D1Repository wasm32 stub (all methods return Internal error)
- connection.rs: open() helper — WAL + foreign_keys pragmas
- mod.rs: cfg-gate async_trait (Send on native, ?Send on wasm32)

Handlers (src/bin/api/handlers/mod.rs):
- All 7 routes: GET /api/, random, {id}, list, PUT create, POST update, DELETE
- Router order: random BEFORE {id} (prevents "random" matching as id)
- Auth: X-Auth-Code header validation → 403 on mismatch
- 13 handler unit tests with MockRepo

main.rs: opens DB, runs migrations, wraps in Arc<dyn Repo + Send + Sync>,
  binds on $PORT (default 3000)

Cargo.toml: tower dev-dep for ServiceExt::oneshot in tests

All 32 tests pass (26 api + 6 lib)

Tickets closed: 00aff0 a5049d 6e829e 28e7d9 886bfd 2ce22e 5dbb7d 05f8ae
                d792e2 5d9f5a b20b5a 175382 03bb91

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 52e771e9c4 feat(quotesdb): merge UI shell, API client, storage and base components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 6f7614c0c8 feat(quotesdb): implement UI app shell, API client, storage, and base components
- Add BrowserRouter with all 5 routes (Home, Browse, QuoteDetail, Author, Submit)
- Implement typed API client (list, get, random, create, update, delete)
- Implement sessionStorage auth code helpers (set/get/clear)
- Add ErrorDisplay, QuoteCard, AuthModal, Pagination components
- Add stub page components for initial compilation
- Fix Cargo.toml: uuid js feature for wasm32, getrandom 0.3 wasm_js for rand dep,
  js-sys and Storage web-sys feature for API client and storage module

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt a7b2d6fd4e feat(quotesdb): add QuoteRepository trait, migrations, and thiserror dependency
- db/mod.rs: QuoteRepository async trait + ListResult/DeleteResult/DbError types
- db/migrations.rs: SQL DDL strings for quotes, quote_tags, and indexes
- lib.rs: fix rand 0.9 trait import (SliceRandom → IndexedRandom)
- Cargo.toml: add thiserror = "2" for DbError derive

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 83f4aacdf5 feat(quotesdb): set up Trunk.toml and index.html for UI build
- Trunk.toml: target index.html, proxy /api/* to localhost:3000
- index.html: link CSS, Rust wasm binary (ui), copy _redirects

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 8da0d754a0 chore(quotesdb): merge 9ef703 - add _redirects SPA fallback 5 months ago
Elijah Voigt af8c476305 chore(quotesdb): add _redirects SPA fallback for Cloudflare Pages routing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 9f28d99a93 feat(quotesdb): add build.rs to convert api/openapi.yaml to JSON at compile time
- Create build.rs at crate root using serde_yaml (build-dep only) to parse
  api/openapi.yaml and write compact JSON to $OUT_DIR/openapi.json
- cargo:rerun-if-changed ensures re-conversion only when spec changes
- serde_yaml never enters the Workers or native binary (build-dep only)
- Downstream GET /api/ handler consumes via include_str!(concat!(env!("OUT_DIR"), "/openapi.json"))

Closes ticket 8892d5
5 months ago
Elijah Voigt b695cb5b6d feat(quotesdb): add Yew/Wasm UI dependencies to Cargo.toml
- Add yew 0.22 with csr feature for Rust/Wasm SPA frontend
- Add yew-router 0.19 for client-side routing
- Add gloo 0.11 for Web API utilities (timers, fetch, events)
- Add wasm-bindgen 0.2 matching Nix wasm-bindgen-cli 0.2.108
- Add wasm-bindgen-futures 0.4 for async fetch support
- Add web-sys 0.3 for raw DOM/browser API bindings
All UI deps scoped to wasm32 cfg target to avoid host build pollution

Closes ticket 93515e
5 months ago
Elijah Voigt 7bd2bf4ed5 chore(quotesdb): set up Cargo.toml with api and ui dependencies
- Add axum 0.8 + tokio 1 (native-only) for the HTTP API server
- Add rusqlite 0.32 + tokio-rusqlite 0.6 (native-only) for local SQLite
- Add async-trait 0.1 (native-only) for QuoteRepository abstraction
- Add worker 0.5 with d1 feature (wasm32-only) for Cloudflare Workers
- Add rand 0.9 + serde + serde_json (shared) for passphrase/serialisation
- Add build-dependencies: serde_json 1 + serde_yaml 0.9 for build.rs
- Pin rusqlite to 0.32 to match tokio-rusqlite 0.6 transitive dependency

Closes ticket 1f5bb5

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt b0cf17ec83 feat(quotesdb): add generate_id() using UUID v4 — WASM-compatible ID generation
- Add uuid = { version = "1", features = ["v4", "serde"] } to [dependencies]
- Add getrandom = { version = "0.4", features = ["wasm_js"] } under wasm32 cfg
- Implement generate_id() in src/lib.rs with rustdoc and doctest
- Add unit tests for length, hyphen count, and uniqueness

Closes ticket 7a0d9f

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt dc73fc8f69 plan to implement the current batch of tickets 5 months ago
Elijah Voigt 5926d33bcc chore(quotesdb): resolve all triage tickets and create implementation tickets
- All 21 TRIAGE decision tickets resolved with chosen approaches documented
- This session: e2bd9b (SPA routing → _redirects), 2ec8b1 (OpenAPI → build.rs),
  0d84fa (HTTP client → reqwest), 0bc655 (auth code → session storage)
- New implementation tickets created: 9ef703, 8892d5, 5379eb
- Downstream tickets updated with resolved approaches and correct dependencies
- ARCHITECTURE.md updated with pinned WASM dependency versions (yew 0.22,
  yew-router 0.19, wasm-bindgen 0.2)
- XML tags added to all tickets for improved LLM guidance

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt ec2a4055ca docs(quotesdb): add XML tags to all tickets for improved LLM guidance
All 80 nbd tickets updated with structured XML tags:
- Task tickets: <context>, <goal>, <constraints>, <skills>, <validation>, <commit>
- TRIAGE tickets: <context>, <question>, <options>, <resolution>, <commit>
- Project tickets: <context>, <goal>, <skills>, <validation>
- ec118c (root): <skills>, <goal>, <context>, <design>, <phases>, <verification>
- b38032 (done): <goal>, <current-state>, <target-state>, <changes>, <constraints>, <validation>, <summary>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt 65b0fdf1e2 some tickets marked in progress 5 months ago
Elijah Voigt 91ecd91176 refactor(quotesdb): collapse to single crate with api and ui binaries
Replaces the three separate sub-crates (api/, ui/, tests/) with a single
Cargo crate at the quotesdb/ root. Shared code lives in src/lib.rs; the
api and ui are multi-binary targets; integration tests use the standard
Cargo tests/ layout. Trunk files moved to project root with data-bin="ui".

Closes ticket b38032.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
Elijah Voigt de99f1c047 chore(quotesdb): bootstrap project skeleton and design doc
- Scaffold api/, ui/, tests/, infra/, docs/ directories
- Stub Cargo.toml for api, ui, and tests crates
- Write finalized design doc to docs/plans/2026-02-27-quotesdb-design.md
- Add placeholder PLANNING.md, ARCHITECTURE.md, README.md per domain
- Add stub main.rs and tests.rs for api and ui
- Add index.html and Trunk.toml for ui
- Add placeholder infra/main.tf with Cloudflare provider stub

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago

@ -1,6 +0,0 @@
beans:
path: .beans
prefix: vibed-
id_length: 4
default_status: todo
default_type: task

@ -1,69 +0,0 @@
---
# vibed-0fcp
title: Add justfiles for all projects with lint, check, build, build-release, and release recipes
status: todo
type: task
priority: normal
created_at: 2026-03-10T23:29:17Z
updated_at: 2026-03-10T23:29:17Z
---
## Background
The root `TODO.md` calls for each project in the mono-repo to have a `justfile` with standard recipe names so that any contributor can run the same commands regardless of project type. `just` is a command runner similar to Make but with cleaner syntax.
## Recipes to add per project
| Recipe | What it runs |
|---|---|
| `lint` | Auto-format: `cargo fmt`, `tofu fmt`, `mdbook build` (edu), etc. |
| `check` | Static checks: `cargo check && cargo clippy`, `tofu validate && tofu plan`, etc. |
| `build` | Dev build: `cargo build`, `trunk build`, `mdbook build` |
| `build-release` | Optimised binary or bundle: `cargo build --release`, `trunk build --release` |
| `release` | Deploy/publish: `tofu apply`, `wrangler deploy` (where applicable) |
Not every project needs every recipe; stub missing ones with an error or a no-op comment.
## Per-project breakdown
### `nbd/justfile`
- `lint`: `cargo fmt`
- `check`: `cargo check && cargo clippy && cargo test`
- `build`: `cargo build`
- `build-release`: `cargo build --release`
- `release`: n/a (CLI tool, no deployment)
### `edu/justfile`
- `lint`: `mdbook build` (build validates the content)
- `check`: `mdbook build`
- `build`: `mdbook build`
- `build-release`: same as `build` (mdbook has no separate release mode)
- `release`: TBD — CF Pages auto-deploys from main branch once infra is configured (see edu deployment ticket)
### `quotesdb/api/justfile`
- `lint`: `cargo fmt`
- `check`: `cargo check && cargo clippy`
- `build`: `cargo build`
- `build-release`: `cargo build --release`
- `release`: `wrangler deploy` (or `tofu apply` from `infra/`)
### `quotesdb/ui/justfile`
- `lint`: `cargo fmt`
- `check`: `cargo check && cargo clippy`
- `build`: `trunk build`
- `build-release`: `trunk build --release`
- `release`: CF Pages auto-deploys from main branch once infra is configured
## Files to create
- `nbd/justfile`
- `edu/justfile`
- `quotesdb/api/justfile`
- `quotesdb/ui/justfile`
## Conventions
- Recipe names must be consistent across all projects (`lint`, `check`, `build`, `build-release`, `release`).
- Commands are run from the directory containing the `justfile`, so paths are relative to the project directory.
- Add a comment above each recipe explaining what it does.
- Check whether the `just` package is already in each project's Nix dev shell (`flake.nix`); add it if missing.

@ -1,24 +0,0 @@
---
# vibed-0t6e
title: Add API request types
status: todo
type: task
created_at: 2026-04-11T22:34:39Z
updated_at: 2026-04-11T22:34:39Z
parent: vibed-f05l
blocked_by:
- vibed-ca4q
---
Implement the full `SearchRequest` struct tree for the HLTB POST body.
## Todo
- [ ] `SearchRequest` with searchType, searchTerms, searchPage, size, searchOptions, useCache
- [ ] `SearchOptions` with games, users, lists, filter, sort, randomizer
- [ ] `GamesOptions` with userId, platform, sortCategory, rangeCategory, rangeTime, gameplay, rangeYear, modifier
- [ ] `RangeTime { min: Option<u32>, max: Option<u32> }`
- [ ] `Gameplay` with perspective, flow, genre, subGenre
- [ ] `RangeYear { min: &str, max: &str }`
- [ ] `UsersOptions` and `ListsOptions` with sortCategory
- [ ] All structs derive `serde::Serialize` with correct `#[serde(rename = ...)]` annotations
- [ ] `cargo check` passes

@ -1,19 +0,0 @@
---
# vibed-4rql
title: 'Per-habit streaks: each habit scores its own streak value'
status: completed
type: feature
priority: normal
created_at: 2026-07-16T16:31:23Z
updated_at: 2026-07-16T16:37:00Z
---
Replace the single day-level streak + triangular(checks) reveal formula with per-habit streaks: each habit tracks its own consecutive-day streak, and checking it reveals pixels equal to that habit's current streak length (1st day = 1 pixel, 2nd consecutive day = 2, etc.), with no cross-habit multiplier. Remove the global streak header/stat chip entirely (per user decision) and show each habit's own streak inline on its row instead. Update DESIGN.md, README.md, ARCHITECTURE.md, and index.html (mechanics + About text + UI).
## Summary of Changes
- index.html: replaced the day-level triangular(checks) × streak-multiplier reveal with per-habit streaks. Added `habitStreakEndingOn(log, habitId, day)` and `currentHabitStreak(log, habitId, today)` (display, forgives an unchecked-yet today); `revealedUnits()` now sums each checked habit's own streak length per day. Removed `triangular()`, `streakOnDay()`, and the global `computeStreak()`.
- Removed the global "🔥 N-day streak" header label and stat chip entirely (per user decision); each habit row now shows its own inline streak badge (e.g. 🔥3) when > 0.
- Updated the in-app About text and CSS (`.habit-streak`, removed `.streak-label`).
- Updated docs/DESIGN.md (§2 mockup, §3.2, §3.4, §9), docs/ARCHITECTURE.md, and README.md to describe per-habit streak scoring instead of day-level/triangular + multiplier.
- Validated with a Playwright smoke test (seeded localStorage, checkbox clicks) confirming reveal math, streak badges, and toast deltas; no console errors or non-file:// requests. Also captured a headless screenshot to confirm the UI layout.

@ -1,21 +0,0 @@
---
# vibed-54u4
title: Add CLI argument parsing
status: todo
type: task
created_at: 2026-04-11T22:34:31Z
updated_at: 2026-04-11T22:34:31Z
parent: vibed-f05l
blocked_by:
- vibed-ca4q
---
Implement the `Args` struct using `clap` derive API.
## Todo
- [ ] Add `use clap::Parser;` and derive `Args` struct
- [ ] Variadic positional `title: Vec<String>` with `num_args = 1..`
- [ ] `--json` bool flag (output raw JSON)
- [ ] `--all` bool flag (show all results, not just top match)
- [ ] Call `Args::parse()` in `main` and join title words for display
- [ ] `cargo check` passes

@ -1,29 +0,0 @@
---
# vibed-59dh
title: 'bit-by-bit: [hidden] attribute defeated by CSS display rules — celebration overlay stuck onscreen'
status: completed
type: bug
priority: normal
created_at: 2026-07-14T04:30:04Z
updated_at: 2026-07-14T04:32:45Z
---
User report: after onboarding, a bare "You did it!" celebration modal covers the app and its buttons appear dead.
Root cause: `.celebration-overlay`, `.onboarding-overlay` set `display:flex` (and `#artworkActive` has inline `display:flex`, `.upload-preview` sets `display:flex`). Author CSS beats the UA stylesheet's `[hidden]{display:none}`, so toggling the `hidden` attribute in JS has no visual effect on these elements. The celebration overlay was therefore visible from page load with placeholder text, and hideCelebration() couldn't hide it. jsdom tests missed it (no layout).
Fix: global `[hidden]{ display:none !important; }` rule (also covers the inline-style case on #artworkActive).
- [x] Apply global [hidden] rule
- [x] Verify in a real headless browser: celebration hidden on load, onboarding visible on first run, hides after finishing
## Summary of Changes
Added `[hidden]{ display:none !important; }` to the base styles in `bit-by-bit/index.html` (with a comment explaining why). This makes the `hidden` attribute authoritative over author display rules (`.celebration-overlay`/`.onboarding-overlay` flex, `#artworkActive` inline flex, `.upload-preview` flex).
Verified with headless Firefox screenshots (fresh profile each):
- Fresh load: onboarding wizard visible, no stray celebration modal.
- Seeded mid-progress state: main app renders, 3/4 pixels · 75%, overlays hidden.
- Final check-off: image completes to 4/4 · 100%, canvas keeps showing the finished art via the pendingCelebration snapshot, gallery count increments — confirming the celebration flow renders correctly in a real browser.
Lesson recorded: jsdom flow tests check the hidden property, not computed style — visibility bugs need a real-layout browser check.

@ -1,10 +0,0 @@
---
# vibed-6uyy
title: 'bit-by-bit: backfill missed days via a day navigator on the Today card'
status: completed
type: feature
created_at: 2026-07-18T17:26:52Z
updated_at: 2026-07-18T17:26:52Z
---
Let users step back to an earlier day and check off habits they forgot, so a missed day no longer permanently breaks a streak. The Today card became a day navigator (/ arrows + a 'back to today' link); editable range is the active image's start through today. Reveal is derived from the whole log with each habit's streak counted backwards from its own day, so backfilling a missed day automatically re-bridges the streak and paints the pixels it earns. Updated index.html (selectedDay state, clampSelectedDay, nav wiring, per-day streak display, About copy), DESIGN.md (§2, §3.1, §9), and ARCHITECTURE.md. Validated with Playwright: backfilling one gap day bridged the streak and grew reveal 4→10 pixels, nav bounds and back-to-today work, no console errors or non-file requests.

@ -1,25 +0,0 @@
---
# vibed-90il
title: Validation & smoke test
status: todo
type: task
created_at: 2026-04-11T22:35:32Z
updated_at: 2026-04-11T22:35:32Z
parent: vibed-f05l
blocked_by:
- vibed-iq16
- vibed-mlgv
---
Run all validation commands and live smoke tests against the real API.
## Todo
- [ ] `cargo fmt --check` passes
- [ ] `cargo check` passes
- [ ] `cargo clippy -- -D warnings` passes
- [ ] `cargo build --release` succeeds
- [ ] `./target/release/hltb halo` returns results
- [ ] `./target/release/hltb the witcher 3` works without quotes
- [ ] `./target/release/hltb --all zelda` shows numbered list
- [ ] `./target/release/hltb --json hollow knight` outputs valid JSON
- [ ] `./target/release/hltb xyzzy_no_game_zzzq` prints "No results found."

@ -1,16 +0,0 @@
---
# vibed-ca4q
title: Initialize Rust project
status: todo
type: task
created_at: 2026-04-11T22:34:25Z
updated_at: 2026-04-11T22:34:25Z
parent: vibed-f05l
---
Create `Cargo.toml` and `src/main.rs` skeleton for the `hltb` binary.
## Todo
- [ ] Write `Cargo.toml` with package metadata, [[bin]], dependencies (clap, reqwest, serde, serde_json, tokio), and release profile
- [ ] Write minimal `src/main.rs` with a stub `main()`
- [ ] Run `cargo check` and confirm it passes

@ -1,10 +0,0 @@
---
# vibed-f05l
title: hltb CLI — HowLongToBeat search tool
status: todo
type: milestone
created_at: 2026-04-11T22:33:37Z
updated_at: 2026-04-11T22:33:37Z
---
A Rust CLI binary that searches HowLongToBeat.com and prints completion times (Main Story / Main + Extras / Completionist) for any game. Single binary named `hltb`, uses reqwest + clap + serde + tokio.

@ -1,29 +0,0 @@
---
# vibed-fdph
title: Escalating daily pixel rewards (triangular per-day)
status: completed
type: feature
priority: normal
created_at: 2026-07-14T22:38:42Z
updated_at: 2026-07-14T22:46:49Z
---
Reward doing more habits in a single day: the Nth habit checked in a day reveals N units of pixels (1+2+3+... = triangular). Derive revealed pixels from the log per-day triangular sum instead of a flat checks counter; recalibrate pixelsPerCheck so a perfect month still finishes an image. Update DESIGN.md, README.md, ARCHITECTURE.md.
## Summary of Changes
Escalating daily reward implemented in `index.html`:
- Reveal is now **derived from the log** instead of a stored `checks` counter. New helpers `triangular(k)` and `revealedUnits(image)` sum `k·(k+1)/2` units per day (days ≥ startedAt). The Nth habit checked in a day is worth N units, so a 4-check day = 1+2+3+4 = 10 units.
- `getRevealInfo`: `revealedCount = min(opaque, floor(units · pixelsPerCheck))`.
- `pixelsPerCheckFor` recalibrated to `max(1, round(opaque / (triangular(habitCount) · 30)))` so a perfect month still fills the image (verified: ~30 days).
- `onHabitToggle`: snapshots `before` prior to mutating the log; uncheck path no longer touches a counter (past days can't un-reveal). Removed unused `computeFloor`.
- Dropped vestigial `checks` field from `normalizeImage`/`buildImageFromDef`; legacy saves load fine (field ignored, reveal recomputed).
- Upload day-estimate updated to use `triangular(habitCount)·ppc` per perfect day.
- Docs updated: DESIGN.md §3.2 + §9, data model, README.md, ARCHITECTURE.md.
Validation: no external refs (grep http/https/fetch clean), `node --check` passes, pure-function simulation confirms 3/6/9/12-px escalation and ~30-day perfect-month finish. Not opened in a live browser (none available in this environment).
## Browser Verification (headless Firefox 152)
Drove the real app's DOM in headless Firefox with a seeded 4-habit / 4x4-opaque state (pixelsPerCheck=1). Checking habits 14 revealed running totals 1, 3, 6, 10 (deltas +1, +2, +3, +4) — escalating exactly as specified, day total = 10. Unchecking the 4th dropped 10→6 (only today's contribution shrank). No console or window errors (`errors: []`).

@ -1,10 +0,0 @@
---
# vibed-i315
title: Escalating daily pixel rewards (triangular per-day)
status: in-progress
type: feature
created_at: 2026-07-14T22:38:38Z
updated_at: 2026-07-14T22:38:38Z
---
Reward doing more habits in a single day: the Nth habit checked in a day reveals N units of pixels (1+2+3+... = triangular). Derive revealed pixels from the log per-day triangular sum instead of a flat checks counter; recalibrate pixelsPerCheck so a perfect month still finishes an image. Update DESIGN.md, README.md, ARCHITECTURE.md.

@ -1,22 +0,0 @@
---
# vibed-iq16
title: Implement HTTP search function
status: todo
type: task
created_at: 2026-04-11T22:35:13Z
updated_at: 2026-04-11T22:35:13Z
parent: vibed-f05l
blocked_by:
- vibed-0t6e
- vibed-mn9a
---
Implement `async fn search(query: &str) -> Result<SearchResponse, AppError>` using reqwest.
## Todo
- [ ] Define `AppError { Network(reqwest::Error), Parse(serde_json::Error) }` with Display + From impls
- [ ] Implement `search()`: split query on whitespace for searchTerms, build full SearchRequest body
- [ ] POST to `https://howlongtobeat.com/api/search` with headers: User-Agent (Firefox), Referer, Content-Type
- [ ] Use `.json(&body)` for request and `.json::<SearchResponse>()` for response
- [ ] Wire into `main`: call search, match Ok/Err, print error to stderr and exit(1) on failure
- [ ] `cargo check` passes

@ -1,24 +0,0 @@
---
# vibed-mlgv
title: Implement display formatting
status: todo
type: task
created_at: 2026-04-11T22:35:25Z
updated_at: 2026-04-11T22:35:25Z
parent: vibed-f05l
blocked_by:
- vibed-mn9a
- vibed-54u4
---
Implement output formatting functions and the main display dispatcher.
## Todo
- [ ] `fn format_duration(seconds: u32) -> String`: return "N/A" for 0, else "Xh YYm"
- [ ] `fn print_game(game: &GameResult, prefix: Option<&str>)`: print name then 3 time lines with fixed-width labels
- [ ] `fn display(resp: SearchResponse, args: &Args)`: check for zero results, dispatch to --json / --all / default paths
- [ ] Default path: show only `data[0]`
- [ ] `--all` path: numbered list of all results
- [ ] `--json` path: `serde_json::to_string_pretty` of `data[0]` (or full vec with --all)
- [ ] Zero results: print "No results found." and return
- [ ] `cargo check` passes

@ -1,19 +0,0 @@
---
# vibed-mn9a
title: Add API response types
status: todo
type: task
created_at: 2026-04-11T22:35:02Z
updated_at: 2026-04-11T22:35:02Z
parent: vibed-f05l
blocked_by:
- vibed-ca4q
---
Implement `SearchResponse` and `GameResult` deserialization types.
## Todo
- [ ] `SearchResponse { count: u32, data: Vec<GameResult> }` with `Deserialize`
- [ ] `GameResult` with game_id, game_name, comp_main, comp_plus, comp_100, profile_steam, similarity — derive both `Deserialize` and `Serialize` (needed for --json output)
- [ ] Note: comp_main/plus/100 are in **seconds**; 0 means N/A
- [ ] `cargo check` passes

@ -1,37 +0,0 @@
---
# vibed-nf6g
title: Footer, Data tab with wipe, streak-multiplier rewards, About pixel rules
status: completed
type: feature
priority: normal
created_at: 2026-07-14T23:41:06Z
updated_at: 2026-07-15T00:10:14Z
---
Four changes to bit-by-bit index.html + DESIGN.md:
1. Add a page footer disclosing it was written with Claude Code (Sonnet 5, Opus 4.8, Fable 5).
2. Rename the 'Backup' settings tab to 'Data' and add a 'wipe all data' option there.
3. Streak becomes a reward multiplier: units per day = streak-on-that-day x triangular(checks). Recalibrate pixelsPerCheck and the finish estimate; update DESIGN.md.
4. About page: describe specifically how earning pixels works (per-check escalation + streak multiplier), encouraging finishing the whole day.
## Summary of Changes
All four changes landed in `index.html` (+ `docs/DESIGN.md`, `docs/ARCHITECTURE.md`, `README.md`):
1. **Footer** — added `<footer class="app-footer">` (plain text, no external link) disclosing 'Written with Claude Code · Sonnet 5, Opus 4.8 & Fable 5'.
2. **Data tab + wipe** — renamed the 'Backup' settings tab to 'Data' (data-tab/panel id `data`), added a 'Danger zone' with a double-confirm 'Wipe all data' button that clears the localStorage keys, resets to `defaultState()`, and returns to first-run onboarding.
3. **Streak multiplier** — reveal units per day are now `streakOnDay(d) × triangular(checks)` (new `streakOnDay` helper). Recalibrated `pixelsPerCheckFor` to divide by `triangular(30) × triangular(habitCount)`, and fixed the upload finish-day estimate to solve the triangular streak accumulation.
4. **About page** — added two paragraphs describing exactly how pixels are earned (per-check escalation + streak multiplier) and encouraging finishing the whole day's list; updated the version line to name all three models.
## Validation
- `grep` for `http://|https://|fetch(` → no external references.
- Playwright headless run: fresh load shows onboarding + footer; seeded 3-consecutive-day log on a 36px image renders '6 / 36 pixels' (confirms 1+2+3 streak-multiplied units, not the old flat 3); settings tabs are Habits/Artwork/Data/About (no Backup); wipe button clears localStorage and returns to onboarding; **zero console errors, zero non-local network requests**.
## Follow-up: upload during onboarding
The onboarding 'Pick your first picture' step only offered presets — no way to upload an image on first setup (upload previously lived only in Settings → Artwork). Added the upload pipeline to onboarding step 2:
- New file input + grid-size select + name field + live preview/estimate in `#obStep2`, reusing `downsampleImageFile`/`renderThumbnail`.
- Generalized the onboarding selection from `obSelectedPreset` to `obSelectedDef` (preset or upload) plus `obUploadResult`; preset and upload selections mutually supersede each other; 'Start painting' commits either.
- Validated via Playwright: uploading a PNG in onboarding shows the preview + '~N days' estimate, enables Start, and starts a 24×24 `source:'upload'` artwork named from the field — zero console errors, zero network requests. Preset/footer/data/wipe/streak regression suite still green. Updated DESIGN.md §2.

@ -1,28 +0,0 @@
---
# vibed-nz22
title: 'Bit-by-bit: live favicon mirrors the partially revealed artwork'
status: completed
type: feature
priority: normal
created_at: 2026-07-14T20:46:47Z
updated_at: 2026-07-14T20:53:40Z
---
Render the current artwork (revealed pixels solid, unrevealed as loading-static) into a 64x64 offscreen canvas and set it as the page favicon via a data URL, updating on reveal and theme change. Stays single-file, no external requests.
- [x] Add <link rel="icon"> placeholder in head
- [x] Add updateFavicon() offscreen renderer
- [x] Hook into renderArt() and applyTheme()
- [x] Verify no external requests / console errors
## Summary of Changes
- `index.html` head: `<link id="favicon">` + `<link id="faviconApple">` seeded with an inert 1px data URL.
- New `updateFavicon()` / `drawFavicon()`: renders the displayed image into a 64x64 offscreen canvas (integer cell size, letterboxed/centred), revealed pixels solid, unrevealed as an alternating static checker, transparent pixels left as canvas background. Falls back to an 8x8 checker placeholder when no artwork is active. Handed to both link tags via `toDataURL()`.
- Coalesced with a rAF flag so the burst of `renderArt()` calls from one check-off repaints the icon once; href only reassigned when the PNG actually differs.
- Hooked into `renderArt()` (covers reveal, pop, celebration snapshot, empty state) and `applyTheme()` (bg/static tones are theme-dependent).
- `docs/DESIGN.md` section 5 documents the behavior.
## Verification
Headless browser automation was unavailable in the sandbox, so `drawFavicon` and the reveal-order helpers were extracted verbatim from `index.html` and executed in Node against a recording canvas stub with a content-hashed `toDataURL`. With a 16x16 sprite at 5 checks x 8 px/check: 40/40 revealed cells painted solid, 116/116 unrevealed painted static, 100 transparent left as background, 0 mismatches; grid fills the full 64x64; href is stable across an unchanged redraw and changes when more pixels reveal. `grep` confirms no `http://`, `https://`, or `fetch(` in the file.

@ -1,27 +0,0 @@
---
# vibed-orml
title: 'bit-by-bit: rename app to ''Paint by Habits'' + design cleanup pass'
status: completed
type: task
priority: normal
created_at: 2026-07-14T18:53:51Z
updated_at: 2026-07-14T19:06:14Z
---
User requests:
1. Rename the app from "Bit by Bit" to "Paint by Habits" (in-app strings, docs). Keep the localStorage key (bitbybit.v1) for backward compatibility with existing user data.
2. Capture populated screenshots of every view at desktop and mobile sizes (seeding state artificially where needed), then run a design-focused agent pass to clean up the visual design.
- [x] Rename app in index.html (title, header, about, export filename, aria text; storage key kept for compat)
- [x] Rename in README/docs
- [x] Screenshot harness: seeded states, 9 views x mobile/desktop incl. celebration modal (sync-timer trick)
- [x] Design agent pass applied (Fable fork; 18 before/after screenshots)
- [x] Re-screenshot + re-validate (node --check clean, zero external refs, after-shots verified)
## Summary of Changes
1. **Rename:** app is now "Paint by Habits" — page title, header, About, storage-warning/import/upload error copy, export filename (`paint-by-habits-backup-*.json`), and all project docs. localStorage keys (`bitbybit.v1`) intentionally unchanged for backward compatibility with existing user data.
2. **Screenshot harness** (scratchpad `design-shots/`): awk-injected config/seed/driver scripts produce 9 seeded views (main light+dark, onboarding 1+2, four settings tabs, celebration modal via synchronous-timer shim) at 390×844 and 1440×900 in headless Firefox.
3. **Design pass (Fable fork):** two-column desktop layout ≥880px with sticky artwork card; fixed onboarding overflow at 390px; nowrap buttons + visible disabled states; settings tabs no longer clip on mobile; styled `::file-selector-button`; dark-mode progress track visible; clamp()-sized tinted header title; preset label/spacing polish. Verified across all 18 after-shots; `node --check` clean; still zero external references (73.5KB).
Related: `vibed-p2es` (arbitrary emoji input) created as todo, not implemented per user instruction.

@ -1,26 +0,0 @@
---
# vibed-p2es
title: 'bit-by-bit: allow arbitrary emoji for habits, not just the curated list'
status: todo
type: feature
created_at: 2026-07-14T18:53:47Z
updated_at: 2026-07-14T18:53:47Z
---
User request: habits should accept any emoji, not only the 24 curated options in the picker.
Current behavior: the emoji picker is a native <select> with ~24 curated emoji plus "🎲 Auto" (chosen during onboarding and in Settings → Habits).
Desired: user can type/paste any emoji (or grapheme) of their choosing, while keeping the curated list as quick suggestions.
Implementation notes (for whoever picks this up):
- Replace or augment the <select> with a small free-text input (maxlength ~4 to allow multi-codepoint emoji like 👨‍🌾; validate with Intl.Segmenter or a grapheme-aware check, falling back to length caps).
- Sanitize: trim, take the first grapheme cluster; empty → auto-assign as today.
- Keep keyboard accessibility and the 🎲 Auto option.
- Applies to both the onboarding wizard and Settings → Habits (add + rename flows).
- Export/import already stores emoji as plain strings, so no data model change expected.
- [ ] Free-form emoji input in onboarding
- [ ] Free-form emoji input in Settings → Habits
- [ ] Grapheme-aware validation + auto fallback
- [ ] Verify in real browser (screenshots)

@ -1,10 +0,0 @@
---
# vibed-qk1k
title: 'bit-by-bit: Changelog tab in Settings'
status: completed
type: feature
created_at: 2026-07-18T17:26:57Z
updated_at: 2026-07-18T17:26:57Z
---
Added a Changelog tab to the Settings modal (after About) listing released changes as dated entries, one concise sentence each, newest first. Content is distilled from the project's beans issues (release dates from each bean's completion). Implemented as a CHANGELOG data array + renderChangelog() rendering into #panel-changelog, with styling matching the app. Updated DESIGN.md §2 settings sections list. Validated with Playwright + screenshot: tab switches, four date groups render, no console errors or non-file requests.

@ -1,41 +0,0 @@
---
# vibed-vytp
title: Bit by Bit — pixel-art habit tracker (single-file HTML app)
status: completed
type: feature
priority: normal
created_at: 2026-07-14T00:18:14Z
updated_at: 2026-07-14T04:13:54Z
---
Build a local-only, single-page .html habit tracker where checking off daily habits reveals a pixel-art image one pixel at a time.
Requirements:
- Daily habits list, user-defined
- Pixel art image revealed pixel-by-pixel per habit check-off
- Preset pixel art gallery + image upload (downsampled to grid)
- Dark/light mode (auto + manual toggle)
- localStorage only, zero server/network dependencies
- Export/import JSON backup for device migration
- Fun and whimsical, easy to use
- Deliverable: one self-contained index.html
Process: Fable designs (docs/DESIGN.md), Sonnet agent implements, Opus agent reviews and iterates with Sonnet.
- [x] Design spec written (DESIGN.md)
- [x] Project scaffolding (CLAUDE.md, README, docs/)
- [x] Sonnet implementation of index.html (1,496 lines, self-verified with jsdom flow tests)
- [x] Opus review round 1 + Sonnet fixes (NEEDS-FIXES → all 7 findings fixed, regression suite passing)
- [x] Opus verification pass (verdict: SHIP, all 7 fixes verified, no regressions)
- [x] Final validation (node --check clean, zero external references, 71.5KB single file)
## Summary of Changes
Built `bit-by-bit/`, a whimsical single-file pixel-art habit tracker.
- **Design (Fable):** `docs/DESIGN.md` — full product spec: layout, reveal mechanics (seeded mulberry32 permutation, pixelsPerCheck sized for ~30-day completion), data model (`bitbybit.v1` localStorage doc), whimsy requirements, theming, export/import, a11y bar, 8 edge cases.
- **Implementation (Sonnet):** `index.html` (71.5KB, 1,576 lines) — vanilla JS/CSS IIFE, zero external requests, works from file://. 8 hand-shaded preset pixel artworks, image upload with pixelation preview, onboarding wizard, streaks + perfect-day sparkles, confetti celebration, dark/light/auto themes, JSON export/import, reduced-motion support. Verified with jsdom flow tests.
- **Review (Opus):** round 1 found 1 major (completion-moment canvas flash), 4 minors (seed-0 determinism, onboarding cap feedback, theme FOUC, hidden storage banner), 3 nits. All fixed by Sonnet; Opus verification pass returned SHIP with no regressions. Final cosmetic nit applied by Fable.
- **Scaffolding:** CLAUDE.md, README.md (with model credits), docs/PLANNING.md, docs/ARCHITECTURE.md.
Not committed — awaiting user review.

@ -1,67 +0,0 @@
---
# vibed-wqsu
title: Add contact footer to all service front-ends
status: todo
type: task
priority: low
created_at: 2026-03-10T23:29:17Z
updated_at: 2026-03-10T23:29:17Z
---
## Background
The root `TODO.md` calls for a footer on every service front-end with contact info:
> Contact: `<site>@elijah.run`
Where `<site>` depends on the service.
## Target services and email addresses
| Service | Front-end type | Contact email |
|---|---|---|
| `quotesdb` | Yew (Rust/Wasm) | `quotes@elijah.run` |
| `edu` | mdbook static site | `vibedbooks@elijah.run` |
| Blog editor | Yew (not yet built) | `editor@elijah.run` |
## Implementation
### quotesdb/ui
Add a `Footer` component to the Yew app:
1. Create `quotesdb/ui/src/components/footer.rs` (or add inline to `app.rs`).
2. Render a `<footer>` element at the bottom of every page with the text:
`Contact: quotes@elijah.run`
3. Use an `<a href="mailto:quotes@elijah.run">` tag so it is clickable.
4. Include the footer in the top-level layout so every route shows it.
### edu (mdbook)
mdbook supports injecting HTML before/after the content via the `[output.html]` table in `book.toml`:
```toml
[output.html]
additional-js = []
additional-css = []
```
Or use the `before-content` / `after-content` HTML templates. More precisely, create `edu/theme/footer.hbs` with the footer markup and register it in `book.toml`.
Alternatively (simpler): add a `theme/` directory with an `index.hbs` that extends the default template and appends the footer. The mdbook docs for custom theming describe this approach.
Simplest option: add `footer.html` fragment via `[output.html] additional-js` hack, or just add a sentence at the bottom of each page — but that is not scalable.
**Recommended approach for mdbook:** Use mdbook's `theme/index.hbs` override (copy the default from the mdbook source) and add the footer HTML before the closing `</body>` tag:
```html
<footer style="text-align:center;padding:1em;opacity:0.6">
Contact: <a href="mailto:vibedbooks@elijah.run">vibedbooks@elijah.run</a>
</footer>
```
## Files involved
- `edu/theme/index.hbs` (new — copy from mdbook default, add footer)
- `edu/book.toml` (may need `[output.html] theme = "theme"` entry)
- `quotesdb/ui/src/components/footer.rs` (new) or similar

@ -3,13 +3,5 @@
"superpowers@claude-plugins-official": true,
"plugin-dev@claude-plugins-official": true,
"rust-analyzer-lsp@claude-plugins-official": true
},
"hooks": {
"SessionStart": [
{ "hooks": [{ "type": "command", "command": "beans prime" }] }
],
"PreCompact": [
{ "hooks": [{ "type": "command", "command": "beans prime" }] }
]
}
}

@ -327,16 +327,59 @@ Covers:
<task-tracking>
**IMPORTANT**: before you do anything else, run the `beans prime` command and heed its output.
## Task Tracking with nbd
This project uses **beans** for issue tracking. `beans prime` outputs the full usage guide for the current project — always follow its instructions for creating, updating, and tracking work.
`nbd` is a CLI tool for managing work tickets, designed for agent workflows.
### Initialisation
```sh
# Initialise (run once per project directory)
beans init
nbd init
```
Run once in the project root. Creates `.nbd/tickets/`. Safe to run multiple times.
# Get full usage guide and instructions for this project
beans prime
### Core commands
```sh
# Create a new ticket (use --ftype md for a human-readable body)
nbd create --title "Add OAuth login" --type feature --priority 7 --ftype md --json
# List all open tickets (sorted by priority)
nbd list --json
# Read a specific ticket
nbd read <id> --json
# Update a ticket
nbd update <id> --status in_progress --json
nbd update <id> --status done --json
```
### Finding what to work on
```sh
# All tickets that are unblocked and ready to start
nbd ready --json
# The single highest-priority unblocked ticket
nbd next --json
```
### Workflow
1. **Before starting** — create a ticket: `nbd create --title "..." --ftype md --json`
2. **When starting** — mark it in progress: `nbd update <id> --status in_progress --json`
3. **When done** — mark it complete: `nbd update <id> --status done --json`
### Guidelines
- **Always pass `--json`** to every command for structured, unambiguous output.
- **Always pass `--ftype md`** when creating tickets — markdown format keeps the body human-readable.
- Use `jq` to parse and transform JSON output when needed.
- Priority scale 010: use **79** for bugs, **5** for normal tasks, **3** for nice-to-haves.
- `--type` choices: `project`, `feature`, `task`, `bug`.
- Use `--deps id1,id2` to express blockers — tickets that must be done first.
- Create tickets *before* starting non-trivial tasks, not after.
</task-tracking>

7
Cargo.lock generated

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "vibed"
version = "0.1.0"

@ -0,0 +1,6 @@
[package]
name = "vibed"
version = "0.1.0"
edition = "2024"
[dependencies]

@ -1,5 +1,8 @@
# TODO
## Tools
- [x] `nbd` agent tasks management cli
## Projects
- [ ] QuotesDB website
- [ ] Local-first Grocery List app

@ -12,7 +12,6 @@ A mono-repo of CLI tools and self-contained HTTP web services built in Rust, tar
| Service | Description |
|---|---|
| `common` | Shared library crate — types, utilities, and definitions used across services |
| `pages` | Micro static-site host at `pages.elijah.run` — upload an `.html`/`.zip`, served sandboxed at `/<name>/` (Cloudflare Workers + R2 + D1, Rust→wasm, GitHub OAuth) |
_Services will be listed here as they are added._

@ -1 +0,0 @@
# TODO

@ -1,5 +0,0 @@
{
"enabledPlugins": {
"frontend-design@claude-plugins-official": true
}
}

@ -1,19 +0,0 @@
@../CLAUDE.md
# Paint by Habits
A local-only, single-file HTML habit tracker that reveals a pixel-art image as
daily habits are checked off.
## Project-specific notes
- **This project is not a Rust crate.** The deliverable is a single
self-contained `index.html` (vanilla JS + CSS, no build step, no external
requests).
- The authoritative spec is `docs/DESIGN.md`. Behavior changes should update it.
- **Validation:** open `index.html` in a browser from `file://`; there must be
no console errors, no network requests, and the flows in DESIGN.md §9 (edge
cases) must hold. A quick static check: `grep` the file for `http://`,
`https://`, and `fetch(` — external references are forbidden (links in
comments/About text excepted).
- Deployment: copy `index.html` to any static host. No infra needed.

@ -1,57 +0,0 @@
# Paint by Habits 🎨
A whimsical habit tracker where consistency paints a picture. You define your
daily habits and pick a pixel-art image; every habit you check off reveals a few
pixels. Over days and weeks the artwork emerges — finish it, celebrate, pick the
next one.
## What it is
- **One self-contained file:** `index.html`. Vanilla JS + CSS, no build step,
no frameworks, zero network requests. Works from `file://` or any static host.
- **Local-only:** all data lives in your browser's `localStorage`. No server,
no accounts, no tracking.
- **Portable:** export your full state as a JSON backup and import it on
another device.
## How it works
Each artwork is a pixel grid. On start, a seeded PRNG (mulberry32 +
FisherYates) fixes a random reveal order that stays stable across sessions.
**Every habit keeps its own streak** — checking a habit reveals pixels equal to
its own current streak length: 1 pixel the first day, 2 the next consecutive
day, 3 the day after, and so on. There's no bonus for doing several habits in
one day; each habit scores purely on its own history. Bigger images just take
longer to fill in. Pixels never un-reveal across days; missing a day resets
that habit's streak, but never erases pixels already earned. Includes 8
built-in pixel artworks, or upload any image to pixelate it into a 1648 cell
grid.
Dark/light theme follows your OS by default, with a manual override. Each
habit shows its own streak inline on its row; days where you did everything
get a ✨ perfect-day sparkle.
## Run it
Open `index.html` in a browser. That's it. To host it, copy the file to any
static host.
## Test it
There is no test harness checked in (single-file app). Validation performed:
`node --check` on the extracted script, a grep proving zero external
references, and scripted jsdom flow tests covering onboarding, check/uncheck
persistence, day rollover, uncheck floor clamping, corrupt-state recovery,
storage-unavailable mode, import validation, and the completion/celebration
flow. See `docs/DESIGN.md` §9 for the edge-case checklist.
## License
Dual-licensed under [Apache-2.0](../LICENSE-APACHE) and [MIT](../LICENSE-MIT),
following Rust ecosystem convention for this repository.
## Disclaimer
This software was written with Claude Code. Design by **Claude Fable 5**
(`claude-fable-5`), implementation by **Claude Sonnet 5** (`claude-sonnet-5`),
review and verification by **Claude Opus 4.8** (`claude-opus-4-8`).

@ -1,18 +0,0 @@
# Paint by Habits — Architecture
Everything lives in one file, `index.html`, organized into sections:
| Component | Responsibility |
|---|---|
| **State** | Single JSON document under localStorage key `bitbybit.v1`; load/save with corruption guards; versioned for future migration. |
| **Mechanics** | Habit check/uncheck logging for the selected day (today or a backfilled past day, bounded by the active image's start), daily rollover, per-habit streak math, seeded (mulberry32) reveal-order permutation, and log-derived reveal: each habit reveals pixels equal to its own current streak length (1 the first day, 2 the next consecutive day, and so on), independent of any other habit. Because reveal recomputes from the whole log, backfilling a missed day re-bridges that habit's streak automatically. |
| **Artwork** | Preset pixel-art definitions (palette + row strings); upload pipeline (offscreen canvas downsample → pixel grid); gallery of finished pieces. |
| **Rendering** | Canvas renderer for the artwork (revealed pixels, checkerboard for hidden, pop-in animation for deltas); DOM renderer for checklist, stats, settings. |
| **UI wiring** | Onboarding, day navigator on the Today card (backfill past days), settings dialog (habits / artwork / data / about / changelog), theme cycling (auto→light→dark via `data-theme` + CSS custom properties), export/import/wipe, confetti celebration, and a page footer disclosing authorship. |
Data flows one way: user action → mutate state → persist → re-render. The
reveal permutation is derived from a stored seed, never persisted, so exports
stay small and rendering is deterministic.
There is no server, no network I/O, and no build step. See `docs/DESIGN.md` for
the full specification.

@ -1,247 +0,0 @@
# Paint by Habits — Design Specification
**Author:** Claude Fable 5 (design) · **Implementer:** Claude Sonnet · **Reviewer:** Claude Opus
## 1. Product Concept
Paint by Habits is a daily habit tracker where consistency paints a picture — literally.
The user defines daily habits and picks a pixel-art image. Every habit they check
off reveals a few pixels of the hidden artwork. Over days and weeks the image
emerges, and finishing it is the reward. Then they pick the next one.
**Tone:** warm, playful, encouraging. Think "cozy pixel garden," not "productivity
dashboard." Copy should be friendly and lightly whimsical ("3 pixels bloomed!"),
never guilt-trippy. Missing a day never destroys progress — pixels, once revealed,
stay revealed.
**Hard constraints:**
- One self-contained `index.html`. No external requests of any kind: no CDNs, no
fonts, no analytics, no fetch. Must work from `file://` and any static host.
- All state in `localStorage`. No server.
- Vanilla JS + CSS. No frameworks, no build step.
- Dark/light theme, auto-detected with manual override.
- Full-fidelity export/import so users can move devices.
## 2. Layout (single screen, mobile-first)
```
┌──────────────────────────────┐
│ ✿ Paint by Habits 🌓 ⚙️ │ header: title, theme toggle, settings
├──────────────────────────────┤
│ │
│ [ ARTWORK CANVAS ] │ the star of the show, centered,
│ progress: ▓▓▓░░ 42% │ progress bar + "142 / 340 pixels"
│ │
├──────────────────────────────┤
Sunday, July 13 │ day navigator: step back to fix a missed day
│ ┌──────────────────────┐ │
│ │ 💧 Drink water 🔥5[✓]│ │ today's habit checklist — each row shows
│ │ 🏃 Go for a run [ ] │ │ its own streak; big, thumb-friendly rows
│ │ 📖 Read 10 pages 🔥2[]│ │
│ └──────────────────────┘ │
├──────────────────────────────┤
│ total checks · images │ small stat chips
│ finished │
└──────────────────────────────┘
```
- Max content width ~28rem, centered; scales up gracefully on desktop.
- Settings (⚙️) opens a panel/modal with sections: **Habits**, **Artwork**,
**Data** (export/import plus a destructive "wipe all data" action), **About**,
and **Changelog** (a dated list of released changes, one concise sentence each,
newest first). Use `<dialog>` or an equivalent overlay.
- A small page **footer** discloses authorship ("Written with Claude Code ·
Sonnet 5, Opus 4.8 & Fable 5"), plain text with no external link.
- First visit (no saved state): a friendly 2-step onboarding — (1) add habits,
(2) pick an artwork (a preset **or** an uploaded image, same upload pipeline as
settings) — then straight into the main screen.
## 3. Core Mechanics
### 3.1 Habits
- User-defined list, 112 habits. Each habit: name (≤40 chars) and an optional
emoji (offer a small emoji picker of ~24 curated options; default assigned
cyclically if skipped).
- Habits reset daily (local timezone). Checking a habit today logs it for today.
- **Backfilling past days:** the Today card is a day navigator ( / arrows plus
a "back to today" link) so a user who forgot to check off on the day can step
back and fix it. Any day from the **active image's start** through **today** is
editable (checking or unchecking); you cannot edit days before the current
picture existed (they can't paint pixels) or into the future. Because reveal is
derived from the log with each habit's streak counted *backwards* from its own
day, backfilling a missed day automatically re-bridges the streak and paints
the pixels it earns — no separate reconciliation needed.
- Habits can be added/renamed/deleted in settings at any time. Deleting a habit
keeps its historical log entries (they still count toward revealed pixels).
### 3.2 Pixel reveal
- The active image is a grid of pixels; each cell is either a color or
transparent. Only **opaque** pixels count.
- On image start, generate a random permutation of opaque pixel indices using a
**seeded PRNG (mulberry32)** with a stored seed. This is the reveal order —
stable across sessions and re-renders.
- **Per-habit streaks, 1 unit = 1 pixel:** each habit has its own streak —
the number of consecutive days (ending on, and including, a given day) that
habit was checked. Checking a habit reveals pixels equal to **that habit's
own streak length** on the day checked: 1 pixel the first day, 2 pixels the
next consecutive day, 3 the day after, and so on. There is no cross-habit
multiplier or bonus for doing several habits in one day — every habit scores
purely on its own history. A habit's streak counts *backwards* from the day
it's evaluated on, so checking or unchecking a different day (or a different
habit) never changes it.
- **Revealed pixels are derived from the log, not stored.** Revealed pixel total =
`min(opaqueCount, Σ Σ streakLen(habit, d))` over days `d ≥ startedAt` and
habits `habit` checked on day `d`. No separate per-image calibration
constant — bigger images simply take longer to fill in, smaller ones finish
sooner. Because reveal is a pure function of the log, same-day uncheck only
shrinks that habit's contribution for today — past days can never be
un-revealed, and reloads/undo stay perfectly consistent with no separate
counter to keep in sync.
- Unrevealed cells render as a subtle dim checkerboard ("static"). Revealed
cells show their color and **pop in with a tiny scale/settle animation** when
newly revealed (animate only the delta, not on every render).
- When revealed == opaqueCount: **celebration** (see 5), image is archived to
the gallery, and the user is prompted to pick the next artwork.
### 3.3 Artwork sources
1. **Presets:** 8 built-in pixel arts, each 16×16 to 24×24, defined compactly in
JS as `{name, palette, rows}` where rows are strings of palette keys
(`.` = transparent). Suggested set (implementer designs the actual art, and
should make it genuinely charming): mushroom, cat face, heart, rainbow,
rocket, sunflower, friendly ghost, coffee mug.
2. **Upload:** file input (image/*) → draw to offscreen canvas at a chosen grid
size (16 / 24 / 32 / 48) → sample each cell's center region; alpha < 128
transparent. Show a live preview + opaque pixel count before confirming.
Reject nothing — any image works; it just gets pixelated.
- Switching artwork mid-image warns that current progress will be archived as
unfinished (kept in gallery, marked incomplete) — or user cancels.
### 3.4 Dates, streaks, stats
- "Today" = local date `YYYY-MM-DD` (from local time, not UTC). Recompute on an
interval (~30s) and on `visibilitychange` so a tab left open overnight rolls
over correctly (checklist resets, floor for uncheck updates).
- **Streak:** per-habit, not global — each habit's streak is the count of
consecutive days (ending today or yesterday, so it stays "alive" until a day
is fully missed) that habit itself was checked. Shown inline on that habit's
row (e.g. 🔥3), not as a single app-wide number. Separately, show a "perfect
day" sparkle ✨ on days where *every* currently-listed habit was checked —
purely a flair indicator, unrelated to streak math.
- Stat chips: ✅ total checks all-time, 🖼️ images finished.
## 4. Data Model
Single localStorage key: `bitbybit.v1`. Entire state is one JSON document:
```jsonc
{
"version": 1,
"theme": "auto", // "auto" | "light" | "dark"
"habits": [
{ "id": "h_x7f2", "name": "Drink water", "emoji": "💧" }
],
"image": { // active artwork, null if none chosen
"name": "Cheeky Mushroom",
"source": "preset", // "preset" | "upload"
"w": 16, "h": 16,
"pixels": [null, "#e84d4d", ...], // row-major, length w*h, null = transparent
"seed": 123456789,
"startedAt": "2026-07-01"
},
"log": { // immutable history of check-offs
"2026-07-13": ["h_x7f2", "h_a3b9"]
},
"gallery": [ // finished/archived artworks (cap 30, FIFO)
{ "name": "Heart", "w": 16, "h": 16, "pixels": [...],
"finishedAt": "2026-06-20", "complete": true }
]
}
```
- Save on every mutation (debounced is fine). Wrap `JSON.parse` of the loaded
value in try/catch; on corruption, keep the corrupt string under
`bitbybit.v1.corrupt` and start fresh rather than crashing.
- The reveal **order** is derived from `seed` at load time (don't store the full
permutation — keeps exports small; mulberry32 + FisherYates is deterministic).
- Storage safety: 48×48 upload = 2304 pixels ≈ 20KB JSON worst case; gallery
capped at 30 entries. Well within quota. Still wrap `setItem` in try/catch and
surface a friendly error if it fails.
## 5. Whimsy & Delight (required, not optional)
- **Pixel pop:** newly revealed pixels scale/bounce in with slight random stagger.
- **Check-off feedback:** the habit row does a happy little squish; a toast like
"+2 pixels ✨" floats up near the canvas.
- **Completion:** self-contained canvas confetti burst (no library), the finished
image does a gentle celebratory wiggle, and a congratulations card appears
("You painted *Cheeky Mushroom* — 24 days of showing up!") with a button to
choose the next artwork.
- **Copy:** playful microcopy throughout. Empty states are friendly ("No habits
yet — plant your first one 🌱").
- **prefers-reduced-motion:** all animation (pop, confetti, wiggle) degrades to
simple fades or nothing.
- **Live favicon:** the browser-tab icon is a 64×64 miniature of the current
artwork in its current state — revealed pixels solid, unrevealed ones a
two-tone checker, transparent ones the canvas background — so the tab itself
fills in as habits are checked off. Drawn into an offscreen canvas and set as
a data-URL PNG on `<link rel="icon">` (and `apple-touch-icon`); no external
file, no network request. Redrawn on reveal and on theme change, coalesced to
one repaint per frame, and only reassigned when the image actually changes.
## 6. Theming
- CSS custom properties on `:root`; `data-theme="light"|"dark"` attribute set by
JS. `"auto"` follows `prefers-color-scheme` (and listens for changes).
- Toggle button cycles auto → light → dark (icon reflects state).
- Palette direction: soft, warm, slightly candy-colored. Dark mode is a cozy
near-black with muted accents, not pure #000. Both themes must keep the
artwork canvas neutral so the art's own colors read true.
- System font stack; headers get a chunky "pixel-ish" treatment via
letter-spacing/weight (no embedded fonts — keep the file lean).
## 7. Export / Import / Wipe
- **Wipe all data:** a clearly-marked destructive button in the **Data** settings
section (styled as danger). Double-confirms, then removes the localStorage keys,
resets in-memory state to a clean slate, and returns to first-run onboarding.
- **Export:** button in Data settings → downloads
`bit-by-bit-backup-YYYY-MM-DD.json` (pretty-printed full state) via Blob +
object URL. Also a "Copy to clipboard" fallback.
- **Import:** file picker (accept `.json,application/json`) → parse → validate
(`version === 1`, `habits` is array, sanity-check shapes) → show a summary
("3 habits, 142 checks, 1 artwork in progress — replace current data?") →
on confirm, overwrite state and re-render. Invalid files get a kind error
message, never a crash.
## 8. Accessibility & Quality Bar
- Fully keyboard operable; visible focus states; habit checkboxes are real
`<input type="checkbox">` or `role="checkbox"` with labels.
- Canvas has an `aria-label` describing progress; progress also exists as text.
- Color contrast ≥ 4.5:1 for text in both themes.
- Touch targets ≥ 44px. No horizontal page scroll at 320px width.
- No console errors. No globals leaking beyond one app namespace.
- Code organized in clear sections (state, rendering, mechanics, UI wiring)
with brief section comments — one file, but readable.
## 9. Edge Cases (must handle)
1. First run / empty state → onboarding.
2. Editing any day (today or a backfilled past day within the active image's
range) recomputes reveal from the whole log: checking a missed day re-bridges
the affected habit's streak and paints the pixels it earns, unchecking removes
exactly those pixels. Because each habit's streak counts backwards from the
day it's evaluated on, editing one day never corrupts an unrelated day's
contribution. Days before the image's start or in the future are not editable.
3. Habit deleted after being checked today → today's log keeps the id; its
streak/counts keep working off the log id even though it no longer appears
in the habit list or UI.
4. Day rollover with tab open → checklist resets without reload.
5. Import of garbage / wrong-version / truncated JSON → friendly rejection.
6. localStorage full or unavailable (private mode) → app still renders; banner
warns that progress won't persist.
7. Image with very few opaque pixels (e.g., 20) finishes early — fine; celebrate.
8. A big escalating reward overshooting the last pixels just clamps to 100%.
9. Legacy saves carrying an old `checks` field, or images started under the
previous day-level/triangular reward, load fine — it is ignored and reveal
is always recomputed from the log under the current per-habit-streak rule
(revealed totals may shift from what an old version would have shown).

@ -1,33 +0,0 @@
# Paint by Habits — Planning
## Phases
1. **Design** — product spec written by Claude Fable 5 (`docs/DESIGN.md`). ✅
2. **Implementation** — single-file `index.html` implemented by Claude Sonnet
from the spec.
3. **Review & iterate** — Claude Opus reviews the implementation against the
spec; findings are fixed by Sonnet; Opus verifies.
4. **Ship** — file is validated standalone and handed off for static hosting.
## Work log
- **2026-07-13** — Project created (bean `vibed-vytp`). Design spec written.
Implementation and review pipeline run (Fable → Sonnet → Opus).
- **2026-07-13** — Sonnet delivered `index.html` (1,576 lines) with jsdom flow
tests. Opus review round 1: NEEDS-FIXES (1 major — completion-moment canvas
flash; 4 minor; 3 nits). Sonnet fixed all findings. Opus verification pass:
**SHIP**, no regressions. Final nit (render guards using `state.image`
instead of `displayImage()`) applied by Fable; `node --check` clean, zero
external references confirmed. v1 complete.
- **2026-07-13** — User-reported bug (bean `vibed-59dh`): celebration overlay
stuck onscreen from load, blocking the app. Root cause: author `display:flex`
rules override the UA's `[hidden]{display:none}`. Fixed with a global
`[hidden]{display:none !important}` rule; verified with headless Firefox
screenshots (fresh load, mid-progress, completion flow).
- **2026-07-14** — Renamed app to **Paint by Habits** (bean `vibed-orml`);
storage keys kept for backward compatibility. Built a headless-Firefox
screenshot harness (9 seeded views × mobile/desktop) and ran a design
cleanup pass (Claude Fable 5): two-column desktop layout, mobile overflow
and tab-clipping fixes, disabled-button affordances, styled file inputs,
dark-mode progress-track contrast, header typography. Follow-up bean
`vibed-p2es` (arbitrary emoji input) created, not yet implemented.

File diff suppressed because it is too large Load Diff

7
common/Cargo.lock generated

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "common"
version = "0.1.0"

@ -0,0 +1,8 @@
[package]
name = "common"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
description = "Shared types, utilities, and definitions for Vibesville services"
[dependencies]

@ -0,0 +1,12 @@
//! Shared types, utilities, and definitions for Vibesville services.
//!
//! This crate provides common building blocks used across multiple services
//! in the Vibesville mono-repo. Services depend on it via a path dependency:
//!
//! ```toml
//! [dependencies]
//! common = { path = "../common" }
//! ```
#[cfg(test)]
mod tests;

@ -0,0 +1 @@
//! Unit tests for the common crate.

@ -1,6 +0,0 @@
beans:
path: .beans
prefix: edu-
id_length: 4
default_status: todo
default_type: task

@ -1,14 +0,0 @@
---
# edu-3yw9
title: 'Write §2: Monte Carlo Tree Search — algorithm explained'
status: completed
type: task
priority: normal
created_at: 2026-03-13T20:03:17Z
updated_at: 2026-03-13T22:48:46Z
parent: edu-coqp
---
Step-by-step walkthrough of MCTS: selection (UCB1), expansion, simulation/rollout, backpropagation. Include a worked example on a small game tree.
## Summary of Changes\n\nWrote full content for §2 covering MCTS algorithm: the four phases (selection, expansion, simulation, backpropagation), UCB1/UCT formula, worked ASCII tree example, and strengths/limitations.

@ -1,14 +0,0 @@
---
# edu-453h
title: 'Write §13: The full AlphaGo Zero training loop'
status: completed
type: task
priority: normal
created_at: 2026-03-13T20:03:17Z
updated_at: 2026-03-16T01:35:06Z
parent: edu-coqp
---
Reading lesson: generate → train → evaluate → promote. Discuss the ELO-based model selection step and why it matters.
## Summary of Changes\n\nWrote full content for §13 covering the complete AlphaGo Zero training loop: self-play with temperature, training, evaluation gate, and the iterative improvement cycle with complete Rust implementation.

@ -1,16 +0,0 @@
---
# edu-4v13
title: 'Write §8: Exercise 2 — play Tic-Tac-Toe with pure MCTS'
status: completed
type: task
priority: normal
created_at: 2026-03-13T20:03:17Z
updated_at: 2026-03-13T23:04:14Z
parent: edu-coqp
---
Exercise: wire MCTS to the game logic from Exercise 1 and run a match. Show sample output, discuss iteration count vs strength.
## Summary of Changes
Wrote full content for §8: Exercise 2 with MCTS vs MCTS tournament (100 games, iteration count experiments), human vs MCTS CLI game, experimentation prompts, and readiness checklist.

@ -1,14 +0,0 @@
---
# edu-5go8
title: 'Write §3: Why self-play? The AlphaGo Zero insight'
status: completed
type: task
priority: normal
created_at: 2026-03-13T20:03:17Z
updated_at: 2026-03-13T22:51:31Z
parent: edu-coqp
---
Explain the key insight: a capable engine can be its own teacher. Historical context (AlphaGo vs AlphaGo Zero) and why the approach generalises.
## Summary of Changes\n\nWrote full content for §3 covering the evolution from Deep Blue to AlphaGo Zero, the self-play insight, the virtuous training cycle, and how we'll adapt the approach for Tic-Tac-Toe.

@ -1,16 +0,0 @@
---
# edu-7lu6
title: 'Write §12: Exercise 4 — replace rollout with the value network'
status: completed
type: task
priority: normal
created_at: 2026-03-13T20:03:17Z
updated_at: 2026-03-16T01:31:54Z
parent: edu-coqp
---
Exercise: substitute random rollout in MCTS with a neural-network value estimate; compare strength before and after.
## Summary of Changes
Wrote full content for §12: Exercise 4 covering PUCT formula, replacing random rollouts with value network evaluation, adding policy priors to MCTS nodes, modified MCTS code, and pure vs network-guided MCTS comparison.

@ -1,14 +0,0 @@
---
# edu-brtk
title: 'Write §14: Exercise 5 — 1000 self-play games; observe improvement'
status: completed
type: task
priority: normal
created_at: 2026-03-13T20:03:17Z
updated_at: 2026-03-16T01:40:32Z
parent: edu-coqp
---
Capstone exercise: run the full self-play loop for 1000 games; plot win-rate over iterations; discuss what worked and what didn't.
## Summary of Changes\n\nWrote full content for §14: Exercise 5 — the capstone exercise running 1000 self-play games, tracking diagnostics, validating convergence to perfect play, and providing next steps for further learning.

@ -1,48 +0,0 @@
---
# edu-coqp
title: 'edu: write Machine Learning chapter (self-play game AI, Alpha Go Zero style)'
status: completed
type: feature
priority: low
created_at: 2026-03-10T23:30:01Z
updated_at: 2026-03-16T01:40:56Z
---
## Background
From `edu/TODO.md`: Hands-on: Machine Learning; training a computer to play a game by playing against itself (a-la Alpha Go Zero).
A self-play reinforcement learning course. The practical focus is implementing a simplified version of the MCTS + neural network self-play loop in Rust, targeting a simple deterministic two-player game (e.g., Tic-Tac-Toe or Connect Four).
## Content outline (suggested)
### Part 1 — Foundations
1. What is reinforcement learning? (state, action, reward, policy, value)
2. Monte Carlo Tree Search (MCTS) — algorithm explained step by step
3. Why self-play? The AlphaGo Zero insight
### Part 2 — The Game
4. Choosing a simple game: Tic-Tac-Toe as the learning vehicle
5. Representing game state in Rust
6. Exercise 1: Implement the game logic (move generation, win detection, terminal states)
### Part 3 — MCTS
7. Implementing MCTS in Rust (selection, expansion, simulation, backpropagation)
8. Exercise 2: Play Tic-Tac-Toe with pure MCTS (no neural network)
### Part 4 — Neural Network Policy/Value Head
9. Overview of the network architecture (shared trunk + policy head + value head)
10. Integrating a neural network crate (e.g., `tch-rs` or `candle`)
11. Exercise 3: Train the network on MCTS-generated data
12. Exercise 4: Replace MCTS simulation with the learned value function
### Part 5 — Self-Play Loop
13. The full Alpha Go Zero training loop: generate data → train → evaluate → repeat
14. Exercise 5: Run 1000 self-play games and observe the policy improving
## File to create
- `edu/src/ml-self-play.md`
- Add to `edu/src/SUMMARY.md` under a `# Machine Learning` section
## Summary of Changes\n\nAll 14 sections written (7,622 lines total). The chapter covers:\n- Part 1 (§1-3): RL fundamentals, MCTS algorithm, AlphaGo Zero insight\n- Part 2 (§4-6): Tic-Tac-Toe as learning vehicle, Rust game state, Exercise 1\n- Part 3 (§7-8): MCTS implementation in Rust, Exercise 2 (pure MCTS play)\n- Part 4 (§9-12): NN from scratch, training on MCTS data, PUCT-guided MCTS\n- Part 5 (§13-14): Full self-play training loop, capstone exercise

@ -1,16 +0,0 @@
---
# edu-e39n
title: 'Write §5: Representing game state in Rust'
status: completed
type: task
priority: normal
created_at: 2026-03-13T20:03:17Z
updated_at: 2026-03-13T22:56:30Z
parent: edu-coqp
---
Reading lesson: design of Board, Player, Move types. Discuss representation trade-offs (bitboard vs array). Show the full type definitions.
## Summary of Changes
Wrote full content for §5 covering Rust game state representation: Player enum, GameState struct, board indexing, Display impl, move generation, winner checking, and immutable apply_move design.

@ -1,16 +0,0 @@
---
# edu-iv0k
title: 'Write §9: Neural network architecture overview'
status: completed
type: task
priority: normal
created_at: 2026-03-13T20:03:17Z
updated_at: 2026-03-13T23:07:35Z
parent: edu-coqp
---
Conceptual lesson: shared convolutional trunk, policy head (move probabilities), value head (win probability). Diagrams encouraged. No code yet.
## Summary of Changes
Wrote full content for §9 covering neural network fundamentals from scratch: neurons/weights/biases, layers, forward pass, training intuition, and the dual-headed policy+value architecture for game AI with concrete TTT dimensions.

@ -1,16 +0,0 @@
---
# edu-k3tq
title: 'Write §4: Choosing a simple game — Tic-Tac-Toe'
status: completed
type: task
priority: normal
created_at: 2026-03-13T20:03:17Z
updated_at: 2026-03-13T22:53:54Z
parent: edu-coqp
---
Explain why Tic-Tac-Toe is ideal: small state space, deterministic, zero-sum, easily verifiable. Foreshadow how the same approach scales to Go/Chess.
## Summary of Changes
Wrote full content for §4 covering why Tic-Tac-Toe is the ideal learning vehicle: suitable game properties, game tree size, known optimal solution as validation target, and comparison with alternatives.

@ -1,14 +0,0 @@
---
# edu-lqky
title: 'Write §11: Exercise 3 — train the network on MCTS data'
status: completed
type: task
priority: normal
created_at: 2026-03-13T20:03:17Z
updated_at: 2026-03-16T00:32:55Z
parent: edu-coqp
---
Exercise: generate training examples (state, policy vector, value) from pure MCTS self-play; run one training epoch; log loss.
## Summary of Changes\n\nWrote full content for §11: Exercise 3 covering MCTS data generation pipeline, TrainingExample struct, mini-batch SGD training loop, loss tracking, network evaluation, and experimentation prompts.

@ -1,14 +0,0 @@
---
# edu-of9y
title: 'Write §7: Implementing MCTS in Rust'
status: completed
type: task
priority: normal
created_at: 2026-03-13T20:03:17Z
updated_at: 2026-03-13T23:01:59Z
parent: edu-coqp
---
Walk through selection (UCB1 formula), expansion, simulation (random rollout), backpropagation. Show Rust code for the node structure and the four phases.
## Summary of Changes\n\nWrote full content for §7 covering MCTS implementation in Rust: arena-allocated node structure, all four phases implemented and explained, UCT calculation, main loop, and move selection.

@ -1,14 +0,0 @@
---
# edu-pvou
title: 'Write §10: Integrating a neural network crate'
status: completed
type: task
priority: normal
created_at: 2026-03-13T20:03:17Z
updated_at: 2026-03-13T23:11:44Z
parent: edu-coqp
---
Reading lesson: evaluate tch-rs vs candle for this use case; show how to define and initialise the network; basic forward-pass usage.
## Summary of Changes\n\nWrote full content for §10 covering from-scratch neural network implementation in Rust: Layer struct, forward pass with ReLU/softmax/tanh, illegal move masking, Xavier initialization, backpropagation, SGD training, and complete compilable code.

@ -1,77 +0,0 @@
---
# edu-tx54
title: Deploy edu mdbook to Cloudflare Pages at vibebooks.elijah.run
status: completed
type: epic
priority: normal
created_at: 2026-03-10T23:30:01Z
updated_at: 2026-03-10T23:30:01Z
---
## Background
From `edu/TODO.md`:
- Host the mdbook on Cloudflare Pages
- Host on vibebooks.elijah.run
- Create an `infra/` directory containing opentofu configs for the above
- Add a big disclaimer about the content being AI-generated
## Sub-tasks
### 1. Add AI-generated disclaimer
Add a prominent disclaimer page or preface to the mdbook. The disclaimer should say (in the user's words):
> these [chapters] are AI generated and not intended to be definitive, trustworthy, or even good, just an experiment in generating tailored educational content about topics I am interested in but not sure where to start, and with a practical focus on exercises with Rust since that is the language I use most often
Placement options:
- A dedicated `src/disclaimer.md` page added first in `src/SUMMARY.md`
- Or a callout block at the top of every chapter (requires mdbook preprocessor)
- Recommended: `src/disclaimer.md` as the first page, with a short note in each chapter's preamble
### 2. Create `edu/infra/` with OpenTofu configs
Follow the pattern from `quotesdb/infra/` (Cloudflare provider, tofu modules).
Minimal resources:
- `cloudflare_pages_project` — create the Pages project named `vibedbooks`
- `cloudflare_pages_domain` — bind `vibebooks.elijah.run` to the Pages project
- `cloudflare_record` — CNAME DNS record pointing `vibebooks` at the Pages subdomain
File layout:
```
edu/infra/
├── main.tf # provider config, cloudflare_pages_project
├── dns.tf # cloudflare_record and cloudflare_pages_domain
├── variables.tf # cloudflare_account_id, cloudflare_zone_id, etc.
├── outputs.tf # pages subdomain URL
└── .gitignore # *.tfstate, .terraform/
```
All `resource` and `data` blocks must have a comment explaining their purpose (per CLAUDE.md conventions).
### 3. Configure mdbook build for CF Pages
Cloudflare Pages can auto-build mdbook sites if given the right build command and output directory. Set in the Pages project config:
- Build command: `mdbook build`
- Build output directory: `book`
- Root directory: `edu/`
Alternatively, use a CI/CD pipeline (GitHub Actions / Gitea Actions) to build and push to the Pages project via `wrangler pages deploy`.
### 4. Add edu/justfile
Add a `release` recipe to `edu/justfile` (see justfiles ticket) that triggers the Pages deployment once infra is configured.
## Relevant files
- `edu/book.toml` — mdbook configuration
- `edu/src/SUMMARY.md` — add disclaimer page
- `edu/src/disclaimer.md` (new)
- `edu/infra/` (new directory)
- Cloudflare dashboard: Pages project, DNS zone `elijah.run`
## Validation
- `tofu validate && tofu plan` from `edu/infra/`
- `mdbook build` from `edu/` — builds without errors
- After deploy: `curl -I https://vibebooks.elijah.run` returns 200

@ -1,14 +0,0 @@
---
# edu-wobk
title: 'Write §1: What is reinforcement learning?'
status: completed
type: task
priority: normal
created_at: 2026-03-13T20:03:17Z
updated_at: 2026-03-13T22:46:18Z
parent: edu-coqp
---
Cover: state, action, reward, policy, value function. Intuitive explanation with a game-playing example. No code.
## Summary of Changes\n\nWrote full content for §1 covering RL fundamentals: agent/environment loop, state/action/reward/policy/value concepts, contrast with supervised/unsupervised learning, and why RL fits games.

@ -1,14 +0,0 @@
---
# edu-ymux
title: 'Write §6: Exercise 1 — implement Tic-Tac-Toe game logic'
status: completed
type: task
priority: normal
created_at: 2026-03-13T20:03:17Z
updated_at: 2026-03-13T22:58:06Z
parent: edu-coqp
---
Hands-on exercise: move generation, win detection, terminal-state check, displaying the board. Include starter code and expected test output.
## Summary of Changes\n\nWrote full content for §6: Exercise 1 with project setup instructions, 8 unit test specifications with collapsible solutions, a random-game main function, and a readiness checklist.

@ -1,12 +0,0 @@
---
# edu-10m1
title: '§9 Exercise 2: draw a coloured triangle'
status: completed
type: task
priority: normal
created_at: 2026-03-13T19:54:52Z
updated_at: 2026-03-16T02:30:28Z
parent: edu-4u7w
---
Write the exercise to draw a hard-coded coloured triangle: define three vertices in a vertex buffer, write a vertex shader that passes colour through, write a fragment shader that outputs the interpolated colour. First rendered geometry. Part 3 of the Shader Programming chapter.

@ -1,12 +0,0 @@
---
# edu-1nox
title: §17 Signed Distance Fields for font rendering
status: completed
type: task
priority: normal
created_at: 2026-03-13T19:55:11Z
updated_at: 2026-03-16T02:30:28Z
parent: edu-4u7w
---
Write the conceptual section on SDFs for font rendering: what an SDF encodes, why it scales better than bitmaps, how to threshold and anti-alias in the fragment shader. Brief — reading and intuition only. Part 6 of the Shader Programming chapter.

@ -1,12 +0,0 @@
---
# edu-2ak3
title: §3 What is WGSL? Syntax overview
status: completed
type: task
priority: normal
created_at: 2026-03-13T19:54:41Z
updated_at: 2026-03-16T02:30:27Z
parent: edu-4u7w
---
Write the WGSL syntax overview: types (scalars, vectors, matrices), functions, entry points, built-in variables (@vertex, @fragment, @builtin), uniforms and bindings. Show annotated snippets of a minimal vertex+fragment shader pair. Part 1 of the Shader Programming chapter.

@ -1,12 +0,0 @@
---
# edu-2sqo
title: '§13 Compute pipelines: dispatching work groups'
status: completed
type: task
priority: normal
created_at: 2026-03-13T19:55:02Z
updated_at: 2026-03-16T02:30:28Z
parent: edu-4u7w
---
Write the lesson on compute shaders: ComputePipeline vs RenderPipeline, dispatch_workgroups, workgroup size declarations in WGSL, global_invocation_id and how threads are indexed. Part 5 of the Shader Programming chapter.

@ -1,12 +0,0 @@
---
# edu-32xl
title: 'Write §1: What is a language model?'
status: completed
type: task
priority: normal
created_at: 2026-03-13T22:01:47Z
updated_at: 2026-03-16T02:30:26Z
parent: edu-u2w7
---
Next-token prediction as the core task. Intuitive framing: a model that guesses what comes next, trained on raw text. GPT-1 context. No code.

@ -1,12 +0,0 @@
---
# edu-3l9h
title: §7 Vertices, buffers, and the vertex shader
status: completed
type: task
priority: normal
created_at: 2026-03-13T19:54:49Z
updated_at: 2026-03-16T02:30:28Z
parent: edu-4u7w
---
Write the lesson on vertex data: vertex structs in Rust, VertexBufferLayout, uploading a buffer to the GPU with wgpu::util::DeviceExt, and what the vertex shader receives (position, colour, normals). Part 3 of the Shader Programming chapter.

@ -1,56 +0,0 @@
---
# edu-4u7w
title: 'edu: write chapter on shader programming'
status: completed
type: feature
priority: low
created_at: 2026-03-10T23:30:00Z
updated_at: 2026-03-16T02:32:28Z
---
## Background
From `edu/TODO.md`: Hands-on: Shader programming.
A practical introduction to GPU shaders, written with Rust as the host language. The course covers the graphics pipeline conceptually and has hands-on exercises writing WGSL shaders driven by `wgpu`.
## Content outline (suggested)
### Part 1 — The GPU and the Graphics Pipeline
1. CPU vs GPU: parallel execution model
2. The programmable pipeline: vertex shaders, fragment shaders, compute shaders
3. What is WGSL? (WebGPU Shading Language) — syntax overview
### Part 2 — Setting Up with wgpu
4. What is `wgpu`? Cross-platform graphics API in Rust
5. Exercise 1: Create a window and clear it to a colour (the GPU 'hello world')
6. The render loop: swap chains, frames, command encoders
### Part 3 — Vertex and Fragment Shaders
7. Vertices, buffers, and the vertex shader
8. Interpolation and the fragment shader
9. Exercise 2: Draw a coloured triangle
10. Exercise 3: Animate the triangle using a time uniform
### Part 4 — Textures and Samplers
11. Texture coordinates (UVs), texture creation, sampler config
12. Exercise 4: Render a textured quad
### Part 5 — Compute Shaders
13. Compute pipelines: dispatching work groups
14. Storage buffers and read/write access from WGSL
15. Exercise 5: GPU-accelerate a particle simulation
### Part 6 — Going Further
16. Post-processing effects (bloom, blur) — conceptual overview
17. Signed Distance Fields for font rendering
18. Resources: Learn WGPU tutorial, Shadertoy, The Book of Shaders
## File to create
- `edu/src/shaders.md`
- Add to `edu/src/SUMMARY.md` under a `# Graphics` section
## Summary of Changes
All 18 sections written with full educational content covering the GPU execution model, wgpu setup, vertex/fragment shaders, textures, compute shaders, and advanced topics. ~4400 lines of content including full Rust+WGSL code examples, ASCII diagrams, and 5 hands-on exercises.

@ -1,12 +0,0 @@
---
# edu-5g0l
title: '§1 CPU vs GPU: parallel execution model'
status: completed
type: task
priority: normal
created_at: 2026-03-13T19:54:37Z
updated_at: 2026-03-16T02:30:27Z
parent: edu-4u7w
---
Write the lesson on the CPU vs GPU execution model: SIMD vs SIMT, thread hierarchies (threads, warps/waves, workgroups), why GPUs excel at data-parallel work and struggle with branching. Part 1 of the Shader Programming chapter.

@ -1,12 +0,0 @@
---
# edu-6jjp
title: '§5 Exercise 1: create a window and clear it to a colour'
status: completed
type: task
priority: normal
created_at: 2026-03-13T19:54:44Z
updated_at: 2026-03-16T02:30:27Z
parent: edu-4u7w
---
Write the first hands-on exercise: create a winit window, initialise a wgpu surface, and clear it to a solid colour each frame. Walk through Instance → Adapter → Device → Surface → RenderPass with annotated Rust code. The GPU hello world. Part 2 of the Shader Programming chapter.

@ -1,12 +0,0 @@
---
# edu-7do4
title: 'Write §2: Character-level tokenisation'
status: completed
type: task
priority: normal
created_at: 2026-03-13T22:01:48Z
updated_at: 2026-03-16T02:30:26Z
parent: edu-u2w7
---
Explain BPE vs byte-level vs character-level. Motivate character-level as the simplest choice for a from-scratch exercise. Show vocabulary construction.

@ -1,12 +0,0 @@
---
# edu-7m8d
title: '§18 Resources: Learn WGPU, Shadertoy, The Book of Shaders'
status: completed
type: task
priority: normal
created_at: 2026-03-13T19:55:12Z
updated_at: 2026-03-16T02:30:28Z
parent: edu-4u7w
---
Write the resources and next-steps section: links and descriptions for the Learn WGPU tutorial, Shadertoy for experimentation, The Book of Shaders for mathematical foundations, and any other curated pointers. Part 6 of the Shader Programming chapter.

@ -1,12 +0,0 @@
---
# edu-9cnd
title: 'Write §6: The Transformer block'
status: completed
type: task
priority: normal
created_at: 2026-03-13T22:01:55Z
updated_at: 2026-03-16T02:30:26Z
parent: edu-u2w7
---
Attention sublayer + 2-layer feed-forward network + residual connections + layer norm. Describe the GPT-1 block layout. Diagrams encouraged.

@ -1,12 +0,0 @@
---
# edu-9lda
title: '§16 Post-processing effects (bloom, blur): conceptual overview'
status: completed
type: task
priority: normal
created_at: 2026-03-13T19:55:11Z
updated_at: 2026-03-16T02:30:28Z
parent: edu-4u7w
---
Write the conceptual overview of post-processing: render to texture (off-screen framebuffer), Gaussian blur as a separable two-pass compute shader, bloom as blur applied to bright regions. No full exercise — reading only. Part 6 of the Shader Programming chapter.

@ -1,12 +0,0 @@
---
# edu-9sb7
title: 'Write §14: Further reading'
status: completed
type: task
priority: normal
created_at: 2026-03-13T22:02:08Z
updated_at: 2026-03-16T02:30:26Z
parent: edu-u2w7
---
Curated pointers: Attention is All You Need paper, GPT-1 paper, Karpathy's nanoGPT, candle docs, The Illustrated Transformer blog post.

@ -1,12 +0,0 @@
---
# edu-abdu
title: 'Write §10: Cross-entropy loss and the training loop'
status: completed
type: task
priority: normal
created_at: 2026-03-13T22:02:02Z
updated_at: 2026-03-16T02:30:26Z
parent: edu-u2w7
---
Next-token prediction loss: cross-entropy over the vocab. Adam optimiser. Training loop structure: batch → forward → loss → backward → step. No bells and whistles.

@ -1,34 +0,0 @@
---
# edu-al3r
title: 'edu: write chapter on co-op worker-owned business structure'
status: todo
type: task
priority: low
created_at: 2026-03-10T23:30:01Z
updated_at: 2026-03-10T23:30:01Z
---
## Background
From `edu/TODO.md`: write a chapter about how to structure a co-op profit-sharing worker-owned business.
This is a conceptual/informational chapter, not a Rust hands-on. It fits naturally in the Vibed Learning book as a practical guide for engineers who might want to start or join a worker cooperative.
## Content outline (suggested)
1. **What is a worker cooperative?** — definition, historical examples, how they differ from traditional businesses
2. **Legal structures** — co-op incorporation options by jurisdiction (US LCA/LLC, UK LLP, etc.), relevant legislation
3. **Profit sharing models** — patronage dividends, equal shares, labor-hour weighted, hybrid approaches
4. **Governance** — one-member-one-vote, board structure, decision-making processes
5. **Practical startup steps** — founding documents, initial capital, operating agreements
6. **Case studies** — Mondragon, REI, Cooperative Home Care Associates, tech co-ops (Loomio, etc.)
7. **Resources and further reading**
## File to create
- `edu/src/co-op.md`
- Add to `edu/src/SUMMARY.md` under a new `# Business` section (or `# Other Topics`)
## Notes
This chapter is explicitly not intended to be authoritative legal or financial advice — add the standard AI-generated disclaimer.

@ -1,12 +0,0 @@
---
# edu-bycd
title: §11 Texture coordinates (UVs), texture creation, sampler config
status: completed
type: task
priority: normal
created_at: 2026-03-13T19:54:56Z
updated_at: 2026-03-16T02:30:28Z
parent: edu-4u7w
---
Write the lesson on textures: UV coordinates, wgpu texture creation from image data, TextureView, Sampler configuration (filter modes, address modes), and bind group layout for textures. Part 4 of the Shader Programming chapter.

@ -1,12 +0,0 @@
---
# edu-cr0w
title: '§10 Exercise 3: animate the triangle using a time uniform'
status: completed
type: task
priority: normal
created_at: 2026-03-13T19:54:54Z
updated_at: 2026-03-16T02:30:28Z
parent: edu-4u7w
---
Write the exercise to animate the coloured triangle: introduce uniform buffers, upload a time value each frame, rotate or pulse the triangle in the vertex shader using the time uniform. Introduces the uniform/bind-group pattern. Part 3 of the Shader Programming chapter.

@ -1,12 +0,0 @@
---
# edu-cw9v
title: 'Write §4: Embeddings and positional encoding'
status: completed
type: task
priority: normal
created_at: 2026-03-13T22:01:52Z
updated_at: 2026-03-16T02:30:26Z
parent: edu-u2w7
---
Token embedding table (vocab_size × d_model). Learned positional embeddings (GPT-1 style). Explain why position matters for attention.

@ -1,12 +0,0 @@
---
# edu-exby
title: '§15 Exercise 5: GPU-accelerate a particle simulation'
status: completed
type: task
priority: normal
created_at: 2026-03-13T19:55:05Z
updated_at: 2026-03-16T02:30:28Z
parent: edu-4u7w
---
Write the compute shader exercise: implement an N-body or simple gravity particle simulation. Store particle positions and velocities in storage buffers, update them in a compute shader each frame, render the result as points. Part 5 of the Shader Programming chapter.

@ -1,12 +0,0 @@
---
# edu-ga8p
title: §8 Interpolation and the fragment shader
status: completed
type: task
priority: normal
created_at: 2026-03-13T19:54:50Z
updated_at: 2026-03-16T02:30:28Z
parent: edu-4u7w
---
Write the lesson on rasterisation and interpolation: how values output from the vertex shader are interpolated across fragments, what the fragment shader receives and outputs (colour), and how the pipeline assembles a final image. Part 3 of the Shader Programming chapter.

@ -1,12 +0,0 @@
---
# edu-hrfy
title: '§6 The render loop: swap chains, frames, command encoders'
status: completed
type: task
priority: normal
created_at: 2026-03-13T19:54:44Z
updated_at: 2026-03-16T02:30:27Z
parent: edu-4u7w
---
Write the lesson on the wgpu render loop: SurfaceTexture acquisition, CommandEncoder, RenderPass, queue submission, present. Explain double/triple buffering conceptually. Part 2 of the Shader Programming chapter.

@ -1,12 +0,0 @@
---
# edu-hufe
title: 'Write §7: Exercise 2 — implement self-attention in Rust'
status: completed
type: task
priority: normal
created_at: 2026-03-13T22:01:56Z
updated_at: 2026-03-16T02:30:26Z
parent: edu-u2w7
---
Implement scaled dot-product attention using candle tensors. Single head, causal mask, softmax, output projection. Reader writes the core attention function.

@ -1,12 +0,0 @@
---
# edu-i76z
title: 'Write §12: Exercise 5 — sample from the model'
status: completed
type: task
priority: normal
created_at: 2026-03-13T22:02:05Z
updated_at: 2026-03-16T02:30:26Z
parent: edu-u2w7
---
Temperature sampling and greedy decoding. Prompt the trained model and decode character-by-character. Compare output at different training checkpoints.

@ -1,12 +0,0 @@
---
# edu-j35d
title: §4 What is wgpu? Cross-platform graphics API in Rust
status: completed
type: task
priority: normal
created_at: 2026-03-13T19:54:43Z
updated_at: 2026-03-16T02:30:27Z
parent: edu-4u7w
---
Write the wgpu introduction: what it is, how it maps to WebGPU/Vulkan/Metal/D3D12, the key types (Instance, Adapter, Device, Queue, Surface), and how it differs from OpenGL. Part 2 of the Shader Programming chapter.

@ -1,12 +0,0 @@
---
# edu-jybf
title: 'Write §11: Exercise 4 — train on a small text corpus'
status: completed
type: task
priority: normal
created_at: 2026-03-13T22:02:04Z
updated_at: 2026-03-16T02:30:26Z
parent: edu-u2w7
---
Use a small public-domain text (e.g. Shakespeare's sonnets or a children's book). Show data loading, batching with random windows, training loop, loss curve. Reader runs training and watches loss fall.

@ -1,12 +0,0 @@
---
# edu-kkjc
title: 'Write §13: What limits this model?'
status: completed
type: task
priority: normal
created_at: 2026-03-13T22:02:07Z
updated_at: 2026-03-16T02:30:26Z
parent: edu-u2w7
---
Honest assessment: context length, data size, model capacity, compute. Explain why GPT-1 was a big deal in 2018 and what GPT-2/3/4 changed. No code.

@ -1,12 +0,0 @@
---
# edu-r52d
title: '§2 The programmable pipeline: vertex, fragment, compute shaders'
status: completed
type: task
priority: normal
created_at: 2026-03-13T19:54:39Z
updated_at: 2026-03-16T02:30:27Z
parent: edu-4u7w
---
Write the lesson on the programmable graphics pipeline: what happens at each stage, which stages are programmable, what vertex and fragment shaders each receive and output, and where compute shaders fit. Part 1 of the Shader Programming chapter.

@ -1,12 +0,0 @@
---
# edu-s6mr
title: 'Write §5: Self-attention — queries, keys, and values'
status: completed
type: task
priority: normal
created_at: 2026-03-13T22:01:53Z
updated_at: 2026-03-16T02:30:26Z
parent: edu-u2w7
---
Derive the scaled dot-product attention formula from first principles. Single-head attention only (GPT-1 simplicity). Causal masking explained here.

@ -1,12 +0,0 @@
---
# edu-tufd
title: 'Write §3: Exercise 1 — build a character-level tokeniser in Rust'
status: completed
type: task
priority: normal
created_at: 2026-03-13T22:01:50Z
updated_at: 2026-03-16T02:30:26Z
parent: edu-u2w7
---
Implement encode/decode over a fixed character vocabulary. Read a text file, build vocab, encode to integers, decode back. No external crates.

@ -1,54 +0,0 @@
---
# edu-u2w7
title: 'edu: write chapter on creating and training a simple LLM'
status: completed
type: feature
priority: low
created_at: 2026-03-10T23:30:00Z
updated_at: 2026-03-16T02:32:26Z
---
## Background
From `edu/TODO.md`: Hands-on: Creating and training a simple LLM.
A practical course on building a small language model from scratch in Rust, covering tokenisation, the Transformer architecture, and a training loop. The goal is deep understanding rather than production scale.
## Content outline (suggested)
### Part 1 — What is a Language Model?
1. Predicting the next token: the core task
2. Tokenisation: BPE, byte-level, character-level — pick character-level for simplicity
3. Exercise 1: Build a character-level tokeniser in Rust
### Part 2 — The Transformer Architecture
4. Embeddings and positional encoding
5. Self-attention: queries, keys, values — the attention formula
6. Multi-head attention
7. Feed-forward sublayers, residual connections, layer norm
8. Exercise 2: Implement a single attention head in Rust (no ML framework)
### Part 3 — Assembling the Model
9. Stacking Transformer blocks into a decoder-only LM
10. Using `candle` (Hugging Face's Rust ML framework) for tensor ops and autodiff
11. Exercise 3: Define a small GPT-like model in `candle`
### Part 4 — Training
12. Cross-entropy loss for next-token prediction
13. The training loop: forward pass, loss, backward pass, optimizer step
14. Exercise 4: Train on a small text corpus (e.g., Shakespeare or a short book)
15. Exercise 5: Sample from the model and observe output quality vs. training steps
### Part 5 — Reflection
16. What limits this model? Scale, data, compute
17. Pointers to real LLM training (GPT-2, LLaMA, Mistral)
18. Further reading
## File to create
- `edu/src/llm-from-scratch.md`
- Add to `edu/src/SUMMARY.md` under the `# Machine Learning` section
## Summary of Changes
All 14 sections written with full educational content covering language modeling basics, the Transformer architecture, model assembly with candle, training, and reflection. ~1900 lines of content including code examples, ASCII diagrams, and exercises.

@ -1,12 +0,0 @@
---
# edu-ujs5
title: 'Write §9: Exercise 3 — define the GPT-1-style model in candle'
status: completed
type: task
priority: normal
created_at: 2026-03-13T22:02:00Z
updated_at: 2026-03-16T02:30:26Z
parent: edu-u2w7
---
Full model struct in candle: embedding, N transformer blocks, layer norm, unembedding. Hyperparams close to GPT-1 mini (e.g. 24 layers, d_model=128). Reader assembles the forward pass.

@ -1,12 +0,0 @@
---
# edu-uxa1
title: §14 Storage buffers and read/write access from WGSL
status: completed
type: task
priority: normal
created_at: 2026-03-13T19:55:04Z
updated_at: 2026-03-16T02:30:28Z
parent: edu-4u7w
---
Write the lesson on storage buffers: var<storage, read_write> in WGSL, creating STORAGE | COPY_SRC buffers in Rust, reading results back to CPU, and the difference between uniform and storage buffers. Part 5 of the Shader Programming chapter.

@ -1,12 +0,0 @@
---
# edu-vqxk
title: 'Write §8: A decoder-only LM — stacking blocks and the causal mask'
status: completed
type: task
priority: normal
created_at: 2026-03-13T22:01:58Z
updated_at: 2026-03-16T02:30:26Z
parent: edu-u2w7
---
Explain how N transformer blocks are stacked. Causal mask ensures each position only attends to past tokens. Tie weights to the unembedding matrix (GPT-1 style). Final linear + softmax for logits.

@ -1,12 +0,0 @@
---
# edu-xv9j
title: '§12 Exercise 4: render a textured quad'
status: completed
type: task
priority: normal
created_at: 2026-03-13T19:54:58Z
updated_at: 2026-03-16T02:30:28Z
parent: edu-4u7w
---
Write the exercise to render a textured quad: two triangles forming a rectangle, UV coordinates per vertex, load an image with the image crate, upload to a wgpu texture, sample it in the fragment shader. Part 4 of the Shader Programming chapter.

@ -4,7 +4,7 @@ description: work on the highest priority thing
---
* If a ticket ID or description of a ticket is provided, look for that ticket.
* If no ticket/description is provided, use `beans list --json --ready` to choose the highest-priority unblocked bean to work on.
* If no ticket/description is provided, use nbd next to choose the ticket to work on.
* Implement the plan in the selected ticket.
* Update the ticket with the new status.
* Create additional tickets based on work that may have come up during this work.

@ -0,0 +1 @@
use flake

@ -0,0 +1 @@
cache.db

@ -1,13 +1,10 @@
---
# edu-pdeo
title: '§7 Exercise 1: Storing and Retrieving Vectors'
status: completed
type: task
priority: normal
created_at: 2026-03-10T23:29:59Z
updated_at: 2026-03-10T23:29:59Z
---
+++
title = "§7 Exercise 1: Storing and Retrieving Vectors"
priority = 5
status = "todo"
ticket_type = "task"
dependencies = []
+++
## §7 Exercise 1 — Storing and Retrieving Vectors — Stub to fill
File: `edu/src/vector-db.md`, section `### 7. Exercise 1 — Storing and Retrieving Vectors`
@ -74,4 +71,4 @@ SELECT id, label, vector_extract(embedding) FROM items ORDER BY id
## Cargo.toml additions
Add `serde_json = "1"` for JSON array parsing.
Add `serde_json = "1"` for JSON array parsing.

@ -1,12 +1,10 @@
---
# edu-y4e6
title: '§18 What''s Next: Extensions and Further Reading'
status: completed
type: task
priority: normal
created_at: 2026-03-10T23:30:00Z
updated_at: 2026-03-10T23:30:00Z
---
+++
title = "§18 What's Next: Extensions and Further Reading"
priority = 5
status = "todo"
ticket_type = "task"
dependencies = []
+++
## §18 What's Next: Extensions and Further Reading — Stub to fill

@ -1,12 +1,10 @@
---
# edu-63ze
title: '§13 Generating C: Atoms and Expressions'
status: completed
type: task
priority: normal
created_at: 2026-03-10T23:30:00Z
updated_at: 2026-03-10T23:30:00Z
---
+++
title = "§13 Generating C: Atoms and Expressions"
priority = 5
status = "todo"
ticket_type = "task"
dependencies = []
+++
## §13 Generating C: Atoms and Expressions — Stub to fill

@ -1,13 +1,10 @@
---
# edu-ga52
title: '§10 Exercise 3: Semantic Document Search'
status: completed
type: task
priority: normal
created_at: 2026-03-10T23:30:00Z
updated_at: 2026-03-10T23:30:00Z
---
+++
title = "§10 Exercise 3: Semantic Document Search"
priority = 5
status = "todo"
ticket_type = "task"
dependencies = []
+++
## §10 Exercise 3 — Semantic Document Search — Stub to fill
File: `edu/src/vector-db.md`, section `### 10. Exercise 3 — Semantic Document Search`
@ -80,4 +77,4 @@ Print results ranked by distance with the passage text.
## Reference solution
Full self-contained `main.rs` inside `<details>`: creates table, embeds and inserts all 15 passages, runs three queries, prints results.
Full self-contained `main.rs` inside `<details>`: creates table, embeds and inserts all 15 passages, runs three queries, prints results.

@ -1,12 +1,8 @@
---
# edu-4gok
title: 'Markov exercise: N-gram Generalization (Rust)'
status: completed
type: task
priority: high
created_at: 2026-03-10T23:30:00Z
updated_at: 2026-03-13T22:37:26Z
parent: edu-svom
---
Write Section 8 of edu/markov.md: Exercise 4 — N-gram Generalization\n\nLearning objectives:\n- Generalize from bigrams to arbitrary-order n-gram chains\n- Use Vec<String> as a HashMap key (or a joined string)\n- Empirically compare output quality for n = 1, 2, 3, 4\n\nContent to produce:\n- Setup instructions (extend Exercise 3 project)\n- Step-by-step hints:\n 1. Modify train to use a sliding window of n words as the key\n 2. Modify generate to maintain a deque/window of the last n words\n 3. Run on the same corpus with n = 1, 2, 3, 4 and print 50 words each\n 4. Discuss observations: when does it start memorising the corpus?\n- Full reference solution\n- Stretch goal: implement character-level n-grams instead of word-level\n\nTarget: replace the stub in edu/markov.md §8
+++
title = "Markov exercise: N-gram Generalization (Rust)"
priority = 7
status = "done"
ticket_type = "task"
dependencies = []
+++
Write Section 8 of edu/markov.md: Exercise 4 — N-gram Generalization\n\nLearning objectives:\n- Generalize from bigrams to arbitrary-order n-gram chains\n- Use Vec<String> as a HashMap key (or a joined string)\n- Empirically compare output quality for n = 1, 2, 3, 4\n\nContent to produce:\n- Setup instructions (extend Exercise 3 project)\n- Step-by-step hints:\n 1. Modify train to use a sliding window of n words as the key\n 2. Modify generate to maintain a deque/window of the last n words\n 3. Run on the same corpus with n = 1, 2, 3, 4 and print 50 words each\n 4. Discuss observations: when does it start memorising the corpus?\n- Full reference solution\n- Stretch goal: implement character-level n-grams instead of word-level\n\nTarget: replace the stub in edu/markov.md §8

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save