| 1 | export type StructuredFinalization = Record<string, any>;
|
| 2 |
|
| 3 | const boundedStrings = (value: unknown, limit: number, maxLength: number) =>
|
| 4 | (Array.isArray(value) ? value : [])
|
| 5 | .filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
|
| 6 | .slice(0, limit)
|
| 7 | .map((item) => item.trim().slice(0, maxLength));
|
| 8 |
|
| 9 | export function extractFinalizationJson(value: string): unknown {
|
| 10 | const marker = value.indexOf('OPENCLASP_FINALIZATION');
|
| 11 | const source = marker >= 0 ? value.slice(marker + 'OPENCLASP_FINALIZATION'.length) : value;
|
| 12 | const start = source.indexOf('{');
|
| 13 | if (start < 0) throw new Error('Finalization JSON object is missing');
|
| 14 | let depth = 0;
|
| 15 | let quoted = false;
|
| 16 | let escaped = false;
|
| 17 | for (let index = start; index < source.length; index += 1) {
|
| 18 | const character = source[index]!;
|
| 19 | if (quoted) {
|
| 20 | if (escaped) escaped = false;
|
| 21 | else if (character === '\\') escaped = true;
|
| 22 | else if (character === '"') quoted = false;
|
| 23 | continue;
|
| 24 | }
|
| 25 | if (character === '"') quoted = true;
|
| 26 | else if (character === '{') depth += 1;
|
| 27 | else if (character === '}' && --depth === 0) return JSON.parse(source.slice(start, index + 1));
|
| 28 | }
|
| 29 | throw new Error('Finalization JSON object is incomplete');
|
| 30 | }
|
| 31 |
|
| 32 | export function parseFinalizationAssessment(
|
| 33 | text: string,
|
| 34 | interactionId: string,
|
| 35 | ): StructuredFinalization {
|
| 36 | const value = extractFinalizationJson(text) as StructuredFinalization;
|
| 37 | const outcomes = ['success', 'partial', 'failure', 'cancelled'];
|
| 38 | const workAgain = ['yes', 'no', 'unsure'];
|
| 39 | const statuses = ['met', 'partially_met', 'missed', 'unknown'];
|
| 40 | const dimensions = [
|
| 41 | 'overall_satisfaction',
|
| 42 | 'outcome_satisfaction',
|
| 43 | 'communication',
|
| 44 | 'timeliness',
|
| 45 | 'scope_adherence',
|
| 46 | 'evidence_quality',
|
| 47 | 'correction_handling',
|
| 48 | 'reliability',
|
| 49 | ];
|
| 50 | if (!outcomes.includes(value.outcome) || typeof value.summary !== 'string')
|
| 51 | throw new Error('Finalization outcome or summary is invalid');
|
| 52 | if (!workAgain.includes(value.wouldWorkAgain))
|
| 53 | throw new Error('Finalization wouldWorkAgain is invalid');
|
| 54 | if (typeof value.confidence !== 'number' || value.confidence < 0 || value.confidence > 1)
|
| 55 | throw new Error('Finalization confidence is invalid');
|
| 56 | if (!value.ratings || typeof value.ratings !== 'object')
|
| 57 | throw new Error('Finalization ratings are missing');
|
| 58 | const ratings = Object.fromEntries(
|
| 59 | dimensions.map((dimension) => {
|
| 60 | const rating = value.ratings[dimension];
|
| 61 | if (typeof rating !== 'number' || rating < 0 || rating > 1)
|
| 62 | throw new Error(`Finalization rating ${dimension} is invalid`);
|
| 63 | return [dimension, rating];
|
| 64 | }),
|
| 65 | );
|
| 66 | const criteria = (Array.isArray(value.criteria) ? value.criteria : [])
|
| 67 | .slice(0, 100)
|
| 68 | .map((item) => {
|
| 69 | if (!item || typeof item !== 'object' || typeof item.criterion !== 'string')
|
| 70 | throw new Error('Finalization criterion is invalid');
|
| 71 | return {
|
| 72 | criterion: item.criterion.slice(0, 1000),
|
| 73 | status: statuses.includes(item.status) ? item.status : 'unknown',
|
| 74 | ...(typeof item.explanation === 'string'
|
| 75 | ? { explanation: item.explanation.slice(0, 1000) }
|
| 76 | : {}),
|
| 77 | evidenceReferences: boundedStrings(item.evidenceReferences, 50, 2048),
|
| 78 | };
|
| 79 | });
|
| 80 | return {
|
| 81 | interactionId,
|
| 82 | outcome: value.outcome,
|
| 83 | summary: value.summary.slice(0, 2000),
|
| 84 | criteria,
|
| 85 | deliverables: boundedStrings(value.deliverables, 100, 1000),
|
| 86 | actionsTaken: boundedStrings(value.actionsTaken, 100, 1000),
|
| 87 | blockers: boundedStrings(value.blockers, 100, 1000),
|
| 88 | corrections: boundedStrings(value.corrections, 100, 1000),
|
| 89 | evidenceReferences: boundedStrings(value.evidenceReferences, 100, 2048),
|
| 90 | confidence: value.confidence,
|
| 91 | ratings,
|
| 92 | wouldWorkAgain: value.wouldWorkAgain,
|
| 93 | reasonCodes: boundedStrings(value.reasonCodes, 32, 128),
|
| 94 | ...(typeof value.privateComment === 'string'
|
| 95 | ? { privateComment: value.privateComment.slice(0, 1000) }
|
| 96 | : {}),
|
| 97 | };
|
| 98 | }
|