Contributions
Phase 2b lets a server send what it knows back to the community metadata database, always through the Bridge (decisions 18 and 19: every write to meta goes through the Bridge, which holds the GitHub credential; decision 8: the server never calls the Bridge). Three parts:
- Accounts on the Bridge's page: a GitHub sign-in to file as, and the agent's credential (handed to the agent container per run, see The credential hand-off).
sidecars.contribute: share a book's characters and recaps that a run kept local, without generating again.meta.correct: file an admin's edits of a matched book's details as one correction-batch issue.
Both kinds, and sidecars.generate with contribute: "issue", belong to the
server's contributions module: see
Jobs of a managed module. What
meta does with the issues is in
Contributing data.
The GitHub sign-in
The Bridge signs in to the AudioSilo Contributions GitHub App (id 5387686,
slug audiosilo-contributions; the App and its settings are in
Configuration) with the device flow,
which needs only the App's public client ID. The code is the Bridge's
internal/ghauth (a leaf: standard library only, every base URL injectable) and
internal/accounts.
- Starting (
POST /api/accounts/github/device,accounts.GitHub.Start):RequestCodeasks GitHub for a device code; the page shows theuser_code, theverification_uriandexpires_at, and the Bridge polls in the background, one flow at a time (a new one, or a sign-out, ends the one before, which then stores nothing). - Polling (
Poll,Wait): GitHub answers every poll HTTP 200, with anerrorwhile it isn't done.authorization_pendingpolls on;slow_downadds 5 seconds to the interval;expired_tokenandtoken_expiredend the flow as expired,access_deniedas denied. A poll GitHub doesn't answer (unreachable, a 5xx or a 429) adds 5 seconds and polls on until the code expires. - Tokens. The answer is a
ghu_user token (8 hours) and aghr_refresh token (6 months).User(GET /user) reads the login. The pair and both expiries are sealed as one vault value,github_user_token. - Refreshing (
accounts.GitHub.Token): before the access token expires the Bridge refreshes it with the client ID alone (grant_type=refresh_token, no secret). A refresh ends the old pair at once, so it is single-flight, the new pair is persisted before it is used, and the refresh is detached from its caller (a page request that goes away mid-refresh would otherwise lose the rotated pair). An answer carrying any OAutherrorcode, whatever its HTTP status, isErrBadRefresh(a spent refresh token answersincorrect_client_credentials) and refuses the sign-in. Any other failure counts as no answer (GitHub unreachable, a non-2xx without an OAuth error, an answer that isn't JSON or carries no token): it keeps the pair (the access token serves until it expires) and is tried again a minute later. A refresh GitHub took whose answer was lost has spent the pair, so that next try refuses the sign-in. Once the access token has expired as well,Tokenanswersaccounts.ErrUnrefreshed(wrapping the refresh's error), andcontrib.TokenSource.Resolve, with no environment token orghto fall back on, answerscontrib.ErrSignInUnrefreshed. That is anErrNoCredentialforerrors.Is, so a caller that only asks whether a token resolves treats it as none. The job failures (below) and a park's reason on the Bridge's Jobs tab word it apart from "not signed in": waiting until GitHub can be reached ends it, not a new sign-in. The console's park line, the bell and the notification go by the park code alone, so one sentence covers both causes. - Refused and OK. A
401on the current device token marks the account refused (refused_at,last_error), which dropscan_contribute. A 401 on a token a refresh has since replaced is ignored, and so is one on a token already past its own expiry (served while GitHub couldn't be reached to refresh it): the refresh decides then, and a revoked sign-in is still refused through it (ErrBadRefresh). A REST call that works moveslast_ok_at(contrib.NewClientForwires both hooks); it never ends a refusal, only a new sign-in does. - Signing out (
DELETE /api/accounts/github,SignOut): ends a flow in progress and deletes the sign-in here first (the vault value and the accounts row), thenPOST https://api.github.com/credentials/revokewith both tokens and no auth (202), best effort: a revoke that fails leaves the sign-in deleted, and the answer'srevoke_errorsays so for the page to show as a warning. The page's manage link (manage_url,https://github.com/settings/apps/authorizations) shows only while signed in.
A sign-in that changes (signed in, signed out, refused) kicks a heartbeat, so the
console's Setup card and the contribute feature follow within seconds, and
re-admits every book parked contrib_unavailable, whatever its reason, once the
Bridge can contribute and its engine has started. A Bridge start never re-admits
them, so a sign-in that lands while the engine is still starting (a first native
start downloading ffmpeg) leaves them for Retry. A book parked there while
signed in (the sign-in's refresh couldn't reach GitHub) sees no change, and a
refresh that later succeeds re-admits nothing, so it waits for Retry.
While the sign-in's expired access token can't be refreshed and there is no
environment token or gh to fall back on, nothing resolves, so the Bridge
reports it as unable to share until a refresh succeeds (the next token request a
minute later, the heartbeat's own included): the heartbeat's can_contribute
(and the estimate's) is false, so the server drops the contribute feature (the
contributions module flag is false, and the console's Modules card says "Sign
in to GitHub on your Bridge"), the console's Setup card reads as not signed in,
and the console offers no share, no Suggest corrections and no contribute
switch, while the Accounts tab keeps its Signed in badge (sharing uses
"None"). A job queued from a page loaded before then is refused
409 cannot_contribute. None of these says that GitHub is unreachable; only the
job failures (below: a job queued before the heartbeat told the server), the
park's reason (pipeline.ContribUnrefreshedMsg) on the Bridge's Jobs tab and a
warning in the Bridge's log at each failed refresh do. The console's park line,
the bell and the notification read the same for both causes.
The accounts table (migration 0006) keeps only what the page shows, never a
credential: provider (github, agent, or agent/<kind>, which keeps when that
kind was set), login, signed_in_at, last_ok_at, refused_at, last_error.
performed_via_github_app on an issue proves only that it was filed with a token
of this App, which anyone can get: meta treats it as a routing hint and validates
every body.
The page's routes
On the Bridge's page API (the page's session, like every other route; see Development). No route ever answers a credential back.
| Route | What it does |
|---|---|
GET /api/accounts | {github, agent} |
POST /api/accounts/github/device | starts a sign-in; answers {user_code, verification_uri, expires_at} |
DELETE /api/accounts/github | signs out (also ends a sign-in in progress); answers {accounts, revoke_error} |
PUT /api/accounts/agent | {kind, value}: seals one agent credential; answers the accounts |
DELETE /api/accounts/agent?kind= | removes one kind ("" for all); answers the accounts |
github: available (a client ID is configured: always, as an empty
github.client_id falls back to the built-in one), signed_in, refused,
login, signed_in_at, last_ok_at, last_error, pending (the code above, or
null), flow_error (why the last flow ended without a sign-in), env_token
(AUDIOSILO_BRIDGE_GITHUB_TOKEN is set; it wins unless GitHub refused it), from
(the token used now: env, device, gh or ""), can_contribute,
manage_url.
agent: backend, kinds (the kinds that backend takes, in the order it uses
them: claude_oauth_token, anthropic_api_key for claude; openai_api_key for
codex), set (kind to boolean), set_at (when the set last changed: a save, or
a removal that left others), set_at_by_kind (Phase 2b, additive: each held
kind's own save time; a kind saved before per-kind rows existed is left out),
available, detail, handoff (the runs get the Bridge's credential) and
container_credential (the kind the container has of its own, which wins; ""
for none). A value must be one
printable line without spaces, at most 2048 bytes (ErrInvalidCredential, said
without the value). Changing a credential resumes the books and description
jobs parked agent_credential_refused and agent_unavailable
(Parks).
The page's Accounts tab (web/src/components/tabs/AccountsTab.tsx,
web/src/lib/accounts.ts) polls every 3 seconds only while a sign-in is pending.
Each agent field says Set with that credential's own saved time
(set_at_by_kind), and the GitHub card says the App opens issues, plus a secret
gist when a characters or recaps file is too long for an issue.
Sharing later: sidecars.contribute
"Share with the community" after a run kept on the server (decision 17). Family
sidecars; a book path required; it writes nothing into the library.
The server (bridge/contribute.go) takes params strictly:
{work_id?, dimensions?}, dimensions from characters and recaps, each once,
stored in that order (absent or empty means both); a work_id at most 200 bytes,
no control characters. It is refused 409 generate_open while a
sidecars.generate job is open for the book, and a new generate job is refused
409 contribute_open while a contribute job is queued or working; it cancels a
parked one, or a queued one held while the contributions module is off ("a
generate job for the book replaced what it was sharing").
The Bridge (generate/contributejob.go, light lane) reads its params
leniently (a later server's field ignored), then:
- finds the book through the library's mapping without needing it writable
(
prepareRead); - finds the engine book that keeps the results (
notesBook: the sidecars survive the transcript purge) and holds it, so no stage runs on it meanwhile; pipeline.Executor.Sharefiles them the way the contributing stage does in issue mode, with no agent and no spend. The work is the job'swork_id(a retired slug followed to its survivor and recorded on the book), else the Bridge's own match for the engine book (the console sends nowork_id), else the engine book's exact identifiers; an unmatched book fails ("Match the book first"): a share never proposes a new work. A dimension counts as filed only when its row issubmitted,pr_openormerged(a local export, acoveredverdict or a closed issue is filed again); one already filed is kept and reported. A dimension the community already has iscovered. Each dimension's gist and issue are guarded against a second filing across shares and the contributing stage's retries. A filing marker is recorded before each create (contrib.filing/<book id>/<kind>), timed byClient.Now: GitHub's clock, learned from theDateof its answers (the Bridge's own while GitHub hasn't answered). A create GitHub refused, or one that never left (contrib.NothingFiled), clears its time from the marker. The next submit then:- adopts the gist or issue the user made since the marker, less
contrib.FilingSkew(GistsSince,IssuesSinceBy; the rows of pages read before one that failed count too); - files again when GitHub's list is complete and holds none (for an issue, also
only once the marker is a minute old,
contrib.FilingSettlethroughClient.Settling: GitHub may still be making it; an issue edited since doesn't match, so it is filed again); - files again (
refileStale, logged) when the list can't reach back to the marker (contrib.ErrListIncomplete: the list since the marker only grows, so no later lookup settles it) and the marker is over 24 hours old by GitHub's clock (contrib.FilingStale;Client.StaleFilingis false until GitHub has answered that submit): meta's intake sends a member the work already has to a maintainer, and nothing on the page clears a marker; - otherwise (a refusal, a rate limit, an outage, a cancel) fails the share
(
ErrFilingUnknown) with the marker kept, whatever its age; a401there is worded as the refused sign-in (below).
- adopts the gist or issue the user made since the marker, less
Result:
{
"library_id": 1, "path": "Jack London/The Call of the Wild", "work_id": "the-call-of-the-wild",
"contributed": { "characters": "https://github.com/KodeStar/audiosilo-meta-community/issues/41" },
"covered": ["recaps"]
}
Failures, worded for the console: not signed in ("The Bridge isn't signed in to
GitHub: sign in on the Bridge's Accounts tab, then share the book again."), signed
in but the sign-in couldn't be refreshed ("The Bridge's GitHub sign-in couldn't be
refreshed because GitHub couldn't be reached: share the book again once it
can."), GitHub refused the sign-in during the share (a 401: "sign in again on
the Bridge's Accounts tab, then share the book again"; a sign-in GitHub had
already refused resolves no credential, so it reads as not signed in), nothing
kept here ("it was not generated here, or they were removed"), a generate job
still running, not matched, and an earlier share that stopped while filing when
the Bridge can't yet tell whether GitHub made the issue or the gist it links ("so
it filed nothing more: share the book again in a few minutes, and if this keeps
happening, look for the issue on the community repository"). A finished job
whose contributed is empty and covered isn't is "Already in the community" in
the server's notification (bridge.Job.SharedNothing). The unrefreshed sign-in
is told apart before the share starts (Options.ContribCredential) and, when
filing parks contrib_unavailable, from the park's Cause (the resolve error),
never from the reason's text.
The console (features/bridge/share-section.tsx, shareOffered) offers it in
the book's Community card only while the module's flag is on, an online Bridge
offers sidecars, the brief estimate's can_contribute is true and the community
answered, no generate or contribute job is open, and only for the dimensions the
book has locally, the community lacks and no earlier job contributed
(contributedLinks). An existing share job shows with its Cancel whatever the
flag says.
Corrections: meta.correct
"Suggest corrections" (decision 18, CORRECTIONS-SPIKE sections 4.1 and 5). Family
and feature meta; a book path required; it writes nothing into the library.
Params (strict, bridge.CheckCorrect; the bounds are in the
API reference, the field list is
bridge.CorrectionFields):
{
"work_id": "project-hail-mary",
"recording_id": "ray-porter-2021",
"asin": "B08G9PRS1K",
"changes": [ { "field": "title", "current": "Project Hail Mar", "corrected": "Project Hail Mary" } ],
"evidence": "Corrected in an AudioSilo server library (audiosilo-server 2.5.0).",
"source": "ignored: the server stamps it"
}
Beyond the reference: in the changes' values and the evidence a control character
is unicode.IsControl (C1 included; the identifiers refuse C0 and DEL only), and both it and a bidirectional control
(isBidiControl) are looked for in the text with its character references
decoded the way the community's issue form reads them (checkProse on
decodeRefs, mirroring audiosilo-meta's issueform formValue: each numeric or
named HTML reference ending in ; decoded, one naming whitespace read as a space,
one that decodes to more than two characters left as written; GitHub shows the
evidence's decoded too). The server (and the console) also decode an upper-case
...;, which meta's form leaves as written, on purpose: GitHub shows it
decoded, so re-aligning decodeRefs with formValue must keep [xX]. A
reference is ASCII, so this covers the characters as written as well, and
‮ is refused like the character itself. Only references to the other
C0 characters, DEL and the C1 points Windows-1252 leaves unmapped (0x81, 0x8D,
0x8F, 0x90, 0x9D) count as control characters: one to whitespace reads as a
space, � as U+FFFD, and the rest of 0x80-0x9F as Windows-1252 characters. The lengths (500, 2000) count the text as written, trimmed,
before decoding. The decoded text isn't trimmed again, so a U+0085 before a
trailing   (which a trim after decoding would drop) is refused. A corrected value is then read as the community reads it, decoded
and trimmed (asRead), and compared with current, which is taken as the record
spells it: one that so reads as nothing or as current changes nothing and is
refused (a corrected Tom & Jerry reads Tom & Jerry: refused against a
current Tom & Jerry, a correction of a current Tom & Jerry). An evidence
of exactly _No response_ (the form's blank field), or one that reads as nothing,
is "evidence is required". evidence keeps its line breaks (CRLF stored as LF)
and tabs, and stays within 2000 characters both as written and as the issue
carries it (issueLength: a line starting with # loses its indent and gains a
backslash); asin, isbn and region (the marketplace the ASIN sells in) say
how the book was found.
What the Bridge would refuse when filing is a 400 here, except a work_id the
community doesn't have: the server checks it only as text (at most 200 bytes, no
control characters), and such a job fails on the Bridge. Then the handler's
checkCorrections (security-relevant: the server files only an admin's own
edits) requires each change's field to be edited on the book
(catalog.SourceEdited: never a scanned, path, ABS or accepted-community value)
with corrected equal to the book's value now (MatchState.EditedValue), else
400 not_edited naming the field. It stores the book's own spelling of each
corrected value and stamps source as audiosilo-server/<version> whatever
well-formed source was sent (at most 128 bytes, no control characters). Only the
corrected values are bound to the book: current and the records the job names
(work_id, recording_id, asin, isbn, region) are the caller's (the console
sends the book's community match).
The Bridge (internal/correct, light lane; ParseParams strict, work_id
required, an unknown field refused, the params at most 64 KiB as the server
stores them, which a batch at every server bound fits; MapCorrections bounds
the batch again as meta does: see the workspace CROSS-REPO.md §23 before raising
a bound or adding a field):
-
resolves the GitHub credential before reading the work's community record or GitHub's issues (only a resumed job's kept result, a marker with no time to look from (below) and the Bridge's community metadata being off,
errMetaOff, come first): none failserrNoCredential("The Bridge isn't signed in to GitHub: sign in on the Bridge's Accounts tab, then suggest the corrections again."), a sign-in that couldn't be refreshed (contrib.ErrSignInUnrefreshed) failserrUnrefreshed("The Bridge's GitHub sign-in couldn't be refreshed because GitHub couldn't be reached: suggest the corrections again once it can."); -
reads the work's live coverage (
metaops.FreshCoverageForWork;errMetaOffwhen itsmetadata.base_urlis off) and picks the recording: the job'srecording_id, else the one recording that lists the job's ASIN (in itsregion, when given) or ISBN; none, or several, is no recording; -
maps the changes onto meta's batch vocabulary (
contrib.MapCorrections):Server field Meta record and field Applied by titlework titlemeta's bot authorwork authorsa maintainer series,series_index,more_serieswork series,series_index,more_seriesa maintainer publishedrecording release_datemeta's bot isbnrecording isbn(added)meta's bot narratorrecording narratorsa maintainer The series fields go to the work, which meta's batch form takes, not to the series record's
works[]entry the spike drew (section 5). A recording field with no recording is addressed to the work, and meta hands it to a maintainer (who finds the recording). Anything else (an unknown field, a duplicate field (two distinct ISBNs excepted), a value past the bounds) isErrInvalidCorrectionfor the whole issue; -
composes the issue (
contrib.CorrectionBatchIssue, meta's form rendered verbatim): title[corrections] <work title>, labelsdataanddata:correction-batch, sections Record (the work's page on meta.audiosilo.app), Changes (fenced JSON{"v": 1, "source": "...", "changes": [{record, field, current, corrected}]}, every change naming its record), Evidence / source and the ticked CC0 dedication; -
files one issue on the core repository (audiosilo-meta) with the GitHub token, and reads no labels back: an issue filed with the Accounts sign-in (the App's token) is labelled by meta's
route.yml, while one filed withAUDIOSILO_BRIDGE_GITHUB_TOKENorghby an account that can't label there arrives without labels and waits for a maintainer.
Guarded against filing twice, per request. A job's result is kept under
meta.correct/<server id>/<job id>, so a resumed job (the same job run again
after a restart) returns it. The filing itself is guarded per request, not per
job or book: before CreateIssue a marker {since, sum, job} (since GitHub's
time from Client.Now, sum the contrib.IssueSum of the title and body sent)
goes into the Bridge's store under meta.correct.filing/<server id>/<request>,
the request being what the admin asked to change (filingKey): the sha256 of the
record and the changes as a set of {field, corrected}, sorted. The record is
the work_id and the recording_id when there is one, else the ASIN, ISBN and
region that pick the recording, in the forms ParseParams gives them (the ASIN
upper case, the region lower case). The evidence, the server's source stamp,
each change's current value and the order of the changes don't count: the
dialog starts from its default evidence each time it opens
(book.corrections.evidencePrefill, which names the server's version, in the
admin's language). So the job resumed, or a new job asking for the same changes
(the admin suggesting them again after a lost answer, whatever the evidence says
now and wherever the book is on disk), finds the marker. Another field, another
corrected value (editing the book's title changes its row's), or a change added
or dropped is a new request and files again. So is a retry after the community
applied some of the rows between the lost answer and the retry: the console
offers only the rows that still differ, so its change set shrinks (an accepted
duplicate, below). The marker is deleted only while it is still the one the run
wrote or adopted (clearFiling): once the filed or adopted issue is recorded as the job's
result (a failed result write keeps it, so the job resumed adopts the issue), when
nothing can have been filed (contrib.NothingFiled: a 4xx answer, a rate limit,
or a request that never left), or once a person has been told the Bridge can't
tell (below). A 5xx, which can come after the issue was made, a timeout or a lost
answer (the client never resends a POST) keeps it and fails the job
errNoAnswer ("GitHub didn't confirm the issue ... suggest them again in a
minute"). The user guide quotes these failures word for word
(Suggest corrections).
A run that finds a marker lists the core repository's issues made since its time
less contrib.FilingSkew (contrib.IssuesSince: the list endpoint, not search,
which lags; the rows of pages read before one that failed count too) and:
- adopts a
[corrections]issue whose sum is the job's or the marker's, wherever it is in the list. The marker's is the request as composed then: the title is the work's live title, so a title correction meta's bot applied since (or a recording that took the ASIN since) changes what the run composes now, and so does other evidence or a newsourcestamp. The job that wrote the marker (resumed) can always adopt the marker's sum; another job only while the marker is under a day old by GitHub's clock (contrib.FilingStale), unless the new job composes it exactly (the same evidence andsourcestamp, with the work's title and the recording unchanged): its own sum adopts the issue at any age, open or closed. Otherwise the same changes suggested again later are no retry of the lost answer (whose issue may be closed by then): that issue reads as a same-record batch (below) and the person is told once; - on a
401(contrib.Unauthorized, even past a same-record batch on a page read before) failserrRefused(sign in again on the Accounts tab), and on a rate limiterrRateLimited, the marker kept either way; - when the list is complete back to the marker and holds nothing, but the marker
is under a minute old by GitHub's clock (
contrib.FilingSettle,Client.Settling: GitHub may still be making the issue), failserrSettling("suggest them again in a minute"); - for a batch with the same Record (the work's page, whatever the recording)
and another body (
otherBatch: anyone's, filed by hand or from another server, or its own edited since) fails asking the admin to look for a[corrections]issue first (errFiledBefore), whatever the marker's age. It is said once only when the list was read back to the marker whole and the marker has settled: then the marker goes, so the next suggestion files. Otherwise the marker stays and the next suggestion is told again. A marker with no time to look from is told the same way, once, before the credential is resolved or anything is read from meta or GitHub; - files when the list is complete back to the marker and holds none;
- for any other lookup failure (unlike a share's, also a list it can't read at
all) fails
errFiledBeforewith the marker kept, until the marker is over 24 hours old by GitHub's clock (contrib.FilingStale;Client.StaleFilingis false until GitHub has answered that run): then it files again.
meta's intake labels a batch whose every change the record already holds
data:duplicate; one with a field a maintainer applies reaches a maintainer
again. On the create, GitHub's refusal (a 4xx, a rate limit) clears the marker,
since nothing was filed: a 401 fails errRefused, a rate limit
errRateLimited, and any other refusal "could not file the corrections: ...".
Duplicates stay possible, accepted, and meta's intake catches a batch already
applied: the same corrections suggested again after they were filed (a confirmed
filing clears its marker, and the console unticks only the rows the book's
newest corrections job suggested with the same value, and none when that job
failed or was cancelled), a new request (a change added, dropped or corrected
differently) that repeats changes already filed, among them a retry after the
community applied some of the rows since the lost answer (the console's change
set shrinks) and a retry after the community changed which recording or work
the book's ASIN or ISBN names (the console resolves the record from a live
lookup each time the dialog opens, so the key changes), a refile once the marker
is over 24 hours old, the next suggestion after an otherBatch that was the
run's own issue edited since or, for another job once the marker is over a day
old, the lost filing's issue unedited (the job composes another: other
evidence, a new source stamp, a retitled work), and a POST still in flight
after the one-minute settle when the lookup finds nothing.
Result:
{ "issue_url": "https://github.com/KodeStar/audiosilo-meta/issues/2701", "issue_number": 2701,
"changes": 2, "auto": ["title"], "maintainer": ["narrator"] }
auto and maintainer are as known at filing. The Bridge doesn't follow the
issue, so a batch meta applies only in part isn't reported back (deferred).
The console (features/book/corrections-dialog.tsx, corrections-model.ts)
offers Suggest corrections in the Community card (correctionsOffered) while
the module's flag and capabilities.metadata are on, the online Bridge lists the
meta feature, the book is matched with an ASIN or ISBN, and an edited
correction field holds a value (isbn only beside an ASIN: without one the ISBN
is how the community finds the book); CorrectionsSection also holds the offer
back while a corrections job for the book is open, showing its status instead. The dialog looks the book up by ASIN, else
ISBN (matchBook), takes the candidate the identifier resolved, diffs the edited
values against the community's (communityValues for that recording;
published against the recording's release date, and no change when it is the
same as, or less precise than, that date or the work's first publication; an
ISBN the recording already lists is no change), ticks each sendable row not suggested by the book's last
corrections job, badges it Applied automatically or Reviewed by a
maintainer (handling: a recording field is automatic only when the identifier
names the recording, correctionRecording), requires the evidence (prefilled)
and queues one meta.correct. The job's status and issue link show in the same
card part. The server enforces the 25-change cap and the kind's params cap
(API reference), which a
batch at every bound fits. The dialog runs the server's checks (above) on each
value and the evidence, so it never sends what would get the 400: decodeRefs
mirrors the server's (numeric references as Go's html decodes them, named ones
through the browser's own HTML parser, ≪⃒ and ≫⃒ left as written as Go
leaves them) and goTrim trims as Go's strings.TrimSpace does.
- A row is left unticked, its box disabled (
sendable), with the first flag in the ordertooLong,control,bidi,noChange(refusal) shown asbook.corrections.<flag>: a value over 500 characters, a control character or a bidirectional control (U+202A-U+202E, U+2066-U+2069) as written or as a character reference that reads as one (above), or a corrected value that reads as the community's value or as nothing. - An evidence problem stops Suggest N corrections sending anything
(
correctionProblem, shown asbook.corrections.<problem>):noEvidence(blank,_No response_or reading as nothing; shown once Suggest N corrections has been pressed),evidenceTooLong(over 2000 characters as written or as the issue carries it,issueLength),evidenceControl(a control character other than a line break or a tab, written or as a reference that reads as one) andevidenceBidi.
The dialog's header clamps the book's title to two lines (line-clamp-2, the
whole title in its title attribute and read out), so a long title never
squeezes the review on a phone.
Notifications
The server words the new kinds itself (internal/notify, bridge_job_finished
with shared_nothing, and the contrib_unavailable park): see
Notifications.