Back to Body Journal
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.

src/case-record.ts
import { photoMetadata, type CasePhoto } from './photo-record.js';
import { mouthCatalog } from './mouth-catalog.js';
import { parseJawOpening, mouthLimit } from './jaw-pose.js';
import { layers, markLayer, MOUTH_MODEL, SKELETON_MODEL, MUSCLE_MODEL, ORGAN_MODEL, type AnatomyLayer } from './layers.js';
import { skeletonCatalog } from './skeleton-catalog.js';
import { muscleCatalog, organCatalog } from './atlas-catalog.js';
import { describePose, makeConcern, poses, type Concern, type Point3, type Profile } from './case.js';
import { boundedText, MAX_CASE_BYTES, object } from './ai-contract.js';
import { ANATOMY_MODEL, ANATOMY_TRIANGLES, anatomyChoices, anatomyLabels, type AnatomyChoice } from './anatomy-contract.js';
import { SKIN_MODEL_ID, SKIN_TRIANGLES } from './skin-data.js';
import { focusLabel, hiddenLabels, parseInspection } from './inspection-record.js';
import { parseFingerPose } from './finger-pose.js';
import { GROUP_MEANING, groupName, parseGroups, type ConcernGroup } from './concern-groups.js';

export interface Draft { version: 3; savedAt: string; profile: Profile; concerns: Concern[]; groups: ConcernGroup[] }
export const DRAFT_KEY = 'body-journal-case-v1';
const MAX_DRAFT_BYTES = 380_000;
export function casePayload(profile: Profile, concerns: Concern[], groups: ConcernGroup[] = [], photos: CasePhoto[] = [], exampleSource?: string): Record<string, unknown> {
  const age = Number(profile.age);
  return {
    version: 1, source: exampleSource ? 'synthetic educational example, not a real patient' : 'adult self-report',
    ...(exampleSource ? { exampleSource } : {}),
    illustrationAnatomy: profile.anatomy || 'auto',
    bodyMapRevision: mapRevision(profile, concerns),
    profile: {
      ageYears: profile.age && Number.isInteger(age) && age >= 18 && age <= 120 ? age : null,
      sexAssignedAtBirth: profile.sex || null,
      heightCm: profile.heightConfirmed ? profile.height : null,
      weightKg: profile.weightConfirmed ? profile.weight : null,
      existingConditionsAndInjuries: profile.conditions || null,
      medicationsAndSupplements: profile.medications || null,
      allergiesAndReactions: profile.allergies || null,
      smokingHistory: profile.smoking || null, pregnancyPossibility: profile.pregnancy || null,
      recentIllnessInjuryTravelOrChanges: profile.recentChanges || null,
    },
    concerns: concerns.map(c => ({
      id: c.id, symptom: c.type, locationOrSymptomName: c.location, localized: c.localized,
      illustratedLayer: c.localized ? c.layer : null,
      inspectionDisplay: c.localized && c.inspection ? { focus: focusLabel(c.inspection), hiddenAnatomy: hiddenLabels(c.inspection), meaning: 'Illustration view only; hidden anatomy is a display choice, not an absent structure or a clinical finding. Open or uneven edges from visibility filtering are rendering artifacts, not injuries.' } : null,
      approximatePaintedSurfaceAreas: [...new Set(c.marks.flatMap(m => m.surface ? [m.surface.label] : []))],
      severityOutOf10: c.type === 'Pain' ? c.severity : null,
      onset: c.onsetDate || c.onset || null,
      ...(c.onsetDate && c.onset ? { exactOnsetDate: c.onsetDate, approximateOnsetInPatientWords: c.onset } : {}), relativeOnsetReportedOn: c.onset && !c.onsetDate ? c.onsetRecordedOn : null, onsetSpeed: c.onsetSpeed || null,
      ...(c.timingText ? { timingInPatientWords: c.timingText, timingReportedOn: c.onsetRecordedOn } : {}),
      ...(c.chronic ? { patientReportedChronic: c.chronic === 'yes' ? true : c.chronic === 'no' ? false : 'unsure' } : {}),
      pattern: c.pattern === 'Not specified' ? null : c.pattern,
      patientDescription: c.description || null, reportedTriggers: c.triggers,
      effectOnDailyLife: c.impact || null, reliefAndTreatmentsTried: c.relief || null,
      illustratedPose: c.localized ? describePose(c.pose, c.joints, c.fingers, c.layer === 'mouth' ? c.jawOpening ?? 0 : undefined) : null,
    })),
    ...(groups.length ? { patientSuspectedRelationships: parseGroups(groups, concerns).map(g => ({ id: g.id, name: groupName(g), concernIds: g.concernIds, patientExplanation: g.explanation || null })) } : {}),
    ...(photoMetadata(photos).length ? { patientPhotos: photoMetadata(photos) } : {}),
    evidenceLimits: [...(groups.length ? [GROUP_MEANING] : []), ...(concerns.some(c => c.layer === 'mouth') ? [mouthLimit] : []),photos.some(p => p.includeInAi) ? 'No physical examination or measured vital signs are provided. Patient photo metadata is listed; pixels are available only for explicitly attached images. Other images are schematic body maps. Copies are resized and compressed, may omit detail, and are not diagnostic scans. Do not infer observations from metadata alone.' : 'No examination, vital signs, patient photographs, file contents or laboratory results are provided. Any supplied images are schematic body-map annotations only.', 'Locations, selected anatomical structures and poses are patient annotations, not clinical findings. A mark on an illustrated structure does not establish that a bone, muscle or organ causes the symptom. Internal layers are approximate reference anatomy, not a prediction of this patient’s anatomy.', 'Painted surface labels are approximate. The editable location text takes precedence if they differ.', 'Null or empty fields are unknown, not absent symptoms.'],
  };
}
export function serializeCase(profile: Profile, concerns: Concern[], groups: ConcernGroup[] = [], photos: CasePhoto[] = [], exampleSource?: string): string {
  if (profile.age && (!/^\d+$/.test(profile.age) || +profile.age < 18 || +profile.age > 120)) throw new Error('Enter an adult age from 18 to 120, or leave it blank.');
  const text = JSON.stringify(casePayload(profile, concerns, groups, photos, exampleSource), null, 2);
  if (new TextEncoder().encode(text).byteLength > MAX_CASE_BYTES) throw new Error('This case is too long for the current AI bridge. Shorten the descriptions before sending.');
  return text;
}
export function draftString(profile: Profile, concerns: Concern[], groups: ConcernGroup[] = []): string {
  const text = JSON.stringify({ version: 3, savedAt: new Date().toISOString(), profile, concerns, groups });
  if (new TextEncoder().encode(text).byteLength > MAX_DRAFT_BYTES) throw new Error('This draft exceeds host storage. Reduce painted strokes before saving.');
  parseDraft(text);
  return text;
}
export function parseDraft(raw: string): Draft {
  if (new TextEncoder().encode(raw).byteLength > MAX_DRAFT_BYTES) throw new Error('Draft is too large.');
  const data = object(JSON.parse(raw)), p = object(data.profile);
  if (![1, 2, 3].includes(Number(data.version)) || typeof data.version !== 'number' || !Array.isArray(data.concerns) || data.concerns.length > 50) throw new Error('Unsupported draft.');
  const savedAt = boundedText(data.savedAt, 40);
  if (!Number.isFinite(Date.parse(savedAt))) throw new Error('Invalid draft date.');
  const finite = (value: unknown, min: number, max: number): number => {
    if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max) throw new Error('Invalid numeric value.');
    return value;
  };
  const text = (value: unknown, max = 2000) => boundedText(value, max, true);
  const bool = (value: unknown): boolean => { if (typeof value !== 'boolean') throw new Error('Invalid flag.'); return value; };
  const profile: Profile = {
    age: text(p.age, 3), sex: text(p.sex, 40), anatomy: p.anatomy === undefined ? 'auto' : text(p.anatomy, 10) as AnatomyChoice, height: finite(p.height, 135, 210), weight: finite(p.weight, 40, 160),
    heightConfirmed: bool(p.heightConfirmed), weightConfirmed: bool(p.weightConfirmed),
    conditions: text(p.conditions), medications: text(p.medications), allergies: text(p.allergies), smoking: text(p.smoking, 80),
    pregnancy: text(p.pregnancy, 80), recentChanges: text(p.recentChanges),
  };
  if (!anatomyChoices.includes(profile.anatomy)) throw new Error('Invalid illustration anatomy.');
  // Drafts preserve incomplete input; adult eligibility is checked before AI submission.
  if (profile.age && !/^\d{1,3}$/.test(profile.age)) throw new Error('Invalid saved age.');
  const seen = new Set<number>();
  const point = (value: unknown): Point3 => { if (!Array.isArray(value) || value.length !== 3) throw new Error('Invalid point.'); return value.map(n => finite(n, -100, 100)) as Point3; };
  const allowedParts = /^(torso|pelvis|head|neck|nose|(left|right)-(upperarm|forearm|hand|thigh|calf|foot|knee|elbow|shoulder))$/;
  const concerns = data.concerns.map(value => {
    const c = object(value), id = finite(c.id, 1, 1_000_000);
    if (!Number.isInteger(id) || seen.has(id) || !poses.includes(c.pose as Concern['pose'])) throw new Error('Invalid concern identity or pose.');
    seen.add(id);
    const next = makeConcern(id, text(c.location, 100), c.pose as Concern['pose'], bool(c.localized));
    if (!['Pain','Skin change','Numbness','Swelling','Other'].includes(String(c.type))) throw new Error('Invalid symptom type.');
    if(c.layer!==undefined && !layers.includes(c.layer as AnatomyLayer)) throw new Error('Invalid anatomy layer.');
    next.layer=(c.layer || 'skin') as AnatomyLayer;
    if(c.inspection!==undefined) next.inspection=parseInspection(c.inspection,next.layer);
    if(c.fingers!==undefined) next.fingers=parseFingerPose(c.fingers);
    if(c.jawOpening!==undefined) next.jawOpening=parseJawOpening(c.jawOpening);
    next.type = c.type as Concern['type']; next.severity = c.severity === null ? null : finite(c.severity, 0, 10);
    for (const key of ['onset','onsetDate','onsetRecordedOn','onsetSpeed','pattern'] as const) next[key] = text(c[key], 100);
    const validDate = (date: string) => /^\d{4}-\d{2}-\d{2}$/.test(date) && Number.isFinite(Date.parse(date)) && new Date(date).toISOString().slice(0,10) === date;
    if ((next.onsetDate && !validDate(next.onsetDate)) || !validDate(next.onsetRecordedOn)) throw new Error('Invalid onset date.');
    next.timingText = c.timingText === undefined ? '' : text(c.timingText, 600);
    if (c.chronic !== undefined && !['', 'yes', 'no', 'unsure'].includes(String(c.chronic))) throw new Error('Invalid chronic status.');
    next.chronic = (c.chronic || '') as Concern['chronic'];
    for (const key of ['description','impact','relief'] as const) next[key] = text(c[key]);
    if (!Array.isArray(c.triggers) || c.triggers.length > 8 || !Array.isArray(c.marks) || c.marks.length > 600) throw new Error('Invalid concern details.');
    next.triggers = c.triggers.map(t => text(t, 60));
    const joints = object(c.joints);
    if (Object.keys(joints).length > 30) throw new Error('Too many joints.');
    for (const [key, coords] of Object.entries(joints)) {
      if (!/^(left|right)-(arm|elbow|hip|knee|leg|shoulder)$/.test(key)) throw new Error('Invalid joint.');
      next.joints[key] = point(coords);
    }
    next.marks = c.marks.map(value => {
      const mark = object(value), part = text(mark.part, 50);
      if (!allowedParts.test(part)) throw new Error('Invalid body part.');
      const parsed: Concern['marks'][number] = { part, point: point(mark.point), normal: point(mark.normal), size: finite(mark.size, 0.001, 0.1), stroke: finite(mark.stroke, 0, 1e15) };
      if (mark.surface !== undefined) {
        const s = object(mark.surface), external = s.model === ANATOMY_MODEL;
        const catalog = s.model === MOUTH_MODEL ? mouthCatalog : s.model === SKELETON_MODEL ? skeletonCatalog : s.model === MUSCLE_MODEL ? muscleCatalog : s.model === ORGAN_MODEL ? organCatalog : undefined;
        const structure = catalog?.find(part => part.id === s.zone);
        if(catalog && !structure) throw new Error('Invalid anatomical structure.');
        const triangle = finite(s.triangle, 0, (structure ? structure.triangles : external ? ANATOMY_TRIANGLES : SKIN_TRIANGLES)-1), barycentric = point(s.barycentric);
        if (external && !Object.hasOwn(anatomyLabels, String(s.zone))) throw new Error('Invalid anatomy region.');
        if ((!external && !catalog && s.model !== SKIN_MODEL_ID) || !Number.isInteger(triangle) || barycentric.some(n => n < -0.00001 || n > 1.00001) || Math.abs(barycentric.reduce((a,b) => a+b,0)-1) > 0.0001 || !Array.isArray(s.uv) || s.uv.length !== 2) throw new Error('Invalid anatomy anchor.');
        parsed.surface = { model: String(s.model), triangle, barycentric, uv: s.uv.map(n => finite(n,-0.001,1.001)) as [number,number], zone: text(s.zone,100), label: structure ? structure.label : text(s.label,100) };
      }
      if(markLayer(parsed)!==next.layer) throw new Error('Mark belongs to a different anatomy layer.');
      return parsed;
    });
    return next;
  });
  const groups = parseGroups(data.groups === undefined && data.version !== 3 ? [] : data.groups, concerns);
  return { version: 3, savedAt, profile, concerns, groups };
}

// Non-security revision marker used only to detect stale rendered evidence.
function mapRevision(profile:Profile, concerns:Concern[]):string {
  const data=JSON.stringify([profile.height,profile.weight,profile.sex,profile.anatomy,concerns.map(c=>[c.id,c.layer,c.marks,c.joints,c.inspection,c.fingers,c.jawOpening])]);
  let a=2166136261,b=5381; for(let i=0;i<data.length;i++){a=Math.imul(a^data.charCodeAt(i),16777619);b=Math.imul(b,33)^data.charCodeAt(i);}
  return `${data.length}-${a>>>0}-${b>>>0}`;
}