SOURCE / PINNED RELEASE
Made of little things.
Body Journal
- Release
- e8049f8da4e2…
- Author-recorded commit
- 61c04bf0e8aa…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-6c4554844c7
Archive hash verified: 4369d778da046a8b…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { parsePhotos, type CasePhoto } from './photo-record.js';
import { parseJawOpening } from './jaw-pose.js';
import { layers, type AnatomyLayer } from './layers.js';
import { parseDraft, type Draft } from './case-record.js';
import { boundedText, MAX_CASE_BYTES, object, parseReport, type Report } from './ai-contract.js';
import { poses, type Pose, type Point3 } from './case.js';
import { parseInspection, type InspectionView } from './inspection-record.js';
import { parseFingerPose, type FingerPose } from './finger-pose.js';
export interface CompletedReport { report: Report; model: string; provider: string; caseText: string; at: string; imageCount?:number; photoCount?: number }
export interface SessionRecord {
version: 1; draft: Draft;
view: { page: 'map' | 'context' | 'review'; panel: 'body' | 'profile' | 'concerns'; selectedId: number | null; expandedConcernId?: number | null; expandedGroupIds?: string[]; pose: Pose; layer?: AnatomyLayer; inspection?: InspectionView; fingers?: FingerPose; joints?: Record<string, Point3>; anatomyPanelOpen?: boolean; jawOpening?: number; newObservation?: boolean; exampleAttachment: boolean };
completed?: CompletedReport; photos?: CasePhoto[]; exampleSource?: string;
}
export function parseSession(raw: string): SessionRecord {
if (new TextEncoder().encode(raw).byteLength > 460_000) throw new Error('This session is too large to save. Remove a photo, reduce painted strokes or clear the report.');
const data = object(JSON.parse(raw));
// Reuse the existing storage slot and accept earlier manual drafts in place.
if (data.profile) {
const draft = parseDraft(raw);
return { version: 1, draft, view: { page: 'map', panel: 'body', selectedId: draft.concerns[0]?.id || null, pose: draft.concerns[0]?.pose || 'standing', exampleAttachment: false } };
}
if (data.version !== 1) throw new Error('Unsupported session.');
const draft = parseDraft(JSON.stringify(data.draft)), v = object(data.view);
if (!['map', 'context', 'review'].includes(String(v.page)) || !['body', 'profile', 'concerns'].includes(String(v.panel)) || !poses.includes(v.pose as Pose) || typeof v.exampleAttachment !== 'boolean') throw new Error('Invalid session view.');
const view: SessionRecord['view'] = { page: v.page as SessionRecord['view']['page'], panel: v.panel as SessionRecord['view']['panel'], selectedId: draft.concerns.some(c => c.id === v.selectedId) ? Number(v.selectedId) : null, pose: v.pose as Pose, exampleAttachment: v.exampleAttachment };
// Omission keeps older sessions opening their selected concern. Null records
// an intentionally collapsed editor without changing the painting target.
if (v.expandedConcernId !== undefined) view.expandedConcernId = v.expandedConcernId === view.selectedId ? view.selectedId : null;
if (v.expandedGroupIds !== undefined) {
if (!Array.isArray(v.expandedGroupIds) || v.expandedGroupIds.length > 20 || v.expandedGroupIds.some(id => typeof id !== 'string')) throw new Error('Invalid expanded groups.');
view.expandedGroupIds = [...new Set(v.expandedGroupIds as string[])].filter(id => draft.groups.some(g => g.id === id));
}
if(v.layer!==undefined) {
if(!layers.includes(v.layer as AnatomyLayer)) throw new Error('Invalid view layer.');
view.layer=v.layer as AnatomyLayer;
}
if(v.newObservation!==undefined){if(typeof v.newObservation!=='boolean')throw new Error('Invalid observation mode.');view.newObservation=v.newObservation;}
if(v.anatomyPanelOpen!==undefined){if(typeof v.anatomyPanelOpen!=='boolean')throw new Error('Invalid anatomy panel state.');view.anatomyPanelOpen=v.anatomyPanelOpen;}
if(v.joints!==undefined){
const joints=object(v.joints);if(Object.keys(joints).length>8)throw new Error('Invalid working pose.');view.joints={};
for(const [id,value] of Object.entries(joints)){
if(!/^(left|right)-(arm|elbow|hip|knee)$/.test(id)||!Array.isArray(value)||value.length!==3||!value.every(n=>typeof n==='number'&&Number.isFinite(n)&&Math.abs(n)<=Math.PI*2))throw new Error('Invalid working joint.');
view.joints[id]=value as Point3;
}
}
let completed: CompletedReport | undefined;
if(v.inspection!==undefined) view.inspection=parseInspection(v.inspection,view.layer || 'skin');
if(v.fingers!==undefined) view.fingers=parseFingerPose(v.fingers);
if(v.jawOpening!==undefined) view.jawOpening=parseJawOpening(v.jawOpening);
if (data.completed) {
const c = object(data.completed), at = boundedText(c.at, 40), caseText = boundedText(c.caseText, MAX_CASE_BYTES);
if (!Number.isFinite(Date.parse(at)) || new TextEncoder().encode(caseText).byteLength > MAX_CASE_BYTES) throw new Error('Invalid saved report context.');
object(JSON.parse(caseText));
completed = { report: parseReport(c.report), model: boundedText(c.model, 120), provider: boundedText(c.provider, 80), caseText, at };
if(c.imageCount!==undefined){if(!Number.isInteger(c.imageCount)||Number(c.imageCount)<0||Number(c.imageCount)>50)throw new Error('Invalid image count.');completed.imageCount=Number(c.imageCount);}
if(c.photoCount!==undefined){if(!Number.isInteger(c.photoCount)||Number(c.photoCount)<0||Number(c.photoCount)>(completed.imageCount || 0))throw new Error('Invalid photo count.');completed.photoCount=Number(c.photoCount);}
}
const photos = parsePhotos(data.photos, draft.concerns.map(c => c.id));
const exampleSource = data.exampleSource === undefined ? undefined : boundedText(data.exampleSource, 100);
return { version: 1, draft, view, completed, photos, ...(exampleSource ? { exampleSource } : {}) };
}
export function sessionString(record: SessionRecord): string { return JSON.stringify(parseSession(JSON.stringify(record))); }
