156 lines
7.3 KiB
TypeScript
156 lines
7.3 KiB
TypeScript
// 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 { 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<Value extends string, Meaning> {
|
||
/** 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<Value, Meaning>` 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<Value extends string, Meaning>(
|
||
entries: Readonly<Record<Value, Meaning>>,
|
||
unknown: Meaning,
|
||
): Vocabulary<Value, Meaning> {
|
||
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: string; tone?: Tone }>(
|
||
{
|
||
uploading: { label: 'загрузка', tone: 'note' },
|
||
parsing: { label: 'разбор', tone: 'note' },
|
||
not_started: { label: 'в очереди' },
|
||
translating: { label: 'перевод', tone: 'note' },
|
||
awaiting_bank: { label: 'нужна подпись', tone: 'warn' },
|
||
finalizing: { label: 'финал', tone: 'note' },
|
||
ready: { label: 'готова', tone: 'ok' },
|
||
// 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.
|
||
paused: { label: 'остановлена: лимиты', tone: 'warn' },
|
||
stopped: { label: 'остановлена' },
|
||
rejected: { label: 'не разобрана', tone: 'danger' },
|
||
failed: { label: 'ошибка', tone: 'danger' },
|
||
},
|
||
// 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.
|
||
{ label: 'неизвестное состояние' },
|
||
);
|
||
|
||
/**
|
||
* 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<Schemas['PausedReason'], { label: string }>(
|
||
{ credit_exhausted: { label: 'перевод остановлен: лимиты исчерпаны' } },
|
||
// 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: 'перевод остановлен' },
|
||
);
|
||
|
||
/**
|
||
* 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<Schemas['NoteSeverity'], { tone: 'note' | 'quiet' }>(
|
||
{ attention: { tone: 'note' }, glance: { tone: 'quiet' } },
|
||
{ tone: 'note' },
|
||
);
|
||
|
||
/**
|
||
* Signing status of a bank row, three-valued. `canon` says whether the term is injected as canon;
|
||
* the label names the state for a reader who has to VERIFY the bank (owner's remarks 7/13/20).
|
||
*
|
||
* ⚠ The words are the contract's own description of each value, not new product verbs
|
||
* (`TermStatus`: "only `approved` is injected as canon", `auto` is "proposed by the engine,
|
||
* nobody looked", `draft` is "a human started and did not finish"). The signing SCREEN with its
|
||
* actions is S5, and no verb of that screen is invented here.
|
||
*/
|
||
export const termStatus = vocabulary<
|
||
Schemas['TermStatus'],
|
||
{ canon: boolean; label: string; tone?: Tone }
|
||
>(
|
||
{
|
||
auto: { canon: false, label: 'предложен' },
|
||
draft: { canon: false, label: 'черновик', tone: 'warn' },
|
||
approved: { canon: true, label: 'подписан', tone: 'ok' },
|
||
},
|
||
// Safe direction: an unknown status does not get to claim the row is signed.
|
||
{ canon: false, label: 'состояние неизвестно' },
|
||
);
|
||
|
||
/**
|
||
* 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<Schemas['TermKind'], { label: string }>(
|
||
{
|
||
name: { label: 'имя' },
|
||
place: { label: 'место' },
|
||
title: { label: 'титул' },
|
||
term: { label: 'термин' },
|
||
nickname: { label: 'прозвище' },
|
||
},
|
||
{ label: 'тип не определён' },
|
||
);
|
||
|
||
/** Provenance of a bank row. Narrowing only, same reason as `termKind`. */
|
||
export const termOrigin = vocabulary<Schemas['TermOrigin'], Record<string, never>>(
|
||
{ seed: {}, ruby: {}, mined: {} },
|
||
{},
|
||
);
|
||
|
||
/** State of a pair. Narrowing only: the reader already has one phrase for "no text yet". */
|
||
export const unitState = vocabulary<Schemas['UnitState'], Record<string, never>>(
|
||
{ translated: {}, withheld: {}, pending: {} },
|
||
{},
|
||
);
|
||
|
||
/** Credit balance state. No screen reads it before S7 (settings) — see BACKLOG Ф-27. */
|
||
export const usageState = vocabulary<Schemas['Usage']['state'], Record<string, never>>(
|
||
{ ok: {}, low: {}, exhausted: {} },
|
||
{},
|
||
);
|