@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.
- package/LICENSE +1 -1
- package/README.md +11 -1
- package/dist/decode.d.ts.map +1 -1
- package/dist/decode.js +15 -6
- package/dist/decode.js.map +1 -1
- package/dist/decode_generic.d.ts +1 -5
- package/dist/decode_generic.d.ts.map +1 -1
- package/dist/decode_generic.js +392 -142
- package/dist/decode_generic.js.map +1 -1
- package/dist/delta.js +1 -1
- package/dist/delta.js.map +1 -1
- package/dist/encode.d.ts.map +1 -1
- package/dist/encode.js +22 -7
- package/dist/encode.js.map +1 -1
- package/dist/generic.d.ts +0 -6
- package/dist/generic.d.ts.map +1 -1
- package/dist/generic.js +146 -114
- package/dist/generic.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/scalar.d.ts +27 -0
- package/dist/scalar.d.ts.map +1 -0
- package/dist/scalar.js +315 -0
- package/dist/scalar.js.map +1 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +9 -1
- package/dist/session.js.map +1 -1
- package/dist/stream.d.ts +1 -1
- package/dist/stream.js +7 -7
- package/dist/stream.js.map +1 -1
- package/dist/stream_generic.d.ts +1 -1
- package/dist/stream_generic.d.ts.map +1 -1
- package/dist/stream_generic.js +5 -27
- package/dist/stream_generic.js.map +1 -1
- package/package.json +2 -2
- package/src/decode.ts +18 -6
- package/src/decode_generic.ts +352 -137
- package/src/delta.ts +1 -1
- package/src/encode.ts +19 -7
- package/src/generic.ts +127 -128
- package/src/index.ts +1 -0
- package/src/scalar.ts +249 -0
- package/src/session.ts +6 -1
- package/src/stream.ts +7 -7
- 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
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
97
|
+
cells.push(formatScalar(v, 0x7c));
|
|
70
98
|
}
|
|
71
99
|
}
|
|
72
100
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
144
|
+
out += `${prefix}@${i} =${formatScalar(item, 0)}\n`;
|
|
142
145
|
}
|
|
143
146
|
}
|
|
147
|
+
return out;
|
|
144
148
|
}
|
|
145
149
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
|
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
|
|
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)
|
|
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)
|
|
140
|
+
if (!groupOrder.includes(g) && c > 0) deferredCounts.push(c);
|
|
141
141
|
}
|
|
142
142
|
if (this.edgeCount > 0) {
|
|
143
|
-
|
|
143
|
+
deferredCounts.push(this.edgeCount);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
this.w.write(
|
|
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. */
|
package/src/stream_generic.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
}
|