// Closed wire vocabularies, and what each value MEANS to the user. One file per the ratified // answer to the owner's control question (companion §5): "a new product state edits one file — // the value → appearance map on the `src/api/` seam". // // Two things live here together on purpose. The list of values, because the seam has to narrow a // value that came off the wire; and the product word, because splitting them means a new status // is added in one file and stays unnamed in another. // // ⚠ The unknown branch is the point of this file, not a nicety. Generated types turn `enum` into // a CLOSED union, a minor bump may legally add a value, and the typechecker does not catch it: // indexing a `Record` by a finite union compiles silently, so `runStates[book.status]` returns // `undefined` and the tree dies on `undefined.tone` (BACKLOG Ф-22). import type { MessageKey } from '../i18n/text'; import type { components } from './schema'; type Schemas = components['schemas']; /** * Colour is the second channel: four steps, and every one of them is a STATE — work in progress, * finished, waiting on a person, refused. Never decoration. */ export type Tone = 'note' | 'ok' | 'warn' | 'danger'; export interface Vocabulary { /** The values of version 0.x. Derived from the entries, so the two cannot drift. */ readonly values: readonly Value[]; /** Narrow a value that came off the wire. An unrecognized one becomes `null`, never a throw. */ read(value: string): Value | null; /** What the value means, including the branch for a value this build does not know. */ describe(value: Value | null): Meaning; } // `Record` is what makes this exhaustive: a value added to the spec fails the // typecheck here until it is given a meaning, which is the whole mechanism. function vocabulary( entries: Readonly>, unknown: Meaning, ): Vocabulary { const values = Object.keys(entries) as Value[]; return { values, read: (value) => (Object.hasOwn(entries, value) ? (value as Value) : null), describe: (value) => (value === null ? unknown : entries[value]), }; } /** * Product status of a book. The label is a WORD, not a dot: nine states could not be told apart * by shape and tone alone — a probe put three pairs on the same pixels — so the word carries the * meaning and colour only doubles it, for attention and refusal. */ export const bookStatus = vocabulary< Schemas['BookStatus'], { label: MessageKey; tone?: Tone; startable?: boolean; intake?: boolean } >( { // `intake` — the book is still ARRIVING: it has no run, therefore no event stream, therefore // no way of announcing that it is over. The reads poll while it lasts (src/api/queries.ts). uploading: { label: 'status.uploading', tone: 'note', intake: true }, parsing: { label: 'status.parsing', tone: 'note', intake: true }, not_started: { label: 'status.notStarted', startable: true }, translating: { label: 'status.translating', tone: 'note' }, awaiting_bank: { label: 'status.awaitingBank', tone: 'warn' }, finalizing: { label: 'status.finalizing', tone: 'note' }, ready: { label: 'status.ready', tone: 'ok', startable: true }, // Resumable stop, so no danger tone: the screen must not read as a failure (D39.100 K-8). // ⚠ The wording is the OWNER'S and is not ours to shorten (В-6). S3.5 did shorten it to fit // the badge slot and had to put it back: the badge truncates with a tooltip instead, and // which word belongs in the tree is a question for the owner, not a layout decision. // // Startable, and that is not the same as resumable: a run stopped by a ceiling is not moved by // `resume` at all (contract §resumeRun), while a NEW run carries a new ceiling of its own — // which is the whole reason the ceiling travels with the start. paused: { label: 'status.paused', tone: 'warn', startable: true }, stopped: { label: 'status.stopped', startable: true }, rejected: { label: 'status.rejected', tone: 'danger' }, failed: { label: 'status.failed', tone: 'danger', startable: true }, }, // Neutral on purpose: a state from a future contract is not an error, and calling it one would // put an alarm on a screen that the product forbids to look alarming. // // ⚠ NOT startable, and the direction is picked rather than defaulted: a minor bump adds states, // and the states a pipeline gains are stages of a run in flight — exactly where starting a second // one is refused. An action offered for a state this build cannot read would be a promise made on // a guess; the state itself is still named on the card. { label: 'status.unknown' }, ); /** * Why a run is paused. The API carries state, the phrase is drawn here — and the phrase for * `credit_exhausted` is the owner's wording (D39.100 K-8), not ours. */ export const pausedReason = vocabulary( { credit_exhausted: { label: 'paused.creditExhausted' } }, // An unknown reason claims nothing about the cause. It still says the stop is resumable, // because that is true of every value of this vocabulary by contract. { label: 'paused.unknown' }, ); /** * Why a book was rejected → what the reader is told and what they can do about it (0.2.3). * * Two things ride together because they are one decision: the phrase names what happened, the * advice names the NEXT ACTION, and the next action is the whole reason the contract carries three * values instead of a boolean. "The file is not one we can read" ends in checking the file; "not * us, not now" ends in waiting. A vocabulary that carried only the phrase would leave the second * half to be re-invented by every screen that shows a rejected book. * * ⚠ The wording is this session's own (owner's word 14.08) and awaits his approval. The boundary it * is written inside is the one `Problem` states for the platform's phrases: no stages, no models, * nothing about how the parsing works. */ export const rejectReason = vocabulary< Schemas['RejectReason'], { label: MessageKey; advice: MessageKey } >( { source_unreadable: { label: 'rejected.sourceUnreadable', advice: 'rejected.sourceUnreadableAdvice', }, not_configured: { label: 'rejected.notConfigured', advice: 'rejected.notConfiguredAdvice' }, parser_unavailable: { label: 'rejected.parserUnavailable', advice: 'rejected.parserUnavailableAdvice', }, }, // A reason this build does not know — and an absent one, which the contract says to render the // same way — claims nothing about whose fault it is. It must not send the reader to check a file // that may be perfectly good, nor promise that waiting will help. { label: 'rejected.unknown', advice: 'rejected.unknownAdvice' }, ); /** * Severity step of a note → look of the callout. * * ⚠ An unknown step takes the ATTENTION branch, not the quiet one. The quiet default was the * defect: a step from a future contract would be silently demoted to "have a glance", and the one * thing a severity axis must never do is hide something that matters. The words for the steps * themselves are the owner's and are not invented here (BACKLOG Ф-21, question В-3). */ export const noteSeverity = vocabulary( { attention: { tone: 'note' }, glance: { tone: 'quiet' } }, { tone: 'note' }, ); /** * Signing status of a bank row, three-valued. `canon` — whether the term is injected as canon; it * is the one thing the contract states outright ("only `approved` is injected as canon"). * * ⚠ No product WORDS here, and that is a decision rather than an omission. The three words this * vocabulary carried ("proposed" · "draft" · "signed") came from the owner's remarks 7 and 13 (the * bank has to be verifiable) and were rendered by no screen at all: remark 20 took the per-row * signing state off the bank table — one signature covers the whole bank — and the signing screen * with its verbs is S5. Unrendered product wording drifts silently from the wording that is finally * chosen, so it is written when there is a screen to write it for. */ export const termStatus = vocabulary( { auto: { canon: false }, draft: { canon: false }, approved: { canon: true } }, // Safe direction: an unknown status does not get to claim the row is signed. { canon: false }, ); /** * Kind of term. Not cosmetic: the contract says `name` and `place` ROUTE a term into * transliteration, so a bank row shown without its kind is a row signed blind — which is why the * reference view now carries it. * * The unknown branch is the contract's own requirement, not our invention: a row whose kind the * engine could not decide "MUST be shown as kind not decided and MUST NOT be dropped". */ export const termKind = vocabulary( { name: { label: 'kind.name' }, place: { label: 'kind.place' }, title: { label: 'kind.title' }, term: { label: 'kind.term' }, nickname: { label: 'kind.nickname' }, }, { label: 'kind.undecided' }, ); /** Provenance of a bank row. Narrowing only, same reason as `termKind`. */ export const termOrigin = vocabulary>( { seed: {}, ruby: {}, mined: {} }, {}, ); /** State of a pair. Narrowing only: the reader already has one phrase for "no text yet". */ export const unitState = vocabulary>( { translated: {}, withheld: {}, pending: {} }, {}, ); /** Credit balance state. No screen reads it before S7 (settings) — see BACKLOG Ф-27. */ export const usageState = vocabulary>( { ok: {}, low: {}, exhausted: {} }, {}, );