@blackwell-systems/gcf 0.6.1 → 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 +11 -1
  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
package/src/generic.ts CHANGED
@@ -1,164 +1,163 @@
1
1
  /**
2
- * Generic encoder: converts any JS value into GCF tabular format.
2
+ * Generic encoder: converts any JS value into GCF v2.0 generic profile.
3
3
  */
4
-
5
- function formatValue(value: unknown): string {
6
- if (value === null || value === undefined) return '-';
7
- if (typeof value === 'number' || typeof value === 'boolean') return String(value);
8
- const s = String(value);
9
- if (s.includes('|') || s.includes('\n') || s === '') return JSON.stringify(s);
10
- return s;
11
- }
4
+ import { formatScalar, formatKey, ATTACHMENT } from './scalar.js';
12
5
 
13
6
  function indent(depth: number): string {
14
7
  return ' '.repeat(depth);
15
8
  }
16
9
 
17
- function isUniformObjectArray(arr: unknown[]): boolean {
18
- if (arr.length === 0) return false;
19
- const objects = arr.filter(
20
- (item): item is Record<string, unknown> =>
21
- typeof item === 'object' && item !== null && !Array.isArray(item),
22
- );
23
- if (objects.length !== arr.length) return false;
24
-
25
- const referenceKeys = Object.keys(objects[0]).sort().join(',');
26
- const sampleSize = Math.min(5, objects.length);
27
- const refSet = new Set(Object.keys(objects[0]));
28
-
29
- for (let i = 1; i < sampleSize; i++) {
30
- const keys = Object.keys(objects[i]);
31
- const currentSet = new Set(keys);
32
- // 70% overlap check.
33
- const union = new Set([...refSet, ...currentSet]);
34
- const intersection = [...refSet].filter((k) => currentSet.has(k));
35
- if (intersection.length / union.size < 0.7) return false;
36
- }
10
+ export function encodeGeneric(data: unknown): string {
11
+ let out = 'GCF profile=generic\n';
12
+ out += encodeRootValue(data);
13
+ return out;
14
+ }
37
15
 
38
- return true;
16
+ function encodeRootValue(v: unknown): string {
17
+ if (v === null || v === undefined) return '=-\n';
18
+ if (Array.isArray(v)) return encodeRootArray(v);
19
+ if (typeof v === 'object') return encodeObject(v as Record<string, unknown>, 0);
20
+ return `=${formatScalar(v, 0)}\n`;
39
21
  }
40
22
 
41
- function encodeArray(
42
- arr: unknown[],
43
- name: string,
44
- lines: string[],
45
- depth: number,
46
- ): void {
23
+ function encodeObject(obj: Record<string, unknown>, depth: number): string {
47
24
  const prefix = indent(depth);
25
+ let out = '';
26
+ for (const key of Object.keys(obj)) {
27
+ const value = obj[key];
28
+ const fk = formatKey(key);
29
+ if (Array.isArray(value)) {
30
+ out += encodeNamedArray(fk, value, depth);
31
+ } else if (typeof value === 'object' && value !== null) {
32
+ out += `${prefix}## ${fk}\n`;
33
+ out += encodeObject(value as Record<string, unknown>, depth + 1);
34
+ } else {
35
+ out += `${prefix}${fk}=${formatScalar(value, 0)}\n`;
36
+ }
37
+ }
38
+ return out;
39
+ }
48
40
 
49
- if (isUniformObjectArray(arr)) {
50
- const objects = arr as Record<string, unknown>[];
41
+ function encodeRootArray(arr: unknown[]): string {
42
+ if (arr.length === 0) return '## [0]\n';
43
+ if (allPrimitives(arr)) {
44
+ const vals = arr.map(v => formatScalar(v, 0x2c));
45
+ return `## [${arr.length}]: ${vals.join(',')}\n`;
46
+ }
47
+ const fields = tabularFields(arr);
48
+ if (fields) return encodeTabular('## ', arr, fields, 0);
49
+ return encodeExpanded('## ', arr, 0);
50
+ }
51
+
52
+ function encodeNamedArray(name: string, arr: unknown[], depth: number): string {
53
+ const prefix = indent(depth);
54
+ if (arr.length === 0) return `${prefix}## ${name} [0]\n`;
55
+ if (allPrimitives(arr)) {
56
+ const vals = arr.map(v => formatScalar(v, 0x2c));
57
+ return `${prefix}${name}[${arr.length}]: ${vals.join(',')}\n`;
58
+ }
59
+ const fields = tabularFields(arr);
60
+ if (fields) return encodeTabular(`${prefix}## ${name} `, arr, fields, depth);
61
+ return encodeExpanded(`${prefix}## ${name} `, arr, depth);
62
+ }
51
63
 
52
- // Collect all keys across items.
53
- const allKeys = new Set<string>();
54
- for (const obj of objects) {
55
- for (const k of Object.keys(obj)) allKeys.add(k);
64
+ function tabularFields(arr: unknown[]): string[] | null {
65
+ if (arr.length === 0) return null;
66
+ const fieldOrder: string[] = [];
67
+ const seen = new Set<string>();
68
+ for (const item of arr) {
69
+ if (typeof item !== 'object' || item === null || Array.isArray(item)) return null;
70
+ for (const k of Object.keys(item as Record<string, unknown>)) {
71
+ if (!seen.has(k)) { fieldOrder.push(k); seen.add(k); }
56
72
  }
73
+ }
74
+ return fieldOrder.length > 0 ? fieldOrder : null;
75
+ }
57
76
 
58
- // Separate primitive fields from nested fields.
59
- const primitiveFields: string[] = [];
60
- const nestedFields: string[] = [];
61
- for (const key of allKeys) {
62
- const sample = objects.find((o) => o[key] !== undefined)?.[key];
63
- if (
64
- typeof sample === 'object' &&
65
- sample !== null
66
- ) {
67
- nestedFields.push(key);
77
+ function encodeTabular(headerPrefix: string, arr: unknown[], fields: string[], depth: number): string {
78
+ const prefix = indent(depth);
79
+ const fmtFields = fields.map(f => formatKey(f));
80
+ let out = `${headerPrefix}[${arr.length}]{${fmtFields.join(',')}}\n`;
81
+
82
+ for (let i = 0; i < arr.length; i++) {
83
+ const obj = arr[i] as Record<string, unknown>;
84
+ const cells: string[] = [];
85
+ const attachments: { name: string; value: unknown }[] = [];
86
+ let rowHasAttachment = false;
87
+
88
+ for (const f of fields) {
89
+ if (!(f in obj)) { cells.push('~'); continue; }
90
+ const v = obj[f];
91
+ if (v === null || v === undefined) { cells.push('-'); continue; }
92
+ if (typeof v === 'object') {
93
+ cells.push('^');
94
+ attachments.push({ name: f, value: v });
95
+ rowHasAttachment = true;
68
96
  } else {
69
- primitiveFields.push(key);
97
+ cells.push(formatScalar(v, 0x7c));
70
98
  }
71
99
  }
72
100
 
73
- lines.push(`${prefix}## ${name} [${arr.length}]{${primitiveFields.join(',')}}`);
74
-
75
- if (nestedFields.length === 0) {
76
- // Pure flat rows: no @id prefix.
77
- for (const obj of objects) {
78
- const vals = primitiveFields.map((f) => formatValue(obj[f]));
79
- lines.push(`${prefix}${vals.join('|')}`);
80
- }
101
+ const row = cells.join('|');
102
+ if (rowHasAttachment) {
103
+ out += `${prefix}@${i} ${row}\n`;
81
104
  } else {
82
- // Rows with nested fields: @N prefix.
83
- for (let i = 0; i < objects.length; i++) {
84
- const obj = objects[i];
85
- const vals = primitiveFields.map((f) => formatValue(obj[f]));
86
- lines.push(`${prefix}@${i} ${vals.join('|')}`);
87
- for (const nk of nestedFields) {
88
- const nv = obj[nk];
89
- if (nv === undefined || nv === null) continue;
90
- if (Array.isArray(nv)) {
91
- encodeArray(nv, nk, lines, depth + 1);
92
- } else if (typeof nv === 'object') {
93
- encodeObject(nv as Record<string, unknown>, nk, lines, depth + 1);
94
- }
95
- }
96
- }
105
+ out += `${prefix}${row}\n`;
97
106
  }
98
- } else {
99
- // Check if all items are primitives (inline as comma-separated).
100
- const allPrimitive = arr.length > 0 && arr.every(
101
- (item) => item === null || typeof item !== 'object',
102
- );
103
-
104
- if (allPrimitive) {
105
- const vals = arr.map((item) => formatValue(item));
106
- lines.push(`${prefix}${name}[${arr.length}]: ${vals.join(',')}`);
107
- } else {
108
- // Non-uniform with objects: per-item encoding.
109
- lines.push(`${prefix}## ${name} [${arr.length}]`);
110
- for (let i = 0; i < arr.length; i++) {
111
- const item = arr[i];
112
- if (typeof item === 'object' && item !== null && !Array.isArray(item)) {
113
- lines.push(`${prefix}@${i}`);
114
- encodeObject(item as Record<string, unknown>, null, lines, depth + 1);
115
- } else {
116
- lines.push(`${prefix}@${i} ${formatValue(item)}`);
117
- }
107
+
108
+ for (const att of attachments) {
109
+ const attPrefix = prefix + ' ';
110
+ const fk = formatKey(att.name);
111
+ if (Array.isArray(att.value)) {
112
+ out += encodeAttachmentArray(attPrefix, fk, att.value as unknown[], depth + 2);
113
+ } else {
114
+ out += `${attPrefix}.${fk} {}\n`;
115
+ out += encodeObject(att.value as Record<string, unknown>, depth + 2);
118
116
  }
119
117
  }
120
118
  }
119
+ return out;
121
120
  }
122
121
 
123
- function encodeObject(
124
- obj: Record<string, unknown>,
125
- name: string | null,
126
- lines: string[],
127
- depth: number,
128
- ): void {
129
- const prefix = indent(depth);
130
-
131
- if (name !== null) {
132
- lines.push(`${prefix}## ${name}`);
122
+ function encodeAttachmentArray(attPrefix: string, fk: string, arr: unknown[], depth: number): string {
123
+ if (arr.length === 0) return `${attPrefix}.${fk} [0]\n`;
124
+ if (allPrimitives(arr)) {
125
+ const vals = arr.map(v => formatScalar(v, 0x2c));
126
+ return `${attPrefix}.${fk} [${arr.length}]: ${vals.join(',')}\n`;
133
127
  }
128
+ const fields = tabularFields(arr);
129
+ if (fields) return encodeTabular(`${attPrefix}.${fk} `, arr, fields, depth);
130
+ return encodeExpanded(`${attPrefix}.${fk} `, arr, depth);
131
+ }
134
132
 
135
- for (const [key, value] of Object.entries(obj)) {
136
- if (Array.isArray(value)) {
137
- encodeArray(value, key, lines, depth);
138
- } else if (typeof value === 'object' && value !== null) {
139
- encodeObject(value as Record<string, unknown>, key, lines, depth + 1);
133
+ function encodeExpanded(headerPrefix: string, arr: unknown[], depth: number): string {
134
+ const prefix = indent(depth);
135
+ let out = `${headerPrefix}[${arr.length}]\n`;
136
+ for (let i = 0; i < arr.length; i++) {
137
+ const item = arr[i];
138
+ if (Array.isArray(item)) {
139
+ out += encodeExpandedArrayItem(prefix, i, item, depth);
140
+ } else if (typeof item === 'object' && item !== null) {
141
+ out += `${prefix}@${i} {}\n`;
142
+ out += encodeObject(item as Record<string, unknown>, depth + 1);
140
143
  } else {
141
- lines.push(`${prefix}${key}=${formatValue(value)}`);
144
+ out += `${prefix}@${i} =${formatScalar(item, 0)}\n`;
142
145
  }
143
146
  }
147
+ return out;
144
148
  }
145
149
 
146
- /**
147
- * Encode any JS value into GCF tabular format.
148
- */
149
- export function encodeGeneric(data: unknown): string {
150
- // Primitives.
151
- if (data === null || data === undefined || typeof data !== 'object') {
152
- return String(data);
153
- }
154
-
155
- const lines: string[] = [];
156
-
157
- if (Array.isArray(data)) {
158
- encodeArray(data, 'root', lines, 0);
159
- } else {
160
- encodeObject(data as Record<string, unknown>, null, lines, 0);
150
+ function encodeExpandedArrayItem(prefix: string, idx: number, arr: unknown[], depth: number): string {
151
+ if (arr.length === 0) return `${prefix}@${idx} [0]\n`;
152
+ if (allPrimitives(arr)) {
153
+ const vals = arr.map(v => formatScalar(v, 0x2c));
154
+ return `${prefix}@${idx} [${arr.length}]: ${vals.join(',')}\n`;
161
155
  }
156
+ const fields = tabularFields(arr);
157
+ if (fields) return encodeTabular(`${prefix}@${idx} `, arr, fields, depth + 1);
158
+ return encodeExpanded(`${prefix}@${idx} `, arr, depth + 1);
159
+ }
162
160
 
163
- return lines.join('\n') + '\n';
161
+ function allPrimitives(arr: unknown[]): boolean {
162
+ return arr.every(v => typeof v !== 'object' || v === null);
164
163
  }
package/src/index.ts CHANGED
@@ -6,5 +6,6 @@ export { Session, encodeWithSession } from './session.js';
6
6
  export { encodeDelta } from './delta.js';
7
7
  export { encodeGeneric } from './generic.js';
8
8
  export { decodeGeneric } from './decode_generic.js';
9
+ export { formatScalar, formatKey, parseScalar, needsQuote, quoteString } from './scalar.js';
9
10
  export { StreamEncoder, type StreamWriter, type StreamOptions } from './stream.js';
10
11
  export { GenericStreamEncoder } from './stream_generic.js';
package/src/scalar.ts ADDED
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Common scalar grammar for GCF v2.0.
3
+ * Shared between encoder, decoder, and streaming encoder.
4
+ */
5
+
6
+ const JSON_NUMBER_RE = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
7
+ const NUMERIC_LIKE_RE = /^[+-]?\.?\d/;
8
+
9
+ /** Check if a string value must be quoted per Section 2.4. */
10
+ export function needsQuote(s: string): boolean {
11
+ if (s === '') return true;
12
+ if (s === '-' || s === '~' || s === '^' || s === 'true' || s === 'false') return true;
13
+ if (JSON_NUMBER_RE.test(s)) return true;
14
+ if (NUMERIC_LIKE_RE.test(s)) return true;
15
+ if (s[0] === ' ' || s[s.length - 1] === ' ') return true;
16
+ if (s[0] === '#' || s[0] === '@') return true;
17
+ for (let i = 0; i < s.length; i++) {
18
+ const c = s.charCodeAt(i);
19
+ if (c === 0x22 || c === 0x5c || c < 0x20 || c === 0x0a || c === 0x0d ||
20
+ c === 0x7c || c === 0x2c) return true; // " \ control \n \r | ,
21
+ }
22
+ return false;
23
+ }
24
+
25
+ /** Produce a JSON-compatible quoted string. */
26
+ export function quoteString(s: string): string {
27
+ let out = '"';
28
+ for (let i = 0; i < s.length; i++) {
29
+ const c = s.charCodeAt(i);
30
+ switch (c) {
31
+ case 0x22: out += '\\"'; break;
32
+ case 0x5c: out += '\\\\'; break;
33
+ case 0x08: out += '\\b'; break;
34
+ case 0x0c: out += '\\f'; break;
35
+ case 0x0a: out += '\\n'; break;
36
+ case 0x0d: out += '\\r'; break;
37
+ case 0x09: out += '\\t'; break;
38
+ default:
39
+ if (c < 0x20) {
40
+ out += '\\u' + c.toString(16).padStart(4, '0');
41
+ } else {
42
+ out += s[i];
43
+ }
44
+ }
45
+ }
46
+ return out + '"';
47
+ }
48
+
49
+ /** Format a JS value as a GCF scalar. delimiter is '|', ',', or 0. */
50
+ export function formatScalar(v: unknown, delimiter: number = 0): string {
51
+ if (v === null || v === undefined) return '-';
52
+ if (typeof v === 'boolean') return v ? 'true' : 'false';
53
+ if (typeof v === 'number') return formatNumber(v);
54
+ const s = String(v);
55
+ if (needsQuote(s) || (delimiter && s.includes(String.fromCharCode(delimiter)))) {
56
+ return quoteString(s);
57
+ }
58
+ return s;
59
+ }
60
+
61
+ /** Format a number per Section 2.3 canonical rules. */
62
+ export function formatNumber(f: number): string {
63
+ if (Object.is(f, -0)) return '-0';
64
+ if (f === 0) return '0';
65
+ const abs = Math.abs(f);
66
+ if (abs >= 1e-6 && abs < 1e21) {
67
+ return toPreciseDecimal(f);
68
+ }
69
+ // Exponent notation.
70
+ let s = f.toExponential();
71
+ // Normalize: lowercase e, no leading zeros in exponent.
72
+ s = s.replace(/[eE]\+?0*(\d)/, 'e+$1').replace(/[eE]-0*(\d)/, 'e-$1');
73
+ return s;
74
+ }
75
+
76
+ function toPreciseDecimal(f: number): string {
77
+ // String(f) produces the shortest representation that round-trips through parseFloat.
78
+ return String(f);
79
+ }
80
+
81
+ const BARE_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
82
+
83
+ /** Check if a key is a valid bare key. */
84
+ export function isBareKey(s: string): boolean {
85
+ return BARE_KEY_RE.test(s);
86
+ }
87
+
88
+ /** Format a key, quoting if necessary. */
89
+ export function formatKey(s: string): string {
90
+ return isBareKey(s) ? s : quoteString(s);
91
+ }
92
+
93
+ // --- Decoder scalar parsing ---
94
+
95
+ /** Parse a GCF scalar token per Section 2.1 precedence. */
96
+ export function parseScalar(s: string, tabularContext: boolean): any {
97
+ if (s === '') return '';
98
+
99
+ // 1. Quoted string.
100
+ if (s[0] === '"') return parseQuotedString(s);
101
+
102
+ // 2. Null.
103
+ if (s === '-') return null;
104
+
105
+ // 3. Missing (tabular only).
106
+ if (s === '~') {
107
+ if (!tabularContext) throw new Error('invalid_missing: ~ outside tabular row cell');
108
+ return MISSING;
109
+ }
110
+
111
+ // 4. Attachment (tabular only).
112
+ if (s === '^') {
113
+ if (!tabularContext) throw new Error('invalid_attachment_marker: ^ outside tabular row cell');
114
+ return ATTACHMENT;
115
+ }
116
+
117
+ // 5. Boolean.
118
+ if (s === 'true') return true;
119
+ if (s === 'false') return false;
120
+
121
+ // 6. Number.
122
+ if (JSON_NUMBER_RE.test(s)) {
123
+ const f = Number(s);
124
+ if (!isNaN(f)) return f;
125
+ }
126
+
127
+ // 7. Bare string.
128
+ return s;
129
+ }
130
+
131
+ export const MISSING = Symbol('missing');
132
+ export const ATTACHMENT = Symbol('attachment');
133
+
134
+ /** Parse a JSON-compatible quoted string. */
135
+ export function parseQuotedString(s: string): string {
136
+ if (s.length < 2 || s[0] !== '"') throw new Error('unterminated_quote');
137
+ let out = '';
138
+ let i = 1;
139
+ while (i < s.length) {
140
+ if (s[i] === '"') {
141
+ if (i + 1 !== s.length) throw new Error('trailing_characters: after closing quote');
142
+ return out;
143
+ }
144
+ if (s[i] === '\\') {
145
+ if (i + 1 >= s.length) throw new Error('unterminated_quote');
146
+ i++;
147
+ switch (s[i]) {
148
+ case '"': out += '"'; break;
149
+ case '\\': out += '\\'; break;
150
+ case '/': out += '/'; break;
151
+ case 'b': out += '\b'; break;
152
+ case 'f': out += '\f'; break;
153
+ case 'n': out += '\n'; break;
154
+ case 'r': out += '\r'; break;
155
+ case 't': out += '\t'; break;
156
+ case 'u': {
157
+ if (i + 4 >= s.length) throw new Error('invalid_escape: incomplete unicode');
158
+ const hex = s.slice(i + 1, i + 5);
159
+ const code = parseInt(hex, 16);
160
+ if (isNaN(code)) throw new Error(`invalid_escape: invalid unicode \\u${hex}`);
161
+ // Surrogate pair handling.
162
+ if (code >= 0xd800 && code <= 0xdbff) {
163
+ if (i + 10 >= s.length || s[i + 5] !== '\\' || s[i + 6] !== 'u') {
164
+ throw new Error('invalid_surrogate: isolated high surrogate');
165
+ }
166
+ const hex2 = s.slice(i + 7, i + 11);
167
+ const low = parseInt(hex2, 16);
168
+ if (isNaN(low) || low < 0xdc00 || low > 0xdfff) {
169
+ throw new Error('invalid_surrogate: invalid low surrogate');
170
+ }
171
+ out += String.fromCodePoint(0x10000 + (code - 0xd800) * 0x400 + (low - 0xdc00));
172
+ i += 11;
173
+ continue;
174
+ }
175
+ if (code >= 0xdc00 && code <= 0xdfff) {
176
+ throw new Error('invalid_surrogate: isolated low surrogate');
177
+ }
178
+ out += String.fromCharCode(code);
179
+ i += 5;
180
+ continue;
181
+ }
182
+ default: throw new Error(`invalid_escape: unknown \\${s[i]}`);
183
+ }
184
+ i++;
185
+ continue;
186
+ }
187
+ if (s.charCodeAt(i) < 0x20) {
188
+ throw new Error(`invalid_escape: unescaped control U+${s.charCodeAt(i).toString(16).padStart(4, '0')}`);
189
+ }
190
+ out += s[i];
191
+ i++;
192
+ }
193
+ throw new Error('unterminated_quote');
194
+ }
195
+
196
+ /** Split a string on a delimiter, respecting quoted strings. */
197
+ export function splitRespectingQuotes(s: string, delim: string): string[] {
198
+ const parts: string[] = [];
199
+ let current = '';
200
+ let inQuote = false;
201
+ let escaped = false;
202
+ for (let i = 0; i < s.length; i++) {
203
+ if (escaped) { current += s[i]; escaped = false; continue; }
204
+ if (s[i] === '\\' && inQuote) { current += s[i]; escaped = true; continue; }
205
+ if (s[i] === '"') { inQuote = !inQuote; current += s[i]; continue; }
206
+ if (s[i] === delim && !inQuote) { parts.push(current); current = ''; continue; }
207
+ current += s[i];
208
+ }
209
+ parts.push(current);
210
+ return parts;
211
+ }
212
+
213
+ /** Split a field declaration like {id,"display name","a,b"}. */
214
+ export function splitFieldDecl(s: string): string[] {
215
+ if (s.length < 2 || s[0] !== '{') throw new Error('invalid field declaration');
216
+ const closeIdx = findClosingBrace(s);
217
+ if (closeIdx < 0) throw new Error('invalid field declaration');
218
+ const inner = s.slice(1, closeIdx);
219
+ if (!inner) return [];
220
+ const raw = splitRespectingQuotes(inner, ',');
221
+ const fields: string[] = [];
222
+ const seen = new Set<string>();
223
+ for (const f of raw) {
224
+ const trimmed = f.trim();
225
+ let name: string;
226
+ if (trimmed.length >= 2 && trimmed[0] === '"' && trimmed[trimmed.length - 1] === '"') {
227
+ name = parseQuotedString(trimmed);
228
+ } else {
229
+ if (!isBareKey(trimmed)) throw new Error(`invalid field name: ${trimmed}`);
230
+ name = trimmed;
231
+ }
232
+ if (seen.has(name)) throw new Error(`duplicate_field_name: ${name}`);
233
+ seen.add(name);
234
+ fields.push(name);
235
+ }
236
+ return fields;
237
+ }
238
+
239
+ function findClosingBrace(s: string): number {
240
+ let inQuote = false;
241
+ let escaped = false;
242
+ for (let i = 0; i < s.length; i++) {
243
+ if (escaped) { escaped = false; continue; }
244
+ if (s[i] === '\\' && inQuote) { escaped = true; continue; }
245
+ if (s[i] === '"') { inQuote = !inQuote; continue; }
246
+ if (s[i] === '}' && !inQuote) return i;
247
+ }
248
+ return -1;
249
+ }
package/src/session.ts CHANGED
@@ -80,7 +80,12 @@ export function encodeWithSession(p: Payload, sess: Session | null): string {
80
80
  const lines: string[] = [];
81
81
 
82
82
  // Header with session=true marker.
83
- let header = `GCF tool=${p.tool} budget=${p.tokenBudget} tokens=${p.tokensUsed} symbols=${p.symbols.length} edges=${p.edges.length} session=true`;
83
+ let header = `GCF profile=graph tool=${p.tool}`;
84
+ if (p.tokenBudget) header += ` budget=${p.tokenBudget}`;
85
+ if (p.tokensUsed) header += ` tokens=${p.tokensUsed}`;
86
+ header += ` symbols=${p.symbols.length}`;
87
+ if (p.edges.length > 0) header += ` edges=${p.edges.length}`;
88
+ header += ' session=true';
84
89
  if (p.packRoot) {
85
90
  header += ` pack_root=${p.packRoot}`;
86
91
  }
package/src/stream.ts CHANGED
@@ -48,7 +48,7 @@ export class StreamEncoder {
48
48
  }
49
49
 
50
50
  private writeHeader(tool: string, opts: StreamOptions): void {
51
- const parts = [`GCF tool=${tool}`];
51
+ const parts = [`GCF profile=graph tool=${tool}`];
52
52
  if (opts.tokenBudget) parts.push(`budget=${opts.tokenBudget}`);
53
53
  if (opts.tokensUsed) parts.push(`tokens=${opts.tokensUsed}`);
54
54
  if (opts.packRoot) parts.push(`pack_root=${opts.packRoot}`);
@@ -125,25 +125,25 @@ export class StreamEncoder {
125
125
  }
126
126
 
127
127
  /**
128
- * Emit the ## _summary trailer with final counts. Must be called after all
128
+ * Emit the ##! summary trailer with final counts. Must be called after all
129
129
  * symbols and edges have been written.
130
130
  */
131
131
  close(): void {
132
- const sections: string[] = [];
132
+ const deferredCounts: number[] = [];
133
133
  const groupOrder = ['targets', 'related', 'extended'];
134
134
 
135
135
  for (const g of groupOrder) {
136
136
  const c = this.groupCounts.get(g);
137
- if (c && c > 0) sections.push(`${g}:${c}`);
137
+ if (c && c > 0) deferredCounts.push(c);
138
138
  }
139
139
  for (const [g, c] of this.groupCounts) {
140
- if (!groupOrder.includes(g) && c > 0) sections.push(`${g}:${c}`);
140
+ if (!groupOrder.includes(g) && c > 0) deferredCounts.push(c);
141
141
  }
142
142
  if (this.edgeCount > 0) {
143
- sections.push(`edges:${this.edgeCount}`);
143
+ deferredCounts.push(this.edgeCount);
144
144
  }
145
145
 
146
- this.w.write(`## _summary symbols=${this.nextID} edges=${this.edgeCount} sections=${sections.join(',')}\n`);
146
+ this.w.write(`##! summary symbols=${this.nextID} edges=${this.edgeCount} counts=${deferredCounts.join(',')}\n`);
147
147
  }
148
148
 
149
149
  /** Number of symbols written so far. */
@@ -1,4 +1,5 @@
1
1
  import type { StreamWriter } from './stream.js';
2
+ import { formatScalar } from './scalar.js';
2
3
 
3
4
  interface SectionCount {
4
5
  name: string;
@@ -78,7 +79,7 @@ export class GenericStreamEncoder {
78
79
  this.writer.write(`${name}[${values.length}]: ${parts.join(',')}\n`);
79
80
  }
80
81
 
81
- /** Emit the ## _summary trailer with final counts. Must be called after all data. */
82
+ /** Emit the ##! summary trailer with final counts. Must be called after all data. */
82
83
  close(): void {
83
84
  if (this.current !== null) {
84
85
  this.endArrayInternal();
@@ -86,13 +87,8 @@ export class GenericStreamEncoder {
86
87
  if (this.sections.length === 0) {
87
88
  return;
88
89
  }
89
- let totalRows = 0;
90
- const sectionParts: string[] = [];
91
- for (const s of this.sections) {
92
- sectionParts.push(`${s.name}:${s.count}`);
93
- totalRows += s.count;
94
- }
95
- this.writer.write(`## _summary rows=${totalRows} sections=${sectionParts.join(',')}\n`);
90
+ const counts = this.sections.map(s => String(s.count));
91
+ this.writer.write(`##! summary counts=${counts.join(',')}\n`);
96
92
  }
97
93
 
98
94
  private endArrayInternal(): void {
@@ -105,23 +101,5 @@ export class GenericStreamEncoder {
105
101
  }
106
102
 
107
103
  function formatValue(v: unknown): string {
108
- if (v === null || v === undefined) {
109
- return '-';
110
- }
111
- if (typeof v === 'boolean') {
112
- return v ? 'true' : 'false';
113
- }
114
- if (typeof v === 'number') {
115
- return String(v);
116
- }
117
- if (typeof v === 'string') {
118
- if (v === '') {
119
- return '""';
120
- }
121
- if (v.includes('|') || v.includes('\n')) {
122
- return `"${v.replace(/"/g, '\\"')}"`;
123
- }
124
- return v;
125
- }
126
- return String(v);
104
+ return formatScalar(v, 0x7c); // '|' context
127
105
  }