/** * What saving a note offers to put on the record, and what happens to each offer. * * SPENCER, 2026-09-03, specifying the flow himself: *"They save the note draft and it then says 'Add the * following to the patient's record?' and shows the list of items extracted from the note they can either * add or decline to add."* * * THE UNIT IS THE ITEM, NEVER THE FIELD. No stepper and no wizard: one card per proposed item, added or * declined on its own. A sentence naming two resources is TWO items rather than two steps. * * NOTHING HERE FILES ANYTHING. This decides what is offered and what state each offer is in; the writing * is the overlay's job, after a human has pressed Add on that item. Keeping the decision separate from * the write is what makes "nothing is filed without being pressed" checkable rather than asserted. */ /** The kinds of thing a visit produces. Named by resource, shown to a clinician in their own words. */ export type ProposalKind = | 'diet' | 'vision' | 'risk' | 'condition' | 'medication' | 'referral' | 'immunization' | 'observation' | 'procedure' | 'allergy' | 'labOrder'; export interface RecordProposal { /** Stable within one extraction, so a decision survives a re-render. */ id: string; kind: ProposalKind; /** What the clinician reads first - "Low-carbohydrate diet". */ title: string; /** * Why it is being offered, in the drawing's words: "discussed in the visit". * * NOT THE QUOTE FROM THE NOTE. A proposal that echoes the sentence invites the clinician to check * the extraction's reading comprehension rather than the item, and the note is on the same screen. */ reason: string; /** What the note supplied, shown filled in the overlay's first band. */ prefilled: Record; } /** * Pending until a human decides. There is no third automatic outcome, deliberately - the whole model is * that the system proposes and a person disposes. */ export type ProposalDecision = 'pending' | 'added' | 'declined'; export interface ProposalState { proposal: RecordProposal; decision: ProposalDecision; } export const startProposals = (proposals: readonly RecordProposal[]): ProposalState[] => proposals.map((proposal) => ({ proposal, decision: 'pending' })); /** * Record a decision on one item. * * ONCE DECIDED, NOT RE-DECIDABLE. An item that has been added has a resource on the record behind it; * letting it be added again would file a second one, and letting it flip to declined would say the * clinician refused something that is already in the chart. */ export const decideProposal = ( states: readonly ProposalState[], id: string, decision: Exclude, ): ProposalState[] => states.map((state) => ( state.proposal.id === id && state.decision === 'pending' ? { ...state, decision } : state )); /** What is still waiting on the clinician. */ export const pendingProposals = (states: readonly ProposalState[]): ProposalState[] => states.filter((state) => state.decision === 'pending'); /** * The list is finished when nothing is pending - which is when *"Add anything else to the patient's * record?"* is offered. * * AN EMPTY EXTRACTION IS FINISHED FROM THE START, and that is the case worth getting right: a note the * system could read nothing structured out of must still reach the search fallback. Otherwise the * clinician who most needs the manual door is the one who never sees it. */ export const proposalsSettled = (states: readonly ProposalState[]): boolean => pendingProposals(states).length === 0; /** * THERE IS NO "ADD ALL", AND ITS ABSENCE IS A RULING RATHER THAN AN OMISSION. * * Spencer, 2026-09-03: *"keep it one at a time, no add all."* Bulk filing is how something unread reaches * a patient's record, and one wrong item filed unread is what stops a clinician trusting any of it. * * Exported as a named constant so the rule is greppable and a future "select all" fails a test rather * than a code review. */ export const ADD_ALL_IS_FORBIDDEN = true; /** * How many were offered and how many are left, for the heading. * * COUNTS THE DECIDED ONES TOO. A number that shrinks as work is done reads as the work disappearing; * this says "2 of 5 left" so the clinician can see how much of the list they have been through. */ export const proposalProgress = ( states: readonly ProposalState[], ): { total: number; remaining: number } => ({ total: states.length, remaining: pendingProposals(states).length, });