@the-cascade-protocol/sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +164 -0
  3. package/dist/consent/consent-filter.d.ts +21 -0
  4. package/dist/consent/consent-filter.d.ts.map +1 -0
  5. package/dist/consent/consent-filter.js +71 -0
  6. package/dist/consent/consent-filter.js.map +1 -0
  7. package/dist/consent/index.d.ts +2 -0
  8. package/dist/consent/index.d.ts.map +1 -0
  9. package/dist/consent/index.js +2 -0
  10. package/dist/consent/index.js.map +1 -0
  11. package/dist/deserializer/index.d.ts +7 -0
  12. package/dist/deserializer/index.d.ts.map +1 -0
  13. package/dist/deserializer/index.js +7 -0
  14. package/dist/deserializer/index.js.map +1 -0
  15. package/dist/deserializer/turtle-parser.d.ts +47 -0
  16. package/dist/deserializer/turtle-parser.d.ts.map +1 -0
  17. package/dist/deserializer/turtle-parser.js +840 -0
  18. package/dist/deserializer/turtle-parser.js.map +1 -0
  19. package/dist/index.d.ts +42 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +34 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/jsonld/context.d.ts +29 -0
  24. package/dist/jsonld/context.d.ts.map +1 -0
  25. package/dist/jsonld/context.js +95 -0
  26. package/dist/jsonld/context.js.map +1 -0
  27. package/dist/jsonld/converter.d.ts +50 -0
  28. package/dist/jsonld/converter.d.ts.map +1 -0
  29. package/dist/jsonld/converter.js +139 -0
  30. package/dist/jsonld/converter.js.map +1 -0
  31. package/dist/jsonld/index.d.ts +8 -0
  32. package/dist/jsonld/index.d.ts.map +1 -0
  33. package/dist/jsonld/index.js +8 -0
  34. package/dist/jsonld/index.js.map +1 -0
  35. package/dist/models/activity-snapshot.d.ts +49 -0
  36. package/dist/models/activity-snapshot.d.ts.map +1 -0
  37. package/dist/models/activity-snapshot.js +13 -0
  38. package/dist/models/activity-snapshot.js.map +1 -0
  39. package/dist/models/allergy.d.ts +49 -0
  40. package/dist/models/allergy.d.ts.map +1 -0
  41. package/dist/models/allergy.js +13 -0
  42. package/dist/models/allergy.js.map +1 -0
  43. package/dist/models/common.d.ts +174 -0
  44. package/dist/models/common.d.ts.map +1 -0
  45. package/dist/models/common.js +12 -0
  46. package/dist/models/common.js.map +1 -0
  47. package/dist/models/condition.d.ts +59 -0
  48. package/dist/models/condition.d.ts.map +1 -0
  49. package/dist/models/condition.js +13 -0
  50. package/dist/models/condition.js.map +1 -0
  51. package/dist/models/coverage.d.ts +117 -0
  52. package/dist/models/coverage.d.ts.map +1 -0
  53. package/dist/models/coverage.js +16 -0
  54. package/dist/models/coverage.js.map +1 -0
  55. package/dist/models/family-history.d.ts +38 -0
  56. package/dist/models/family-history.d.ts.map +1 -0
  57. package/dist/models/family-history.js +13 -0
  58. package/dist/models/family-history.js.map +1 -0
  59. package/dist/models/health-profile.d.ts +54 -0
  60. package/dist/models/health-profile.d.ts.map +1 -0
  61. package/dist/models/health-profile.js +11 -0
  62. package/dist/models/health-profile.js.map +1 -0
  63. package/dist/models/immunization.d.ts +78 -0
  64. package/dist/models/immunization.d.ts.map +1 -0
  65. package/dist/models/immunization.js +12 -0
  66. package/dist/models/immunization.js.map +1 -0
  67. package/dist/models/index.d.ts +20 -0
  68. package/dist/models/index.d.ts.map +1 -0
  69. package/dist/models/index.js +7 -0
  70. package/dist/models/index.js.map +1 -0
  71. package/dist/models/lab-result.d.ts +83 -0
  72. package/dist/models/lab-result.d.ts.map +1 -0
  73. package/dist/models/lab-result.js +12 -0
  74. package/dist/models/lab-result.js.map +1 -0
  75. package/dist/models/medication.d.ts +144 -0
  76. package/dist/models/medication.d.ts.map +1 -0
  77. package/dist/models/medication.js +13 -0
  78. package/dist/models/medication.js.map +1 -0
  79. package/dist/models/patient-profile.d.ts +171 -0
  80. package/dist/models/patient-profile.d.ts.map +1 -0
  81. package/dist/models/patient-profile.js +14 -0
  82. package/dist/models/patient-profile.js.map +1 -0
  83. package/dist/models/procedure.d.ts +53 -0
  84. package/dist/models/procedure.d.ts.map +1 -0
  85. package/dist/models/procedure.js +12 -0
  86. package/dist/models/procedure.js.map +1 -0
  87. package/dist/models/sleep-snapshot.d.ts +54 -0
  88. package/dist/models/sleep-snapshot.d.ts.map +1 -0
  89. package/dist/models/sleep-snapshot.js +13 -0
  90. package/dist/models/sleep-snapshot.js.map +1 -0
  91. package/dist/models/vital-sign.d.ts +74 -0
  92. package/dist/models/vital-sign.d.ts.map +1 -0
  93. package/dist/models/vital-sign.js +13 -0
  94. package/dist/models/vital-sign.js.map +1 -0
  95. package/dist/pod/index.d.ts +2 -0
  96. package/dist/pod/index.d.ts.map +1 -0
  97. package/dist/pod/index.js +2 -0
  98. package/dist/pod/index.js.map +1 -0
  99. package/dist/pod/pod-builder.d.ts +63 -0
  100. package/dist/pod/pod-builder.d.ts.map +1 -0
  101. package/dist/pod/pod-builder.js +245 -0
  102. package/dist/pod/pod-builder.js.map +1 -0
  103. package/dist/serializer/index.d.ts +8 -0
  104. package/dist/serializer/index.d.ts.map +1 -0
  105. package/dist/serializer/index.js +8 -0
  106. package/dist/serializer/index.js.map +1 -0
  107. package/dist/serializer/turtle-builder.d.ts +93 -0
  108. package/dist/serializer/turtle-builder.d.ts.map +1 -0
  109. package/dist/serializer/turtle-builder.js +204 -0
  110. package/dist/serializer/turtle-builder.js.map +1 -0
  111. package/dist/serializer/turtle-serializer.d.ts +66 -0
  112. package/dist/serializer/turtle-serializer.d.ts.map +1 -0
  113. package/dist/serializer/turtle-serializer.js +404 -0
  114. package/dist/serializer/turtle-serializer.js.map +1 -0
  115. package/dist/validator/index.d.ts +2 -0
  116. package/dist/validator/index.d.ts.map +1 -0
  117. package/dist/validator/index.js +2 -0
  118. package/dist/validator/index.js.map +1 -0
  119. package/dist/validator/validator.d.ts +16 -0
  120. package/dist/validator/validator.d.ts.map +1 -0
  121. package/dist/validator/validator.js +295 -0
  122. package/dist/validator/validator.js.map +1 -0
  123. package/dist/vocabularies/index.d.ts +8 -0
  124. package/dist/vocabularies/index.d.ts.map +1 -0
  125. package/dist/vocabularies/index.js +7 -0
  126. package/dist/vocabularies/index.js.map +1 -0
  127. package/dist/vocabularies/namespaces.d.ts +125 -0
  128. package/dist/vocabularies/namespaces.d.ts.map +1 -0
  129. package/dist/vocabularies/namespaces.js +361 -0
  130. package/dist/vocabularies/namespaces.js.map +1 -0
  131. package/package.json +48 -0
@@ -0,0 +1,840 @@
1
+ /**
2
+ * Zero-dependency Turtle parser for deserializing Cascade Protocol records.
3
+ *
4
+ * Uses regex-based parsing to convert Turtle (Terse RDF Triple Language)
5
+ * content back into typed Cascade Protocol model objects.
6
+ *
7
+ * Supports:
8
+ * - @prefix declarations
9
+ * - Subject-predicate-object triples
10
+ * - Typed literals (xsd:dateTime, xsd:date, xsd:integer, xsd:double)
11
+ * - URI references (angle-bracket and prefixed forms)
12
+ * - Boolean literals
13
+ * - RDF lists `( item1 item2 ... )`
14
+ * - Blank nodes `[ ... ]`
15
+ * - Triple-quoted long literals
16
+ * - Multi-value predicates (repeated predicate with different objects)
17
+ *
18
+ * @module deserializer
19
+ */
20
+ import { NAMESPACES, TYPE_MAPPING, buildReversePredicateMap } from '../vocabularies/namespaces.js';
21
+ // ─── Reverse Predicate Mapping ──────────────────────────────────────────────
22
+ /**
23
+ * Type-specific predicate overrides for deserialization.
24
+ * These map full predicate URIs to JSON property names for cases where
25
+ * the same local name is used in different namespaces depending on record type.
26
+ */
27
+ const ADDITIONAL_REVERSE_MAPPINGS = {
28
+ // VitalSign uses clinical: namespace for these predicates
29
+ [`${NAMESPACES.clinical}snomedCode`]: 'snomedCode',
30
+ [`${NAMESPACES.clinical}interpretation`]: 'interpretation',
31
+ };
32
+ const REVERSE_PREDICATE_MAP = buildReversePredicateMap(ADDITIONAL_REVERSE_MAPPINGS);
33
+ /**
34
+ * Build a reverse mapping from RDF type URI to record type string.
35
+ */
36
+ function buildReverseTypeMap() {
37
+ const reverseMap = new Map();
38
+ for (const [key, mapping] of Object.entries(TYPE_MAPPING)) {
39
+ const colonIdx = mapping.rdfType.indexOf(':');
40
+ if (colonIdx >= 0) {
41
+ const nsPrefix = mapping.rdfType.slice(0, colonIdx);
42
+ const localName = mapping.rdfType.slice(colonIdx + 1);
43
+ const nsUri = NAMESPACES[nsPrefix];
44
+ if (nsUri) {
45
+ reverseMap.set(`${nsUri}${localName}`, { recordType: localName, mappingKey: key });
46
+ }
47
+ }
48
+ }
49
+ return reverseMap;
50
+ }
51
+ const REVERSE_TYPE_MAP = buildReverseTypeMap();
52
+ // ─── Fields requiring special type conversion ───────────────────────────────
53
+ /** Fields that are booleans */
54
+ const BOOLEAN_FIELDS = new Set([
55
+ 'isActive', 'asNeeded',
56
+ ]);
57
+ /** Fields that are numbers (integers) */
58
+ const INTEGER_TYPE_FIELDS = new Set([
59
+ 'computedAge', 'refillsAllowed', 'supplyDurationDays', 'onsetAge',
60
+ 'steps', 'activeMinutes', 'calories', 'awakenings',
61
+ 'totalSleepMinutes', 'deepSleepMinutes', 'remSleepMinutes', 'lightSleepMinutes',
62
+ ]);
63
+ /** Fields that are numbers (possibly float) */
64
+ const NUMBER_FIELDS = new Set([
65
+ 'value', 'referenceRangeLow', 'referenceRangeHigh',
66
+ 'distance',
67
+ ]);
68
+ /** Fields that hold arrays of strings */
69
+ const ARRAY_TYPE_FIELDS = new Set([
70
+ 'drugCodes', 'affectsVitalSigns', 'monitoredVitalSigns',
71
+ ]);
72
+ // ─── Turtle Tokenizer / Parser ──────────────────────────────────────────────
73
+ /**
74
+ * Parse @prefix declarations from Turtle content.
75
+ */
76
+ function parsePrefixes(content) {
77
+ const prefixes = [];
78
+ const regex = /@prefix\s+(\w+):\s+<([^>]+)>\s*\./g;
79
+ let match;
80
+ while ((match = regex.exec(content)) !== null) {
81
+ prefixes.push({ prefix: match[1] ?? '', uri: match[2] ?? '' });
82
+ }
83
+ return prefixes;
84
+ }
85
+ /**
86
+ * Expand a prefixed name (e.g., "health:medicationName") to a full URI
87
+ * using the parsed prefix declarations.
88
+ */
89
+ function expandPrefixedName(name, prefixMap) {
90
+ // Already a full URI
91
+ if (name.startsWith('http://') || name.startsWith('https://') || name.startsWith('urn:')) {
92
+ return name;
93
+ }
94
+ const colonIdx = name.indexOf(':');
95
+ if (colonIdx < 0)
96
+ return name;
97
+ const prefix = name.slice(0, colonIdx);
98
+ const local = name.slice(colonIdx + 1);
99
+ const nsUri = prefixMap.get(prefix);
100
+ if (nsUri) {
101
+ return `${nsUri}${local}`;
102
+ }
103
+ return name;
104
+ }
105
+ /**
106
+ * Remove Turtle comments (# to end of line) while respecting
107
+ * angle-bracket URIs and quoted strings where # is a literal character.
108
+ */
109
+ function removeComments(content) {
110
+ let result = '';
111
+ let inString = false;
112
+ let inTripleQuote = false;
113
+ let inUri = false;
114
+ for (let i = 0; i < content.length; i++) {
115
+ const ch = content[i];
116
+ // Handle triple-quoted strings
117
+ if (!inString && !inTripleQuote && !inUri && content.slice(i, i + 3) === '"""') {
118
+ inTripleQuote = true;
119
+ result += '"""';
120
+ i += 2;
121
+ continue;
122
+ }
123
+ if (inTripleQuote) {
124
+ if (content.slice(i, i + 3) === '"""' && (i === 0 || content[i - 1] !== '\\')) {
125
+ inTripleQuote = false;
126
+ result += '"""';
127
+ i += 2;
128
+ continue;
129
+ }
130
+ result += ch;
131
+ continue;
132
+ }
133
+ // Handle regular strings
134
+ if (ch === '"' && !inString && !inUri) {
135
+ inString = true;
136
+ result += ch;
137
+ continue;
138
+ }
139
+ if (ch === '"' && inString && (i === 0 || content[i - 1] !== '\\')) {
140
+ inString = false;
141
+ result += ch;
142
+ continue;
143
+ }
144
+ if (inString) {
145
+ result += ch;
146
+ continue;
147
+ }
148
+ // Handle angle-bracket URIs
149
+ if (ch === '<' && !inUri) {
150
+ inUri = true;
151
+ result += ch;
152
+ continue;
153
+ }
154
+ if (ch === '>' && inUri) {
155
+ inUri = false;
156
+ result += ch;
157
+ continue;
158
+ }
159
+ if (inUri) {
160
+ result += ch;
161
+ continue;
162
+ }
163
+ // At top-level: # starts a comment to end of line
164
+ if (ch === '#') {
165
+ // Skip until newline
166
+ while (i < content.length && content[i] !== '\n') {
167
+ i++;
168
+ }
169
+ // Include the newline if present
170
+ if (i < content.length && content[i] === '\n') {
171
+ result += '\n';
172
+ }
173
+ continue;
174
+ }
175
+ result += ch;
176
+ }
177
+ return result;
178
+ }
179
+ /**
180
+ * Unescape a Turtle string literal (handle \\, \", \n, \r, \t).
181
+ */
182
+ function unescapeTurtleLiteral(value) {
183
+ return value
184
+ .replace(/\\t/g, '\t')
185
+ .replace(/\\r/g, '\r')
186
+ .replace(/\\n/g, '\n')
187
+ .replace(/\\"/g, '"')
188
+ .replace(/\\\\/g, '\\');
189
+ }
190
+ /**
191
+ * Strip surrounding angle brackets from a URI.
192
+ */
193
+ function stripAngleBrackets(uri) {
194
+ if (uri.startsWith('<') && uri.endsWith('>')) {
195
+ return uri.slice(1, -1);
196
+ }
197
+ return uri;
198
+ }
199
+ /**
200
+ * Parse Turtle content into a list of parsed triples.
201
+ *
202
+ * This is a lightweight regex-based parser that handles the subset of Turtle
203
+ * used by Cascade Protocol records. It does NOT implement a full Turtle grammar.
204
+ */
205
+ function parseTurtleContent(content) {
206
+ const prefixes = parsePrefixes(content);
207
+ const prefixMap = new Map();
208
+ for (const p of prefixes) {
209
+ prefixMap.set(p.prefix, p.uri);
210
+ }
211
+ // Add well-known prefixes as fallback
212
+ prefixMap.set('rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
213
+ prefixMap.set('xsd', NAMESPACES.xsd);
214
+ const triples = [];
215
+ // Remove prefix declarations
216
+ let body = content
217
+ .replace(/@prefix\s+\w+:\s+<[^>]+>\s*\.\s*/g, '');
218
+ // Remove comments (# to end of line) but NOT when # is inside <...> URIs or "..." strings
219
+ body = removeComments(body).trim();
220
+ // Parse subject blocks: <subject> predicate-list .
221
+ // Match subject URIs or prefixed names
222
+ const subjectRegex = /(<[^>]+>|[a-zA-Z][\w-]*:[\w-]+)\s+/;
223
+ while (body.length > 0) {
224
+ body = body.trim();
225
+ if (body.length === 0)
226
+ break;
227
+ // Match subject
228
+ const subMatch = subjectRegex.exec(body);
229
+ if (!subMatch)
230
+ break;
231
+ let subject = subMatch[1] ?? '';
232
+ if (subject.startsWith('<') && subject.endsWith('>')) {
233
+ subject = subject.slice(1, -1);
234
+ }
235
+ else {
236
+ subject = expandPrefixedName(subject, prefixMap);
237
+ }
238
+ // Find the predicate-object list (everything until the closing '.')
239
+ let startIdx = (subMatch.index ?? 0) + subMatch[0].length;
240
+ const predicateObjects = extractPredicateObjectList(body, startIdx);
241
+ if (!predicateObjects)
242
+ break;
243
+ body = body.slice(predicateObjects.endIndex).trim();
244
+ // Parse each predicate-object pair
245
+ parsePredicateObjectPairs(subject, predicateObjects.content, prefixMap, triples);
246
+ }
247
+ return { prefixes, triples };
248
+ }
249
+ /**
250
+ * Extract the predicate-object list from the current position in the body,
251
+ * handling nested blank nodes and lists.
252
+ */
253
+ function extractPredicateObjectList(body, startIdx) {
254
+ let depth = 0; // for nested [] and ()
255
+ let i = startIdx;
256
+ let inString = false;
257
+ let inTripleQuote = false;
258
+ let inUri = false; // for <...> URI delimiters
259
+ let prevChar = '';
260
+ while (i < body.length) {
261
+ const ch = body[i];
262
+ // Handle triple-quoted strings
263
+ if (!inString && !inTripleQuote && !inUri && body.slice(i, i + 3) === '"""') {
264
+ inTripleQuote = true;
265
+ i += 3;
266
+ continue;
267
+ }
268
+ if (inTripleQuote) {
269
+ if (body.slice(i, i + 3) === '"""' && prevChar !== '\\') {
270
+ inTripleQuote = false;
271
+ i += 3;
272
+ continue;
273
+ }
274
+ prevChar = ch ?? '';
275
+ i++;
276
+ continue;
277
+ }
278
+ // Handle regular quoted strings
279
+ if (ch === '"' && !inString && !inUri && prevChar !== '\\') {
280
+ inString = true;
281
+ i++;
282
+ prevChar = ch;
283
+ continue;
284
+ }
285
+ if (ch === '"' && inString && prevChar !== '\\') {
286
+ inString = false;
287
+ i++;
288
+ prevChar = ch;
289
+ continue;
290
+ }
291
+ if (inString) {
292
+ prevChar = ch ?? '';
293
+ i++;
294
+ continue;
295
+ }
296
+ // Handle angle-bracket URIs <...>
297
+ if (ch === '<' && !inUri) {
298
+ inUri = true;
299
+ i++;
300
+ prevChar = ch;
301
+ continue;
302
+ }
303
+ if (ch === '>' && inUri) {
304
+ inUri = false;
305
+ i++;
306
+ prevChar = ch;
307
+ continue;
308
+ }
309
+ if (inUri) {
310
+ prevChar = ch ?? '';
311
+ i++;
312
+ continue;
313
+ }
314
+ if (ch === '[' || ch === '(')
315
+ depth++;
316
+ if (ch === ']' || ch === ')')
317
+ depth--;
318
+ if (ch === '.' && depth === 0) {
319
+ // Check if this dot is followed by whitespace or end-of-string
320
+ // to distinguish from dots in prefixed names (e.g., "foaf:name")
321
+ const nextChar = i + 1 < body.length ? body[i + 1] : '';
322
+ if (nextChar === '' || nextChar === '\n' || nextChar === '\r' || nextChar === ' ' || nextChar === '\t') {
323
+ return { content: body.slice(startIdx, i).trim(), endIndex: i + 1 };
324
+ }
325
+ }
326
+ prevChar = ch ?? '';
327
+ i++;
328
+ }
329
+ // If we reach the end without a dot, treat the rest as the content
330
+ if (body.slice(startIdx).trim().length > 0) {
331
+ return { content: body.slice(startIdx).trim(), endIndex: body.length };
332
+ }
333
+ return null;
334
+ }
335
+ /**
336
+ * Parse semicolon-separated predicate-object pairs.
337
+ */
338
+ function parsePredicateObjectPairs(subject, content, prefixMap, triples) {
339
+ // Split on ';' that are not inside strings, brackets, or parens
340
+ const pairs = splitOnSemicolon(content);
341
+ for (const pair of pairs) {
342
+ const trimmed = pair.trim();
343
+ if (trimmed.length === 0)
344
+ continue;
345
+ // Handle "a <type>" shorthand
346
+ if (trimmed.startsWith('a ')) {
347
+ const typeValue = trimmed.slice(2).trim();
348
+ const expandedType = typeValue.startsWith('<')
349
+ ? stripAngleBrackets(typeValue)
350
+ : expandPrefixedName(typeValue, prefixMap);
351
+ triples.push({
352
+ subject,
353
+ predicate: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type',
354
+ object: expandedType,
355
+ objectType: 'uri',
356
+ });
357
+ continue;
358
+ }
359
+ // Split predicate and object(s)
360
+ const spaceIdx = findFirstWhitespace(trimmed);
361
+ if (spaceIdx < 0)
362
+ continue;
363
+ const predStr = trimmed.slice(0, spaceIdx).trim();
364
+ const objStr = trimmed.slice(spaceIdx + 1).trim();
365
+ const predUri = predStr.startsWith('<')
366
+ ? stripAngleBrackets(predStr)
367
+ : expandPrefixedName(predStr, prefixMap);
368
+ // Parse the object value
369
+ parseObjectValue(subject, predUri, objStr, prefixMap, triples);
370
+ }
371
+ }
372
+ /**
373
+ * Find the first whitespace character that is not inside brackets or strings.
374
+ */
375
+ function findFirstWhitespace(str) {
376
+ let inQuote = false;
377
+ for (let i = 0; i < str.length; i++) {
378
+ const ch = str[i];
379
+ if (ch === '"' && (i === 0 || str[i - 1] !== '\\')) {
380
+ inQuote = !inQuote;
381
+ }
382
+ if (!inQuote && (ch === ' ' || ch === '\t' || ch === '\n')) {
383
+ return i;
384
+ }
385
+ }
386
+ return -1;
387
+ }
388
+ /**
389
+ * Split a string on ';' characters that are not inside strings, brackets, or parens.
390
+ */
391
+ function splitOnSemicolon(content) {
392
+ const result = [];
393
+ let depth = 0;
394
+ let inString = false;
395
+ let inTripleQuote = false;
396
+ let inUri = false;
397
+ let current = '';
398
+ for (let i = 0; i < content.length; i++) {
399
+ const ch = content[i];
400
+ // Handle triple-quoted strings
401
+ if (!inString && !inTripleQuote && !inUri && content.slice(i, i + 3) === '"""') {
402
+ inTripleQuote = true;
403
+ current += '"""';
404
+ i += 2;
405
+ continue;
406
+ }
407
+ if (inTripleQuote) {
408
+ if (content.slice(i, i + 3) === '"""' && (i === 0 || content[i - 1] !== '\\')) {
409
+ inTripleQuote = false;
410
+ current += '"""';
411
+ i += 2;
412
+ continue;
413
+ }
414
+ current += ch;
415
+ continue;
416
+ }
417
+ if (ch === '"' && !inString && !inUri && (i === 0 || content[i - 1] !== '\\')) {
418
+ inString = true;
419
+ current += ch;
420
+ continue;
421
+ }
422
+ if (ch === '"' && inString && (i === 0 || content[i - 1] !== '\\')) {
423
+ inString = false;
424
+ current += ch;
425
+ continue;
426
+ }
427
+ if (inString) {
428
+ current += ch;
429
+ continue;
430
+ }
431
+ // Handle angle-bracket URIs <...>
432
+ if (ch === '<' && !inUri) {
433
+ inUri = true;
434
+ current += ch;
435
+ continue;
436
+ }
437
+ if (ch === '>' && inUri) {
438
+ inUri = false;
439
+ current += ch;
440
+ continue;
441
+ }
442
+ if (inUri) {
443
+ current += ch;
444
+ continue;
445
+ }
446
+ if (ch === '[' || ch === '(')
447
+ depth++;
448
+ if (ch === ']' || ch === ')')
449
+ depth--;
450
+ if (ch === ';' && depth === 0) {
451
+ result.push(current);
452
+ current = '';
453
+ continue;
454
+ }
455
+ current += ch;
456
+ }
457
+ if (current.trim().length > 0) {
458
+ result.push(current);
459
+ }
460
+ return result;
461
+ }
462
+ /**
463
+ * Parse an object value string into the appropriate type and add a triple.
464
+ */
465
+ function parseObjectValue(subject, predicate, objStr, prefixMap, triples) {
466
+ const trimmed = objStr.trim();
467
+ // Boolean literals
468
+ if (trimmed === 'true' || trimmed === 'false') {
469
+ triples.push({
470
+ subject,
471
+ predicate,
472
+ object: trimmed,
473
+ objectType: 'boolean',
474
+ });
475
+ return;
476
+ }
477
+ // URI reference (angle brackets)
478
+ if (trimmed.startsWith('<') && trimmed.endsWith('>')) {
479
+ triples.push({
480
+ subject,
481
+ predicate,
482
+ object: stripAngleBrackets(trimmed),
483
+ objectType: 'uri',
484
+ });
485
+ return;
486
+ }
487
+ // Prefixed name (e.g., cascade:ClinicalGenerated)
488
+ if (/^[a-zA-Z][\w-]*:[\w-]+$/.test(trimmed)) {
489
+ triples.push({
490
+ subject,
491
+ predicate,
492
+ object: expandPrefixedName(trimmed, prefixMap),
493
+ objectType: 'uri',
494
+ });
495
+ return;
496
+ }
497
+ // Plain integer (no quotes)
498
+ if (/^-?\d+$/.test(trimmed)) {
499
+ triples.push({
500
+ subject,
501
+ predicate,
502
+ object: trimmed,
503
+ objectType: 'integer',
504
+ });
505
+ return;
506
+ }
507
+ // Plain double (no quotes, has decimal)
508
+ if (/^-?\d+\.\d+$/.test(trimmed)) {
509
+ triples.push({
510
+ subject,
511
+ predicate,
512
+ object: trimmed,
513
+ objectType: 'double',
514
+ });
515
+ return;
516
+ }
517
+ // RDF list ( item1 item2 ... )
518
+ if (trimmed.startsWith('(') && trimmed.endsWith(')')) {
519
+ const listContent = trimmed.slice(1, -1).trim();
520
+ // Parse list items
521
+ const items = parseListItems(listContent);
522
+ triples.push({
523
+ subject,
524
+ predicate,
525
+ object: JSON.stringify(items),
526
+ objectType: 'list',
527
+ });
528
+ return;
529
+ }
530
+ // Blank node [ ... ]
531
+ if (trimmed.startsWith('[')) {
532
+ // For blank nodes, we store the inner content as the object
533
+ // and parse it recursively
534
+ const bnodeId = `_:b${Date.now()}${Math.random().toString(36).slice(2, 6)}`;
535
+ triples.push({
536
+ subject,
537
+ predicate,
538
+ object: bnodeId,
539
+ objectType: 'blankNode',
540
+ });
541
+ // Extract inner content between [ and ]
542
+ const innerStart = trimmed.indexOf('[') + 1;
543
+ const innerEnd = trimmed.lastIndexOf(']');
544
+ if (innerEnd > innerStart) {
545
+ const inner = trimmed.slice(innerStart, innerEnd).trim();
546
+ parsePredicateObjectPairs(bnodeId, inner, prefixMap, triples);
547
+ }
548
+ return;
549
+ }
550
+ // Triple-quoted string literal """..."""
551
+ if (trimmed.startsWith('"""')) {
552
+ const endIdx = trimmed.indexOf('"""', 3);
553
+ if (endIdx >= 0) {
554
+ const value = trimmed.slice(3, endIdx);
555
+ const afterQuote = trimmed.slice(endIdx + 3).trim();
556
+ let datatype;
557
+ if (afterQuote.startsWith('^^')) {
558
+ const dtStr = afterQuote.slice(2);
559
+ datatype = dtStr.startsWith('<')
560
+ ? stripAngleBrackets(dtStr)
561
+ : expandPrefixedName(dtStr, prefixMap);
562
+ }
563
+ triples.push({
564
+ subject,
565
+ predicate,
566
+ object: unescapeTurtleLiteral(value),
567
+ objectType: 'literal',
568
+ datatype,
569
+ });
570
+ }
571
+ return;
572
+ }
573
+ // Quoted string literal "..."
574
+ if (trimmed.startsWith('"')) {
575
+ // Find matching closing quote (not escaped)
576
+ let endQuoteIdx = -1;
577
+ for (let i = 1; i < trimmed.length; i++) {
578
+ if (trimmed[i] === '"' && trimmed[i - 1] !== '\\') {
579
+ endQuoteIdx = i;
580
+ break;
581
+ }
582
+ }
583
+ if (endQuoteIdx >= 0) {
584
+ const value = trimmed.slice(1, endQuoteIdx);
585
+ const afterQuote = trimmed.slice(endQuoteIdx + 1).trim();
586
+ let datatype;
587
+ if (afterQuote.startsWith('^^')) {
588
+ const dtStr = afterQuote.slice(2);
589
+ datatype = dtStr.startsWith('<')
590
+ ? stripAngleBrackets(dtStr)
591
+ : expandPrefixedName(dtStr, prefixMap);
592
+ }
593
+ triples.push({
594
+ subject,
595
+ predicate,
596
+ object: unescapeTurtleLiteral(value),
597
+ objectType: 'literal',
598
+ datatype,
599
+ });
600
+ }
601
+ return;
602
+ }
603
+ // Fallback: treat as literal
604
+ triples.push({
605
+ subject,
606
+ predicate,
607
+ object: trimmed,
608
+ objectType: 'literal',
609
+ });
610
+ }
611
+ /**
612
+ * Parse items from an RDF list, handling quoted strings.
613
+ */
614
+ function parseListItems(content) {
615
+ const items = [];
616
+ let remaining = content.trim();
617
+ while (remaining.length > 0) {
618
+ remaining = remaining.trim();
619
+ if (remaining.length === 0)
620
+ break;
621
+ if (remaining.startsWith('"')) {
622
+ // Find closing quote
623
+ let endIdx = -1;
624
+ for (let i = 1; i < remaining.length; i++) {
625
+ if (remaining[i] === '"' && remaining[i - 1] !== '\\') {
626
+ endIdx = i;
627
+ break;
628
+ }
629
+ }
630
+ if (endIdx >= 0) {
631
+ items.push(unescapeTurtleLiteral(remaining.slice(1, endIdx)));
632
+ remaining = remaining.slice(endIdx + 1).trim();
633
+ // Skip optional datatype
634
+ if (remaining.startsWith('^^')) {
635
+ const spaceIdx = remaining.indexOf(' ');
636
+ remaining = spaceIdx >= 0 ? remaining.slice(spaceIdx) : '';
637
+ }
638
+ }
639
+ else {
640
+ break;
641
+ }
642
+ }
643
+ else if (remaining.startsWith('<')) {
644
+ // URI
645
+ const endIdx = remaining.indexOf('>');
646
+ if (endIdx >= 0) {
647
+ items.push(remaining.slice(1, endIdx));
648
+ remaining = remaining.slice(endIdx + 1).trim();
649
+ }
650
+ else {
651
+ break;
652
+ }
653
+ }
654
+ else {
655
+ // Unquoted token
656
+ const spaceIdx = remaining.indexOf(' ');
657
+ if (spaceIdx >= 0) {
658
+ items.push(remaining.slice(0, spaceIdx));
659
+ remaining = remaining.slice(spaceIdx + 1);
660
+ }
661
+ else {
662
+ items.push(remaining);
663
+ remaining = '';
664
+ }
665
+ }
666
+ }
667
+ return items;
668
+ }
669
+ // ─── Public API ─────────────────────────────────────────────────────────────
670
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
671
+ /**
672
+ * Resolve a Cascade record type string (e.g., "MedicationRecord") to the full
673
+ * RDF type URI used in Turtle.
674
+ */
675
+ function resolveTypeUri(type) {
676
+ // Try direct match in TYPE_MAPPING values
677
+ for (const mapping of Object.values(TYPE_MAPPING)) {
678
+ const colonIdx = mapping.rdfType.indexOf(':');
679
+ if (colonIdx >= 0) {
680
+ const nsPrefix = mapping.rdfType.slice(0, colonIdx);
681
+ const localName = mapping.rdfType.slice(colonIdx + 1);
682
+ if (localName === type) {
683
+ const nsUri = NAMESPACES[nsPrefix];
684
+ if (nsUri)
685
+ return `${nsUri}${localName}`;
686
+ }
687
+ }
688
+ }
689
+ return null;
690
+ }
691
+ /**
692
+ * Convert parsed triples for a single subject into a typed record object.
693
+ */
694
+ function triplesToRecord(subjectUri, triples, recordType) {
695
+ const record = {
696
+ id: subjectUri,
697
+ type: recordType,
698
+ };
699
+ // Group triples by predicate for multi-value handling
700
+ const triplesByPredicate = new Map();
701
+ for (const triple of triples) {
702
+ if (triple.subject !== subjectUri)
703
+ continue;
704
+ if (triple.predicate === RDF_TYPE)
705
+ continue;
706
+ const existing = triplesByPredicate.get(triple.predicate);
707
+ if (existing) {
708
+ existing.push(triple);
709
+ }
710
+ else {
711
+ triplesByPredicate.set(triple.predicate, [triple]);
712
+ }
713
+ }
714
+ for (const [predUri, predTriples] of triplesByPredicate) {
715
+ const jsonKey = REVERSE_PREDICATE_MAP.get(predUri);
716
+ if (!jsonKey)
717
+ continue;
718
+ // Handle array fields
719
+ if (ARRAY_TYPE_FIELDS.has(jsonKey)) {
720
+ const values = [];
721
+ for (const t of predTriples) {
722
+ if (t.objectType === 'list') {
723
+ try {
724
+ const parsed = JSON.parse(t.object);
725
+ values.push(...parsed);
726
+ }
727
+ catch {
728
+ values.push(t.object);
729
+ }
730
+ }
731
+ else {
732
+ values.push(t.object);
733
+ }
734
+ }
735
+ record[jsonKey] = values;
736
+ continue;
737
+ }
738
+ // Single-value fields use the first triple
739
+ const triple = predTriples[0];
740
+ if (!triple)
741
+ continue;
742
+ // dataProvenance: extract local name from cascade namespace
743
+ if (jsonKey === 'dataProvenance') {
744
+ const cascadeNs = NAMESPACES.cascade;
745
+ if (triple.object.startsWith(cascadeNs)) {
746
+ record[jsonKey] = triple.object.slice(cascadeNs.length);
747
+ }
748
+ else {
749
+ record[jsonKey] = triple.object;
750
+ }
751
+ continue;
752
+ }
753
+ // Boolean fields
754
+ if (triple.objectType === 'boolean' || BOOLEAN_FIELDS.has(jsonKey)) {
755
+ record[jsonKey] = triple.object === 'true';
756
+ continue;
757
+ }
758
+ // Integer fields
759
+ if (INTEGER_TYPE_FIELDS.has(jsonKey) || triple.objectType === 'integer' ||
760
+ triple.datatype === NAMESPACES.xsd + 'integer') {
761
+ record[jsonKey] = parseInt(triple.object, 10);
762
+ continue;
763
+ }
764
+ // Number fields (plain numeric)
765
+ if (NUMBER_FIELDS.has(jsonKey)) {
766
+ const num = parseFloat(triple.object);
767
+ if (!isNaN(num)) {
768
+ record[jsonKey] = num;
769
+ }
770
+ else {
771
+ record[jsonKey] = triple.object;
772
+ }
773
+ continue;
774
+ }
775
+ // Double/decimal fields
776
+ if (triple.objectType === 'double' ||
777
+ triple.datatype === NAMESPACES.xsd + 'double' ||
778
+ triple.datatype === NAMESPACES.xsd + 'decimal') {
779
+ record[jsonKey] = parseFloat(triple.object);
780
+ continue;
781
+ }
782
+ // URI fields: keep the full URI string
783
+ if (triple.objectType === 'uri') {
784
+ record[jsonKey] = triple.object;
785
+ continue;
786
+ }
787
+ // Default: string literal
788
+ record[jsonKey] = triple.object;
789
+ }
790
+ return record;
791
+ }
792
+ /**
793
+ * Parse Turtle content and return typed records matching the specified type.
794
+ *
795
+ * @param turtle - Turtle document content
796
+ * @param type - Record type string (e.g., "MedicationRecord", "VitalSign", "PatientProfile")
797
+ * @returns Array of parsed records of the specified type
798
+ *
799
+ * @example
800
+ * ```typescript
801
+ * import { deserialize } from '@the-cascade-protocol/sdk';
802
+ * import type { Medication } from '@the-cascade-protocol/sdk';
803
+ *
804
+ * const meds = deserialize<Medication>(turtleString, 'MedicationRecord');
805
+ * ```
806
+ */
807
+ export function deserialize(turtle, type) {
808
+ const { triples } = parseTurtleContent(turtle);
809
+ // Resolve the requested type to a full URI
810
+ const typeUri = resolveTypeUri(type);
811
+ if (!typeUri) {
812
+ throw new Error(`Unknown record type: ${type}. Cannot resolve to RDF type URI.`);
813
+ }
814
+ // Find all subjects with matching rdf:type
815
+ const matchingSubjects = [];
816
+ for (const triple of triples) {
817
+ if (triple.predicate === RDF_TYPE && triple.object === typeUri) {
818
+ matchingSubjects.push(triple.subject);
819
+ }
820
+ }
821
+ // Look up the record type name from REVERSE_TYPE_MAP
822
+ const typeInfo = REVERSE_TYPE_MAP.get(typeUri);
823
+ const recordType = typeInfo?.recordType ?? type;
824
+ // Convert each subject to a record
825
+ return matchingSubjects.map((subjectUri) => triplesToRecord(subjectUri, triples, recordType));
826
+ }
827
+ /**
828
+ * Parse a single record from Turtle content.
829
+ *
830
+ * Returns the first record matching the specified type, or `null` if none found.
831
+ *
832
+ * @param turtle - Turtle document content
833
+ * @param type - Record type string
834
+ * @returns The parsed record, or null
835
+ */
836
+ export function deserializeOne(turtle, type) {
837
+ const results = deserialize(turtle, type);
838
+ return results[0] ?? null;
839
+ }
840
+ //# sourceMappingURL=turtle-parser.js.map