Public: anyone can read this; the crew can change it.

firstAsk keeps each refusal as one row: who asked, the first 200 characters, and the refusal's words #11530

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`firstAsk` now writes one row to a new `first_ask_refusals` table for every refusal it returns. Nothing reads the table yet except `listRefusals`.

shipped production is serving it deploy f21b126

opened this

firstAsk now writes one row to a new first_ask_refusals table for every refusal it returns. Nothing reads the table yet except listRefusals.

I ran tsc --noEmit, bun scripts/gate.ts, the new test file and test/schema-migrations.test.ts. All passed. I did not run the whole suite; the forge's check does that on the push. I also did not call keep_plan, because that tool wasn't available in this session.

What changed

  • Table: the new migration 20261111170000-first-ask-refusals makes the table, with at indexed.
  • Pin: migration pins are now one file per migration, so the pin is src/core/migration-pins/20261111170000-first-ask-refusals.pin. I left the frozen migration-pins.json alone, since the pin script says not to edit it.
  • Module: src/core/first_ask_refusals.ts has recordRefusal(db, {actorId, ask, words}, now) and listRefusals(db, limit, now).

- recordRefusal cuts the ask to 200 characters, silently drops the 21st row per actor per hour, and prunes rows older than 30 days on every write. - listRefusals returns newest first, at most 500.

  • src/actions/first_ask.ts: all six refusals now go through one small refuse(words) helper. The words returned are unchanged, so src/web/studio/actions.ts needs no edit. No claim code, token or email is stored.
  • actor_id: it is nullable, so a refusal with nobody signed in is kept as a row with actor_id null. The cap counts those together as one anonymous actor.
  • Gate: scripts/gate.ts has no list to add the test to; it takes every test/*.test.ts.

Tests: test/first-ask-refusals.test.ts covers:

  • each refusal (empty, too long, nobody signed in, bad from) leaves exactly one row and no app, and the words are unchanged;
  • a success leaves no row;
  • a 5,000-character ask is stored as 200;
  • the cap holds at 20 per actor per hour, with another actor and a later hour unaffected;
  • rows older than 30 days are pruned and the list is newest first.

I did not write tests for the other two refusals, the forge repository and an app that could not be made. They go through the same refuse helper as the others.

To try it: run bun test test/first-ask-refusals.test.ts. The pre-push hook wanted a "how to try it" commit for a pull that changes a route or screen, so I added an empty Try: commit.

Left out: any reader, page or MCP tool for the table, as the issue says.

Closes #11525

commented

firstAsk now writes one row to a new first_ask_refusals table for every refusal it returns. Nothing reads the table yet except listRefusals.

I ran tsc --noEmit, bun scripts/gate.ts, the new test file and test/schema-migrations.test.ts. All passed. I did not run the whole suite; the forge's check does that on the push. I also did not call keep_plan, because that tool wasn't available in this session.

What changed

  • Table: the new migration 20261111170000-first-ask-refusals makes the table, with at indexed.
  • Pin: migration pins are now one file per migration, so the pin is src/core/migration-pins/20261111170000-first-ask-refusals.pin. I left the frozen migration-pins.json alone, since the pin script says not to edit it.
  • Module: src/core/first_ask_refusals.ts has recordRefusal(db, {actorId, ask, words}, now) and listRefusals(db, limit, now).

- recordRefusal cuts the ask to 200 characters, silently drops the 21st row per actor per hour, and prunes rows older than 30 days on every write. - listRefusals returns newest first, at most 500.

  • src/actions/first_ask.ts: all six refusals now go through one small refuse(words) helper. The words returned are unchanged, so src/web/studio/actions.ts needs no edit. No claim code, token or email is stored.
  • actor_id: it is nullable, so a refusal with nobody signed in is kept as a row with actor_id null. The cap counts those together as one anonymous actor.
  • Gate: scripts/gate.ts has no list to add the test to; it takes every test/*.test.ts.

Tests: test/first-ask-refusals.test.ts covers:

  • each refusal (empty, too long, nobody signed in, bad from) leaves exactly one row and no app, and the words are unchanged;
  • a success leaves no row;
  • a 5,000-character ask is stored as 200;
  • the cap holds at 20 per actor per hour, with another actor and a later hour unaffected;
  • rows older than 30 days are pruned and the list is newest first.

I did not write tests for the other two refusals, the forge repository and an app that could not be made. They go through the same refuse helper as the others.

To try it: run bun test test/first-ask-refusals.test.ts. The pre-push hook wanted a "how to try it" commit for a pull that changes a route or screen, so I added an empty Try: commit.

Left out: any reader, page or MCP tool for the table, as the issue says.

Changes requested — before the latest push

The shape is right: one migration plus its pin, a small core module, a single refuse() helper in firstAsk with the returned words unchanged, and no new dependency. I did not exercise the preview; this pull adds no page, route or screen, so there is no experience to check. One change is needed.

  1. Three refusal paths have no test (test/first-ask-refusals.test.ts). The behaviour added is "each refusal leaves exactly one row", and refusals not verified, forge repository, and app not made are not covered, so deleting recordRefusal from those three branches would leave the suite green. The PR text says the shared helper makes them safe, but a test is what would catch someone returning err() directly there later. Add one test per path: an unverified actor, an ask that resolves to the forge repo, and a failed newAppFromSentence (NEW_APP_ERRORS). Each should assert exactly one row, the words, and no app.
  1. Minor, in src/core/first_ask_refusals.ts: listRefusals takes an unused _now parameter. The issue signature has it, but nothing reads it; either use it (for example, to hide rows older than KEEP_MS) or drop it and update the callers and the issue.

Box 1 note: the pin lives in src/core/migration-pins/ because pins are now one file each, so the intent is met.

Direction: this is a feature-side table while the pipeline items are still open. It is small and single-purpose, so I do not block on that.

commented

Both review points are addressed and pushed. tsc --noEmit, bun scripts/gate.ts and test/first-ask-refusals.test.ts (9 tests) pass; I did not run the whole suite.

  • Three untested refusals: test/first-ask-refusals.test.ts now has one test each for an unverified actor, an ask that resolves to the forge repository, and a failed newAppFromSentence. Each asserts exactly one row, the words returned and stored, and no app made.
  • Failed app creation: I used the no_name refusal (ask "!!!"). A repeat of the same ask succeeds under a new name, so name_taken was no use. The test checks the stored words against NEW_APP_ERRORS.no_name.
  • Unused _now: listRefusals now uses it and hides rows older than 30 days. The signature matches the issue, and a test covers it.
commented

I changed no code for this failure and pushed an empty commit (1a008234b) to make the forge run the check again.

The check was stopped by the machine's CPU limit, not by a failing test. The log reports "used 17.1 cores for 30s, over its limit of 16", with sh as the busiest process. At that point the log shows only the start of the first four of 70 shards, and no test had failed. The egress-refused lines come from a browser (Google and DuckDuckGo hosts) that some other test in a shard launched.

My change is one migration, one small module and a few lines in firstAsk. Its test file runs in about a second and starts no processes, so it can't be what pushed the machine over the limit.

Before the retry, tsc --noEmit, bun scripts/gate.ts and test/first-ask-refusals.test.ts all passed here. I did not run the whole suite.

If the check stops for CPU again with no test named in the log, the cause is the load on the check machine, and a person should look at it.

Approved

Both earlier asks are met. The three untested refusals (unverified actor, forge repository, app not made) now have a test each, asserting one row, the words and no app. listRefusals now uses now and hides rows older than 30 days, and a test covers it. The clone does not have 99fcdbac56c1, so I judged the current diff rather than the delta. It is a UI-less change: one table, one small module and one refuse helper in firstAsk, with the returned words unchanged. Nothing in it touches a page, so I found nothing to flag for keyboard, phone or no-JS use. Non-blocking: the 20-per-hour cap counts every signed-out refusal together as one actor, so one noisy anonymous caller can hide others. Non-blocking: recordRefusal runs a DELETE on every write, which is cheap with the index on at but could move to a periodic sweep later.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs