refactor: fold machine-remote-data into remote-data.ts, PascalCase load lifecycle (RD-11)

`machine-remote-data.ts` defined a third encoding of an in-flight fetch:
`LoadLifecycle`. It had three call sites, all one identical line, and the type
was never imported by name. Move the mapping into `remote-data.ts` as
`fromLoadLifecycle`, beside its neighbour `fromResource` — a `RemoteData`
constructor, not a sixth encoding.

The lowercase `loading`/`failed`/`loaded` tags on `BriefState`,
`OrgTemplateState` and `StamdataEditorState` existed only because
`LoadLifecycle` required them. Now that the constraint is inline and
PascalCase, the three machines' load-lifecycle tags become `Loading`,
`Failed` and `Loaded` — matching their own PascalCase message tags in the
same file. `stamdata-editor.machine.spec.ts` no longer asserts a PascalCase
message producing a lowercase state.

`BriefStatus` (the letter's draft/submitted/approved/rejected/sent status,
parsed off the wire from `BriefViewDto`) is a separate tag family and is
untouched — its tag count stays 54 before and after this change.

Delete `machine-remote-data.ts` and merge its spec into `remote-data.spec.ts`.
Regenerate `behaviour-spec.mdx` (the `machineRemoteData` section heading
becomes `fromLoadLifecycle`) and confirm `gen:snippets` reports no drift, since
`remote-data.ts` carries a showcase region.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-09-04 18:07:00 +02:00
co-authored by Claude Sonnet 5
parent 11664d2efa
commit 827c655c1b
19 changed files with 303 additions and 139 deletions
@@ -37,16 +37,16 @@ import { passagesForBesluit } from './besluit';
*/
export type BriefState =
| { tag: 'loading' }
| { tag: 'Loading' }
| {
tag: 'loaded';
tag: 'Loaded';
brief: Brief;
availablePassages: readonly LibraryPassage[];
decisions: BriefDecisions;
}
| { tag: 'failed'; reason: string };
| { tag: 'Failed'; reason: string };
export const initial: BriefState = { tag: 'loading' };
export const initial: BriefState = { tag: 'Loading' };
export type BriefMsg =
| {
@@ -110,7 +110,7 @@ function mapBlocks(brief: Brief, f: (blocks: readonly LetterBlock[]) => LetterBl
/** Apply an edit to the brief, guarded by status. A rejected letter reopens to draft. */
function withEdit(s: BriefState, f: (b: Brief) => Brief): BriefState {
if (s.tag !== 'loaded' || !isEditable(s.brief.status)) return s;
if (s.tag !== 'Loaded' || !isEditable(s.brief.status)) return s;
let brief = f(s.brief);
if (brief.status.tag === 'rejected') brief = { ...brief, status: { tag: 'draft' } };
return { ...s, brief };
@@ -189,13 +189,13 @@ export function reduce(s: BriefState, m: BriefMsg): BriefState {
switch (m.tag) {
case 'BriefLoaded':
return {
tag: 'loaded',
tag: 'Loaded',
brief: m.brief,
availablePassages: m.availablePassages,
decisions: m.decisions,
};
case 'BriefLoadFailed':
return { tag: 'failed', reason: m.reason };
return { tag: 'Failed', reason: m.reason };
case 'Seed':
return m.state;
@@ -203,7 +203,7 @@ export function reduce(s: BriefState, m: BriefMsg): BriefState {
// drafter's free text. `availablePassages` lives on the loaded state, so this stays pure.
case 'BesluitSelected':
return withEdit(s, (b) =>
s.tag === 'loaded' && isSectionEditable(b, 'kern')
s.tag === 'Loaded' && isSectionEditable(b, 'kern')
? composeKern(b, s.availablePassages, m.besluit, m.reasons)
: b,
);
@@ -275,6 +275,6 @@ function transition(
decisions: BriefDecisions,
guard: (b: Brief) => boolean = () => true,
): BriefState {
if (s.tag !== 'loaded' || s.brief.status.tag !== from || !guard(s.brief)) return s;
if (s.tag !== 'Loaded' || s.brief.status.tag !== from || !guard(s.brief)) return s;
return { ...s, brief: { ...s.brief, status: next() }, decisions };
}