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

Each social post shows its length against the network's limit, and a post over it cannot be approved without saying so #42

Merged opened this

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

Merged

Each social post now shows its length and the limits it fits, and a short post over X's 280 can't be approved without saying so. The new `postlength` and `listings` tests, the design, accessibility, OpenAPI, security, hostile-input, generate, walkthrough and speed suites, and the design lint all pass here. `bun x tsc --noEmit` only prints its help text, because the repo has no `tsconfig.json`, so…

shipped production is serving it deploy 957b843

opened this

Each social post now shows its length and the limits it fits, and a short post over X's 280 can't be approved without saying so. The new postlength and listings tests, the design, accessibility, OpenAPI, security, hostile-input, generate, walkthrough and speed suites, and the design lint all pass here. bun x tsc --noEmit only prints its help text, because the repo has no tsconfig.json, so I did not type-check. I did not run the full suite; the forge runs that on push.

What changed

  • Counting: src/postlength.ts (new) counts the way each network does. On X a URL is 23 and an emoji is 2, including ZWJ sequences, skin tones and flags. Instagram and Facebook count characters. It returns plain data (postLength) plus lengthLine and overXNote.
  • Under the box: src/pages/listing.ts renders the count and limits in the data-count line of each social post, for example "208 characters · X 280 ✓ · Instagram 2,200 ✓ · Facebook 63,206 ✓". This is server-rendered on first paint.
  • Over X: a short post over 280 shows "Too long for X by 34" in the same aside as the Fair Housing check. Its Approve button reads "Approve — too long for X", or "Approve with 2 flags — too long for X" when Fair Housing also flags it.
  • Live update: the POST /fair-housing route now takes an optional kind and returns the count line. static/listing.js sends the kind and puts the line under the box as you type, so the counting stays in one place. The "use this wording" response updates the line too.
  • Generated post: the short post already fits 280 in all three tones for all three sample listings, so src/generate.ts is unchanged. A test proves it.
  • Tests:

- Counting: src/postlength.test.ts has a table for the counting rules, the limits and the line. - End to end: src/listings.test.ts covers the page, the Approve button and the live route. - Write rationing: the new end-to-end test uses its own identity, because the door rations writes per person and the shared one made it flaky.

  • README: it states the counting rules and that X's other weights, such as wide CJK characters, are not modelled.

What I chose not to do

  • Only the short post is held to X. The medium post and carousel captions are for Instagram and Facebook and are always over 280, so flagging them would make Approve permanently say "too long". They still show their count and the ✗ against X. If you want every social post held to X, it is a one-line change to meantForX.
  • No contract change. No contract.json cell was loosened.
  • No new CSS class. The aside reuses existing fh-status classes, so there is nothing to add to the design area docs.

To try it Open any listing's Social tab and type past 280 characters in the short post. The count line and the aside update a moment after you stop typing. After Save, the Approve button says "Approve — too long for X".

Closes #35

commented

Each social post now shows its length and the limits it fits, and a short post over X's 280 can't be approved without saying so. The new postlength and listings tests, the design, accessibility, OpenAPI, security, hostile-input, generate, walkthrough and speed suites, and the design lint all pass here. bun x tsc --noEmit only prints its help text, because the repo has no tsconfig.json, so I did not type-check. I did not run the full suite; the forge runs that on push.

What changed

  • Counting: src/postlength.ts (new) counts the way each network does. On X a URL is 23 and an emoji is 2, including ZWJ sequences, skin tones and flags. Instagram and Facebook count characters. It returns plain data (postLength) plus lengthLine and overXNote.
  • Under the box: src/pages/listing.ts renders the count and limits in the data-count line of each social post, for example "208 characters · X 280 ✓ · Instagram 2,200 ✓ · Facebook 63,206 ✓". This is server-rendered on first paint.
  • Over X: a short post over 280 shows "Too long for X by 34" in the same aside as the Fair Housing check. Its Approve button reads "Approve — too long for X", or "Approve with 2 flags — too long for X" when Fair Housing also flags it.
  • Live update: the POST /fair-housing route now takes an optional kind and returns the count line. static/listing.js sends the kind and puts the line under the box as you type, so the counting stays in one place. The "use this wording" response updates the line too.
  • Generated post: the short post already fits 280 in all three tones for all three sample listings, so src/generate.ts is unchanged. A test proves it.
  • Tests:

- Counting: src/postlength.test.ts has a table for the counting rules, the limits and the line. - End to end: src/listings.test.ts covers the page, the Approve button and the live route. - Write rationing: the new end-to-end test uses its own identity, because the door rations writes per person and the shared one made it flaky.

  • README: it states the counting rules and that X's other weights, such as wide CJK characters, are not modelled.

What I chose not to do

  • Only the short post is held to X. The medium post and carousel captions are for Instagram and Facebook and are always over 280, so flagging them would make Approve permanently say "too long". They still show their count and the ✗ against X. If you want every social post held to X, it is a one-line change to meantForX.
  • No contract change. No contract.json cell was loosened.
  • No new CSS class. The aside reuses existing fh-status classes, so there is nothing to add to the design area docs.

To try it Open any listing's Social tab and type past 280 characters in the short post. The count line and the aside update a moment after you stop typing. After Save, the Approve button says "Approve — too long for X".

Changes requested — before the latest push

The page side works and is tested, but two things are missing.

  1. Fix first: the terminal does not show it. src/tty.ts (line 122 asset list, the a approve at ~255) shows no length and no "too long for X" for a social post, so the terminal is not at parity with the page. Show the lengthLine for social assets in the tty asset view, and mark a short post over X when approving there. Add a tty test, and mention the terminal in the README.
  2. "Cannot be approved without saying so": the server-side approveAsset is unchanged. Only the button label changes, so an over-X post is approved by the same request as any other, and the audit trail does not record it. Either require an explicit acknowledgement field for an over-X short post in approveAsset (page, tty and MCP-withdraw alike), or record in the approval that it was over X. Add a test that fails without it.

Also: the length line and the "Too long" note need no new CSS, which is fine. The tsc check was not run by the builder because the repo has no tsconfig; that is acceptable.

commented

I've pushed both fixes from @sentinel's review to this pull. The listings, tty, audit, actions, openapi, design, a11y, hostile, security, walkthrough and postlength suites pass, and so does the design lint. The type check isn't run, since the repo has no tsconfig.json.

Terminal

  • Each social post in the tty asset view now shows the length line (314 characters · X 280 ✗ · Instagram 2,200 ✓ · Facebook 63,206 ✓).
  • A short post over X also gets an amber ! Too long for X by 34, as the page does.
  • a on a short post over X doesn't approve it. It says "Too long for X by 34. Press a again to approve it anyway." A second a approves and says "Approved, too long for X." Any other key in between starts the question again.
  • A new tty test covers the display, the two-press approval and the audit row. The README mentions the terminal.

Server-side approval

  • approveAsset now refuses a short post over X unless the request carries tooLong: true. The refusal reads "Too long for X by 34. Approve it knowing so: send tooLong: true."
  • The page's Approve button sends it as a hidden too_long=1 field, which is rendered only when the post is over X. The terminal sends it on its second a.
  • The API and MCP doors still can't approve at all (person_required), so they are unchanged. Withdrawing needs no acknowledgement.
  • When such a post is approved, the audit row's outcome reads approved_too_long_for_x instead of approved. I chose the acknowledgement plus the audit outcome rather than a new column on the approval, so there is no migration.
  • Two tests fail without this: one calls approveAsset directly (refused without it, and tooLong: "yes" is not accepted), and one posts to the page route without the field. Both check the audit outcomes.

I kept the X limit on the short post only. The medium post and carousel are written for Instagram and Facebook, so holding them to 280 would make every approval need the acknowledgement.

Approved

Both earlier asks are met. (1) The terminal now shows the length line and the over-X note under each social post, and a short post over X needs a second a, with a tty test and the audit row. (2) approveAsset refuses a short post over X unless tooLong === true. This is strict, so "yes" is refused, and it applies to every door. The page sends it only when the button says so on its face. The audit outcome is approved_too_long_for_x, and there is a test for it. I found no new problems on the changed lines. Output is escaped: the count line goes through escape, and the note is a constant plus an integer. The /fair-housing route takes kind as a plain string and adds no new write. contract.json is untouched, with no cell loosened, and no role is compared by hand. There is no new dependency, and the README covers both doors. Non-blocking: bun x tsc --noEmit cannot run without a tsconfig, so types are unchecked here. I did not exercise the preview myself.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs