@blackwell-systems/gcf 0.6.0 → 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 (47) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +21 -26
  3. package/dist/decode.d.ts.map +1 -1
  4. package/dist/decode.js +15 -6
  5. package/dist/decode.js.map +1 -1
  6. package/dist/decode_generic.d.ts +1 -5
  7. package/dist/decode_generic.d.ts.map +1 -1
  8. package/dist/decode_generic.js +392 -142
  9. package/dist/decode_generic.js.map +1 -1
  10. package/dist/delta.js +1 -1
  11. package/dist/delta.js.map +1 -1
  12. package/dist/encode.d.ts.map +1 -1
  13. package/dist/encode.js +22 -7
  14. package/dist/encode.js.map +1 -1
  15. package/dist/generic.d.ts +0 -6
  16. package/dist/generic.d.ts.map +1 -1
  17. package/dist/generic.js +146 -114
  18. package/dist/generic.js.map +1 -1
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/scalar.d.ts +27 -0
  24. package/dist/scalar.d.ts.map +1 -0
  25. package/dist/scalar.js +315 -0
  26. package/dist/scalar.js.map +1 -0
  27. package/dist/session.d.ts.map +1 -1
  28. package/dist/session.js +9 -1
  29. package/dist/session.js.map +1 -1
  30. package/dist/stream.d.ts +1 -1
  31. package/dist/stream.js +7 -7
  32. package/dist/stream.js.map +1 -1
  33. package/dist/stream_generic.d.ts +1 -1
  34. package/dist/stream_generic.d.ts.map +1 -1
  35. package/dist/stream_generic.js +5 -27
  36. package/dist/stream_generic.js.map +1 -1
  37. package/package.json +2 -2
  38. package/src/decode.ts +18 -6
  39. package/src/decode_generic.ts +352 -137
  40. package/src/delta.ts +1 -1
  41. package/src/encode.ts +19 -7
  42. package/src/generic.ts +127 -128
  43. package/src/index.ts +1 -0
  44. package/src/scalar.ts +249 -0
  45. package/src/session.ts +6 -1
  46. package/src/stream.ts +7 -7
  47. package/src/stream_generic.ts +5 -27
@@ -1,18 +1,18 @@
1
1
  import { decode } from './decode.js';
2
+ import { parseScalar, parseQuotedString, splitRespectingQuotes, splitFieldDecl, MISSING, ATTACHMENT, } from './scalar.js';
2
3
  /**
3
- * Decode any GCF text (tabular or graph profile) back into a JS value.
4
- * Returns objects, arrays, and primitives matching the original structure.
5
- *
6
- * If the input starts with "GCF " (graph profile), falls back to decode()
7
- * and returns the Payload as a plain object.
4
+ * Decode GCF v2.0 generic or graph profile text into a JS value.
8
5
  */
9
6
  export function decodeGeneric(input) {
10
7
  input = input.trimEnd();
11
8
  if (!input)
12
- return null;
9
+ throw new Error('missing_header: empty input');
13
10
  const lines = input.split('\n');
14
- // Graph profile fallback.
15
- if (lines[0].startsWith('GCF ')) {
11
+ const header = lines[0].replace(/\r$/, '');
12
+ if (!header.startsWith('GCF '))
13
+ throw new Error('missing_header: first line does not begin with GCF');
14
+ const profile = parseHeaderProfile(header);
15
+ if (profile === 'graph') {
16
16
  const p = decode(input);
17
17
  return {
18
18
  tool: p.tool,
@@ -30,99 +30,139 @@ export function decodeGeneric(input) {
30
30
  source: e.source,
31
31
  target: e.target,
32
32
  edgeType: e.edgeType,
33
- ...(e.status ? { status: e.status } : {}),
33
+ status: e.status ?? '',
34
34
  })),
35
35
  };
36
36
  }
37
+ if (profile !== 'generic')
38
+ throw new Error(`unknown_profile: ${profile}`);
39
+ // Filter body.
40
+ const contentLines = [];
41
+ let summaryLine = '';
42
+ let deferredSectionCount = 0;
43
+ for (let i = 1; i < lines.length; i++) {
44
+ const l = lines[i].replace(/\r$/, '');
45
+ if (l === '')
46
+ continue;
47
+ // Tab check.
48
+ for (let j = 0; j < l.length; j++) {
49
+ if (l[j] === '\t')
50
+ throw new Error('tab_indentation: tabs in leading whitespace');
51
+ if (l[j] !== ' ')
52
+ break;
53
+ }
54
+ const trimmed = l.trimStart();
55
+ if (trimmed.startsWith('# '))
56
+ continue;
57
+ if (trimmed.startsWith('##! ')) {
58
+ summaryLine = trimmed;
59
+ continue;
60
+ }
61
+ if (trimmed.startsWith('## ') && trimmed.includes('[?]'))
62
+ deferredSectionCount++;
63
+ contentLines.push(l);
64
+ }
65
+ // Validate ##! summary counts.
66
+ if (summaryLine && deferredSectionCount > 0) {
67
+ validateSummaryCounts(summaryLine, deferredSectionCount, contentLines);
68
+ }
69
+ if (contentLines.length === 0)
70
+ return {};
71
+ const first = contentLines[0].trimStart();
72
+ // Root scalar.
73
+ if (first.startsWith('=')) {
74
+ if (contentLines.length > 1)
75
+ throw new Error('trailing_characters: extra lines after root scalar');
76
+ return parseScalar(first.slice(1), false);
77
+ }
78
+ // Root array.
79
+ if (first.startsWith('## [')) {
80
+ const [arr] = parseArrayFromHeader(contentLines, 0, 0, first.slice(3));
81
+ return arr;
82
+ }
83
+ // Root object.
37
84
  const result = {};
38
- parseObject(lines, 0, 0, result);
85
+ parseObjectBody(contentLines, 0, 0, result);
39
86
  return result;
40
87
  }
41
- function parseObject(lines, start, depth, out) {
42
- const indent = ' '.repeat(depth);
88
+ function parseHeaderProfile(header) {
89
+ const parts = header.split(/\s+/);
90
+ if (parts.length < 2)
91
+ throw new Error('missing_profile');
92
+ const seen = new Set();
93
+ let profile = '';
94
+ for (let i = 1; i < parts.length; i++) {
95
+ const eq = parts[i].indexOf('=');
96
+ if (eq < 0)
97
+ throw new Error(`malformed_header_field: ${parts[i]}`);
98
+ const key = parts[i].slice(0, eq);
99
+ if (seen.has(key))
100
+ throw new Error(`duplicate_header_field: ${key}`);
101
+ seen.add(key);
102
+ if (key === 'profile')
103
+ profile = parts[i].slice(eq + 1);
104
+ }
105
+ if (!profile)
106
+ throw new Error('missing_profile');
107
+ return profile;
108
+ }
109
+ function parseObjectBody(lines, start, depth, out) {
110
+ const ind = ' '.repeat(depth);
43
111
  let i = start;
44
112
  while (i < lines.length) {
45
- const raw = lines[i].replace(/\r$/, '');
46
- if (raw === '' || raw.startsWith('# ')) {
47
- i++;
48
- continue;
49
- }
50
- if (depth > 0 && !raw.startsWith(indent))
113
+ const line = lines[i];
114
+ if (depth > 0 && !line.startsWith(ind))
51
115
  break;
52
- const content = depth > 0 ? raw.slice(indent.length) : raw;
53
- // Skip _summary.
54
- if (content.startsWith('## _summary')) {
55
- i++;
56
- continue;
116
+ const content = depth > 0 ? line.slice(ind.length) : line;
117
+ if (content.length > 0 && content[0] === ' ') {
118
+ throw new Error('invalid_indent: indentation increases by more than one level');
57
119
  }
58
- // Tabular array or section header.
120
+ // Array section.
59
121
  if (content.startsWith('## ')) {
60
- const header = content.slice(3);
61
- const bracketIdx = header.indexOf(' [');
62
- if (bracketIdx >= 0) {
63
- const name = header.slice(0, bracketIdx);
64
- const rest = header.slice(bracketIdx + 2);
65
- const closeBracket = rest.indexOf(']');
66
- if (closeBracket >= 0) {
67
- const afterBracket = rest.slice(closeBracket + 1);
68
- if (afterBracket.startsWith('{')) {
69
- // Tabular with fields.
70
- const fieldEnd = afterBracket.indexOf('}');
71
- if (fieldEnd >= 0) {
72
- const fields = afterBracket.slice(1, fieldEnd).split(',');
73
- i++;
74
- const [rows, consumed] = parseTabularRows(lines, i, depth, fields);
75
- out[name] = rows;
76
- i += consumed;
77
- continue;
78
- }
79
- }
80
- else {
81
- const countStr = rest.slice(0, closeBracket);
82
- if (countStr === '0') {
83
- out[name] = [];
84
- i++;
85
- continue;
86
- }
87
- // Non-uniform array.
88
- i++;
89
- const [items, consumed] = parseNonUniformArray(lines, i, depth);
90
- out[name] = items;
91
- i += consumed;
92
- continue;
93
- }
94
- }
122
+ const hdr = content.slice(3);
123
+ const bi = hdr.indexOf(' [');
124
+ if (bi >= 0) {
125
+ const name = parseKeyFromHeader(hdr.slice(0, bi));
126
+ checkDup(out, name);
127
+ const [arr, consumed] = parseArrayFromHeader(lines, i, depth, hdr.slice(bi));
128
+ out[name] = arr;
129
+ i += consumed;
130
+ continue;
95
131
  }
96
- // Plain section header.
97
- let name = header;
98
- const bi = name.indexOf(' [');
99
- if (bi >= 0)
100
- name = name.slice(0, bi);
132
+ const name = parseKeyFromHeader(hdr);
133
+ checkDup(out, name);
101
134
  i++;
102
135
  const nested = {};
103
- const consumed = parseObject(lines, i, depth + 1, nested);
136
+ const consumed = parseObjectBody(lines, i, depth + 1, nested);
104
137
  out[name] = nested;
105
138
  i += consumed;
106
139
  continue;
107
140
  }
108
- // Inline primitive array: name[N]: val1,val2,...
109
- const bracketIdx = content.indexOf('[');
110
- if (bracketIdx > 0) {
111
- const colonIdx = content.indexOf(']: ');
112
- if (colonIdx > bracketIdx) {
113
- const name = content.slice(0, bracketIdx);
114
- const valsStr = content.slice(colonIdx + 3);
115
- out[name] = valsStr.split(',').map(v => parseValue(v.trim()));
116
- i++;
117
- continue;
141
+ // Inline array.
142
+ if (!content.startsWith('@') && !content.startsWith('##')) {
143
+ const bracketIdx = content.indexOf('[');
144
+ if (bracketIdx > 0) {
145
+ const rest = content.slice(bracketIdx);
146
+ const closeIdx = rest.indexOf(']');
147
+ if (closeIdx >= 0) {
148
+ const after = rest.slice(closeIdx + 1);
149
+ if (after.startsWith(': ') || after === ':') {
150
+ const name = parseKeyFromHeader(content.slice(0, bracketIdx));
151
+ checkDup(out, name);
152
+ const [arr] = parseArrayFromHeader(lines, i, depth, rest);
153
+ out[name] = arr;
154
+ i++;
155
+ continue;
156
+ }
157
+ }
118
158
  }
119
159
  }
120
160
  // Key=value.
121
- const eqIdx = content.indexOf('=');
161
+ const eqIdx = findKeyValueSplit(content);
122
162
  if (eqIdx > 0) {
123
- const key = content.slice(0, eqIdx);
124
- const val = content.slice(eqIdx + 1);
125
- out[key] = parseValue(val);
163
+ const name = parseKeyFromHeader(content.slice(0, eqIdx));
164
+ checkDup(out, name);
165
+ out[name] = parseScalar(content.slice(eqIdx + 1), false);
126
166
  i++;
127
167
  continue;
128
168
  }
@@ -130,107 +170,317 @@ function parseObject(lines, start, depth, out) {
130
170
  }
131
171
  return i - start;
132
172
  }
133
- function parseTabularRows(lines, start, depth, fields) {
134
- const indent = ' '.repeat(depth);
173
+ function findKeyValueSplit(s) {
174
+ if (!s.length)
175
+ return -1;
176
+ if (s[0] === '"') {
177
+ for (let i = 1; i < s.length; i++) {
178
+ if (s[i] === '\\') {
179
+ i++;
180
+ continue;
181
+ }
182
+ if (s[i] === '"')
183
+ return (i + 1 < s.length && s[i + 1] === '=') ? i + 1 : -1;
184
+ }
185
+ return -1;
186
+ }
187
+ return s.indexOf('=');
188
+ }
189
+ function parseKeyFromHeader(s) {
190
+ s = s.trim();
191
+ if (s.length >= 2 && s[0] === '"')
192
+ return parseQuotedString(s);
193
+ return s;
194
+ }
195
+ function checkDup(obj, key) {
196
+ if (key in obj)
197
+ throw new Error(`duplicate_key: ${key}`);
198
+ }
199
+ function parseArrayFromHeader(lines, headerLine, depth, bracketPart) {
200
+ const bp = bracketPart.trimStart();
201
+ if (!bp.startsWith('['))
202
+ throw new Error('invalid_count');
203
+ const closeIdx = bp.indexOf(']');
204
+ if (closeIdx < 0)
205
+ throw new Error('invalid_count');
206
+ const countStr = bp.slice(1, closeIdx);
207
+ const afterBracket = bp.slice(closeIdx + 1);
208
+ let count = -1;
209
+ if (countStr !== '?')
210
+ count = parseCount(countStr);
211
+ if (count === 0 && !afterBracket.startsWith('{') && !afterBracket.startsWith(':')) {
212
+ return [[], 1];
213
+ }
214
+ // Inline.
215
+ if (afterBracket.startsWith(': ') || afterBracket === ':') {
216
+ const valsStr = afterBracket.startsWith(': ') ? afterBracket.slice(2) : '';
217
+ if (!valsStr) {
218
+ if (count >= 0 && count !== 0)
219
+ throw new Error(`count_mismatch: declared ${count}, got 0`);
220
+ return [[], 1];
221
+ }
222
+ const vals = splitRespectingQuotes(valsStr, ',');
223
+ if (count >= 0 && vals.length !== count)
224
+ throw new Error(`count_mismatch: declared ${count}, got ${vals.length}`);
225
+ return [vals.map(v => parseScalar(v.trim(), false)), 1];
226
+ }
227
+ // Tabular.
228
+ if (afterBracket.startsWith('{')) {
229
+ const braceEnd = findClosingBrace(afterBracket);
230
+ if (braceEnd < 0)
231
+ throw new Error('invalid field declaration');
232
+ const fields = splitFieldDecl(afterBracket.slice(0, braceEnd + 1));
233
+ const [rows, consumed] = parseTabularBody(lines, headerLine + 1, depth, fields, count);
234
+ if (count >= 0 && rows.length !== count)
235
+ throw new Error(`count_mismatch: declared ${count}, got ${rows.length}`);
236
+ return [rows, consumed + 1];
237
+ }
238
+ // Expanded.
239
+ const [items, consumed] = parseExpandedBody(lines, headerLine + 1, depth);
240
+ if (count >= 0 && items.length !== count)
241
+ throw new Error(`count_mismatch: declared ${count}, got ${items.length}`);
242
+ return [items, consumed + 1];
243
+ }
244
+ function findClosingBrace(s) {
245
+ let inQuote = false, escaped = false;
246
+ for (let i = 0; i < s.length; i++) {
247
+ if (escaped) {
248
+ escaped = false;
249
+ continue;
250
+ }
251
+ if (s[i] === '\\' && inQuote) {
252
+ escaped = true;
253
+ continue;
254
+ }
255
+ if (s[i] === '"') {
256
+ inQuote = !inQuote;
257
+ continue;
258
+ }
259
+ if (s[i] === '}' && !inQuote)
260
+ return i;
261
+ }
262
+ return -1;
263
+ }
264
+ function parseTabularBody(lines, start, depth, fields, expectedCount) {
265
+ const ind = ' '.repeat(depth);
135
266
  const rows = [];
136
267
  let i = start;
137
268
  while (i < lines.length) {
138
- const raw = lines[i].replace(/\r$/, '');
139
- if (raw === '') {
140
- i++;
141
- continue;
142
- }
143
- const content = depth > 0 ? (raw.startsWith(indent) ? raw.slice(indent.length) : null) : raw;
269
+ const line = lines[i];
270
+ const content = depth > 0 ? (line.startsWith(ind) ? line.slice(ind.length) : null) : line;
144
271
  if (content === null)
145
272
  break;
146
- if (content.startsWith('## '))
273
+ if (content.startsWith('## ') || content.startsWith('##!'))
274
+ break;
275
+ if (content.length > 0 && content[0] === ' ') {
276
+ const trimmed = content.trimStart();
277
+ if (trimmed.startsWith('.'))
278
+ throw new Error(`orphan_attachment: ${trimmed}`);
147
279
  break;
148
- if (content.startsWith('# ')) {
149
- i++;
150
- continue;
151
280
  }
152
281
  let rowData = content;
153
- let hasNested = false;
282
+ let rowHasID = false;
154
283
  if (rowData.startsWith('@')) {
155
284
  const sp = rowData.indexOf(' ');
156
285
  if (sp > 0) {
157
286
  rowData = rowData.slice(sp + 1);
158
- hasNested = true;
287
+ rowHasID = true;
159
288
  }
160
289
  }
161
- const vals = rowData.split('|');
290
+ const vals = splitRespectingQuotes(rowData, '|');
291
+ if (vals.length !== fields.length)
292
+ throw new Error(`row_width_mismatch: expected ${fields.length}, got ${vals.length}`);
162
293
  const row = {};
294
+ const attachmentFields = [];
163
295
  for (let j = 0; j < fields.length; j++) {
164
- row[fields[j]] = j < vals.length ? parseValue(vals[j]) : null;
296
+ const parsed = parseScalar(vals[j], true);
297
+ if (parsed === MISSING)
298
+ continue;
299
+ if (parsed === ATTACHMENT) {
300
+ attachmentFields.push(fields[j]);
301
+ continue;
302
+ }
303
+ row[fields[j]] = parsed;
165
304
  }
166
305
  i++;
167
- if (hasNested) {
168
- const nestedIndent = indent + ' ';
306
+ if (rowHasID && attachmentFields.length > 0) {
307
+ const attIndent = ind + ' ';
308
+ const resolved = new Set();
169
309
  while (i < lines.length) {
170
- const nl = lines[i].replace(/\r$/, '');
171
- if (!nl.startsWith(nestedIndent))
310
+ const al = lines[i];
311
+ if (!al.startsWith(attIndent))
172
312
  break;
173
- const nc = nl.slice(nestedIndent.length);
174
- if (nc.startsWith('.')) {
175
- const fieldName = nc.slice(1);
176
- i++;
177
- const nested = {};
178
- const consumed = parseObject(lines, i, depth + 2, nested);
179
- row[fieldName] = nested;
180
- i += consumed;
181
- }
182
- else {
313
+ const ac = al.slice(attIndent.length);
314
+ if (!ac.startsWith('.'))
183
315
  break;
184
- }
316
+ const [name, val, consumed] = parseAttachment(lines, i, ac.slice(1), depth + 2);
317
+ if (resolved.has(name))
318
+ throw new Error(`duplicate_attachment: ${name}`);
319
+ resolved.add(name);
320
+ row[name] = val;
321
+ i += consumed;
322
+ }
323
+ for (const f of attachmentFields) {
324
+ if (!resolved.has(f))
325
+ throw new Error(`missing_attachment: ${f}`);
326
+ }
327
+ }
328
+ if (!rowHasID || attachmentFields.length === 0) {
329
+ const attIndent = ind + ' ';
330
+ if (i < lines.length && lines[i].startsWith(attIndent)) {
331
+ const peek = lines[i].slice(attIndent.length);
332
+ if (peek.startsWith('.'))
333
+ throw new Error(`orphan_attachment: ${peek}`);
185
334
  }
186
335
  }
187
336
  rows.push(row);
337
+ if (expectedCount >= 0 && rows.length >= expectedCount)
338
+ break;
188
339
  }
189
340
  return [rows, i - start];
190
341
  }
191
- function parseNonUniformArray(lines, start, depth) {
192
- const indent = ' '.repeat(depth);
342
+ function parseAttachment(lines, lineIdx, rest, depth) {
343
+ let name;
344
+ let afterName;
345
+ if (rest[0] === '"') {
346
+ let closeIdx = -1;
347
+ for (let j = 1; j < rest.length; j++) {
348
+ if (rest[j] === '\\') {
349
+ j++;
350
+ continue;
351
+ }
352
+ if (rest[j] === '"') {
353
+ closeIdx = j;
354
+ break;
355
+ }
356
+ }
357
+ if (closeIdx < 0)
358
+ throw new Error('unterminated_quote');
359
+ name = parseQuotedString(rest.slice(0, closeIdx + 1));
360
+ afterName = rest.slice(closeIdx + 1).trimStart();
361
+ }
362
+ else {
363
+ const sp = rest.indexOf(' ');
364
+ if (sp < 0)
365
+ throw new Error(`invalid attachment: ${rest}`);
366
+ name = rest.slice(0, sp);
367
+ afterName = rest.slice(sp).trimStart();
368
+ }
369
+ if (afterName.startsWith('{}')) {
370
+ const nested = {};
371
+ const consumed = parseObjectBody(lines, lineIdx + 1, depth, nested);
372
+ return [name, nested, consumed + 1];
373
+ }
374
+ if (afterName.startsWith('[')) {
375
+ const [arr, consumed] = parseArrayFromHeader(lines, lineIdx, depth, afterName);
376
+ return [name, arr, consumed];
377
+ }
378
+ throw new Error(`invalid attachment form: ${afterName}`);
379
+ }
380
+ function parseExpandedBody(lines, start, depth) {
381
+ const ind = ' '.repeat(depth);
193
382
  const items = [];
194
383
  let i = start;
195
384
  while (i < lines.length) {
196
- const raw = lines[i].replace(/\r$/, '');
197
- if (raw === '') {
198
- i++;
199
- continue;
200
- }
201
- const content = depth > 0 ? (raw.startsWith(indent) ? raw.slice(indent.length) : null) : raw;
385
+ const line = lines[i];
386
+ const content = depth > 0 ? (line.startsWith(ind) ? line.slice(ind.length) : null) : line;
202
387
  if (content === null)
203
388
  break;
204
- if (content.startsWith('## '))
389
+ if (content.startsWith('## ') || content.startsWith('##!'))
205
390
  break;
206
- if (content.startsWith('@')) {
207
- const sp = content.indexOf(' ');
208
- if (sp > 0) {
209
- items.push(parseValue(content.slice(sp + 1)));
210
- }
391
+ if (!content.startsWith('@'))
392
+ break;
393
+ const sp = content.indexOf(' ');
394
+ if (sp < 0)
395
+ break;
396
+ const idStr = content.slice(1, sp);
397
+ const id = parseInt(idStr, 10);
398
+ if (!isNaN(id) && id !== items.length) {
399
+ throw new Error(`invalid_item_id: expected @${items.length}, got @${idStr}`);
400
+ }
401
+ const marker = content.slice(sp + 1);
402
+ if (marker.startsWith('=')) {
403
+ items.push(parseScalar(marker.slice(1), false));
211
404
  i++;
405
+ continue;
212
406
  }
213
- else {
214
- break;
407
+ if (marker.startsWith('{}')) {
408
+ const nested = {};
409
+ i++;
410
+ const consumed = parseObjectBody(lines, i, depth + 1, nested);
411
+ items.push(nested);
412
+ i += consumed;
413
+ continue;
414
+ }
415
+ if (marker.startsWith('[')) {
416
+ const [arr, consumed] = parseArrayFromHeader(lines, i, depth + 1, marker);
417
+ items.push(arr);
418
+ i += consumed;
419
+ continue;
215
420
  }
421
+ break;
216
422
  }
217
423
  return [items, i - start];
218
424
  }
219
- function parseValue(s) {
220
- if (s === '-')
221
- return null;
222
- if (s === 'true')
223
- return true;
224
- if (s === 'false')
225
- return false;
226
- if (s === '""')
227
- return '';
228
- if (s.length >= 2 && s[0] === '"' && s[s.length - 1] === '"') {
229
- return s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
230
- }
231
- const n = Number(s);
232
- if (!isNaN(n) && s !== '')
233
- return n;
234
- return s;
425
+ function parseCount(s) {
426
+ if (s === '0')
427
+ return 0;
428
+ if (!s.length || s[0] === '0')
429
+ throw new Error(`invalid_count: ${s}`);
430
+ const n = parseInt(s, 10);
431
+ if (isNaN(n) || String(n) !== s)
432
+ throw new Error(`invalid_count: ${s}`);
433
+ return n;
434
+ }
435
+ function validateSummaryCounts(summaryLine, deferredCount, contentLines) {
436
+ // Parse counts from "##! summary counts=N,M,..."
437
+ const parts = summaryLine.split(/\s+/);
438
+ let countsStr = '';
439
+ for (const p of parts) {
440
+ if (p.startsWith('counts=')) {
441
+ countsStr = p.slice(7);
442
+ break;
443
+ }
444
+ }
445
+ if (!countsStr)
446
+ return;
447
+ const countVals = countsStr.split(',');
448
+ if (countVals.length !== deferredCount) {
449
+ throw new Error(`count_mismatch: summary has ${countVals.length} count entries but ${deferredCount} deferred sections`);
450
+ }
451
+ // Count actual items per deferred section.
452
+ const actualCounts = [];
453
+ let inDeferred = false;
454
+ let currentCount = 0;
455
+ for (const l of contentLines) {
456
+ const trimmed = l.trimStart();
457
+ if (trimmed.startsWith('## ') && trimmed.includes('[?]')) {
458
+ if (inDeferred)
459
+ actualCounts.push(currentCount);
460
+ inDeferred = true;
461
+ currentCount = 0;
462
+ continue;
463
+ }
464
+ if (trimmed.startsWith('## ')) {
465
+ if (inDeferred) {
466
+ actualCounts.push(currentCount);
467
+ inDeferred = false;
468
+ }
469
+ continue;
470
+ }
471
+ if (inDeferred && !trimmed.startsWith(' ') && !trimmed.startsWith('.')) {
472
+ currentCount++;
473
+ }
474
+ }
475
+ if (inDeferred)
476
+ actualCounts.push(currentCount);
477
+ for (let i = 0; i < countVals.length; i++) {
478
+ const declared = parseInt(countVals[i], 10);
479
+ if (isNaN(declared))
480
+ throw new Error(`count_mismatch: invalid count value "${countVals[i]}"`);
481
+ if (i < actualCounts.length && declared !== actualCounts[i]) {
482
+ throw new Error(`count_mismatch: section ${i} declared ${declared} in summary, actual ${actualCounts[i]}`);
483
+ }
484
+ }
235
485
  }
236
486
  //# sourceMappingURL=decode_generic.js.map