@blackwell-systems/gcf 0.5.0 → 0.6.1

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/README.md CHANGED
@@ -6,9 +6,9 @@
6
6
 
7
7
  # gcf-typescript
8
8
 
9
- TypeScript implementation of [GCF (Graph Compact Format)](https://gcformat.com/) — the most token-efficient wire format for LLMs. A drop-in alternative to JSON and TOON for any structured data.
9
+ TypeScript implementation of [GCF](https://gcformat.com/) — the most token-efficient wire format for LLMs. A drop-in alternative to JSON and TOON for any structured data.
10
10
 
11
- **79% fewer input tokens than JSON. 75% fewer output tokens. 52% smaller than TOON. 100% LLM comprehension at 500 symbols, where JSON scores 76.9% and TOON scores 92.3%.**
11
+ **79% fewer input tokens than JSON. 63% fewer output tokens. 90.5% average comprehension accuracy across 10 models and 3 providers (four models hit 100%). 1,300+ LLM evaluations. Zero training.**
12
12
 
13
13
  Docs: [gcformat.com](https://gcformat.com/) · [Playground](https://gcformat.com/playground.html) · [GCF vs TOON](https://gcformat.com/guide/vs-toon.html)
14
14
 
@@ -190,33 +190,18 @@ Works on objects, arrays, and primitives. Arrays of uniform objects get tabular
190
190
  | `Session` | Tracker for multi-call deduplication |
191
191
  | `KIND_ABBREV` / `KIND_EXPAND` | Bidirectional kind abbreviation maps |
192
192
 
193
- ## Comprehension Eval
193
+ ## Benchmarks
194
194
 
195
- Rigorous 3-way benchmark (GCF vs TOON vs JSON) at 500 symbols, 200 edges. 13 structured extraction questions sent to an LLM with zero format instructions:
195
+ 1,300+ LLM evaluations across 10 models, 3 providers, and 51 independent test runs.
196
196
 
197
- | Format | Accuracy | Tokens | vs JSON |
198
- |--------|----------|--------|---------|
199
- | **GCF** | **100%** (13/13) | **11,090** | **79% fewer** |
200
- | TOON | 92.3% (12/13) | 16,378 | 69% fewer |
201
- | JSON | 76.9% (10/13) | 53,341 | baseline |
197
+ | | GCF | TOON | JSON |
198
+ |---|---|---|---|
199
+ | **Comprehension** (23 runs, 10 models) | **90.5%** | 68.5% | 53.6% |
200
+ | **Generation** (28 runs, 9 models) | **5/5** | 1.0/5 | 5.0/5 |
201
+ | **Input tokens** (500 symbols) | **11,090** | 16,378 | 53,341 |
202
+ | **Output tokens** (100 symbols) | **5,976** | 8,937 | 16,121 |
202
203
 
203
- GCF is the only format with perfect accuracy at scale, at 32% fewer tokens than TOON.
204
-
205
- Reproduce: `git clone https://github.com/blackwell-systems/gcf-go && cd gcf-go/eval && GOWORK=off go test -run TestComprehension -v -timeout 0`
206
-
207
- ## Token Efficiency (TOON's Own Benchmark)
208
-
209
- Running [TOON's benchmark harness](https://github.com/blackwell-systems/toon/tree/gcf-comparison) with GCF inserted (their datasets, their tokenizer):
210
-
211
- | Track | GCF | TOON | Result |
212
- |-------|-----|------|--------|
213
- | Mixed-structure (nested, semi-uniform) | 170,367 | 227,896 | **GCF 34% smaller** |
214
- | Flat-only (tabular) | 66,029 | 67,837 | **GCF 3% smaller** |
215
- | Semi-uniform event logs | 108,158 | 154,032 | **GCF 42% smaller** |
216
-
217
- GCF wins all 6 datasets. On semi-uniform data (the most common real-world pattern), GCF uses 42% fewer tokens than TOON.
218
-
219
- Reproduce: `git clone https://github.com/blackwell-systems/toon && cd toon && git checkout gcf-comparison && cd benchmarks && pnpm install && pnpm benchmark:tokens`
204
+ GCF wins all 6 datasets on [TOON's own benchmark](https://github.com/blackwell-systems/toon/tree/gcf-comparison). Full results: [gcformat.com/guide/benchmarks](https://gcformat.com/guide/benchmarks.html)
220
205
 
221
206
  ## Links
222
207
 
package/dist/index.d.ts CHANGED
@@ -7,4 +7,5 @@ export { encodeDelta } from './delta.js';
7
7
  export { encodeGeneric } from './generic.js';
8
8
  export { decodeGeneric } from './decode_generic.js';
9
9
  export { StreamEncoder, type StreamWriter, type StreamOptions } from './stream.js';
10
+ export { GenericStreamEncoder } from './stream_generic.js';
10
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -6,4 +6,5 @@ export { encodeDelta } from './delta.js';
6
6
  export { encodeGeneric } from './generic.js';
7
7
  export { decodeGeneric } from './decode_generic.js';
8
8
  export { StreamEncoder } from './stream.js';
9
+ export { GenericStreamEncoder } from './stream_generic.js';
9
10
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAyC,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAyC,MAAM,aAAa,CAAC;AACnF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,38 @@
1
+ import type { StreamWriter } from './stream.js';
2
+ /**
3
+ * GenericStreamEncoder writes GCF tabular output incrementally as rows arrive.
4
+ * Zero buffering: each row is written immediately. A trailer summary is
5
+ * emitted on close() with the final counts.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const enc = new GenericStreamEncoder({ write: (s) => process.stdout.write(s) });
10
+ * enc.beginArray('employees', ['id', 'name', 'department', 'salary']);
11
+ * enc.writeRow([1, 'Alice', 'Engineering', 95000]);
12
+ * enc.writeRow([2, 'Bob', 'Sales', 72000]);
13
+ * enc.endArray();
14
+ * enc.close();
15
+ * ```
16
+ */
17
+ export declare class GenericStreamEncoder {
18
+ private readonly writer;
19
+ private sections;
20
+ private current;
21
+ constructor(writer: StreamWriter);
22
+ /** Start a tabular array section with deferred count [?]. */
23
+ beginArray(name: string, fields: string[]): void;
24
+ /** Emit a single pipe-separated row immediately. */
25
+ writeRow(values: unknown[]): void;
26
+ /** Close the current array section and record its count. */
27
+ endArray(): void;
28
+ /** Emit a key=value line immediately. */
29
+ writeKV(key: string, value: unknown): void;
30
+ /** Start a nested object section (## key). */
31
+ writeSection(name: string): void;
32
+ /** Emit a primitive array inline: name[N]: val1,val2,val3 */
33
+ writeInlineArray(name: string, values: unknown[]): void;
34
+ /** Emit the ## _summary trailer with final counts. Must be called after all data. */
35
+ close(): void;
36
+ private endArrayInternal;
37
+ }
38
+ //# sourceMappingURL=stream_generic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream_generic.d.ts","sourceRoot":"","sources":["../src/stream_generic.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAahD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAsB;IACtC,OAAO,CAAC,OAAO,CAA4B;gBAE/B,MAAM,EAAE,YAAY;IAIhC,6DAA6D;IAC7D,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI;IAQhD,oDAAoD;IACpD,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI;IASjC,4DAA4D;IAC5D,QAAQ,IAAI,IAAI;IAIhB,yCAAyC;IACzC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAI1C,8CAA8C;IAC9C,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAOhC,6DAA6D;IAC7D,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI;IAKvD,qFAAqF;IACrF,KAAK,IAAI,IAAI;IAgBb,OAAO,CAAC,gBAAgB;CAOzB"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * GenericStreamEncoder writes GCF tabular output incrementally as rows arrive.
3
+ * Zero buffering: each row is written immediately. A trailer summary is
4
+ * emitted on close() with the final counts.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * const enc = new GenericStreamEncoder({ write: (s) => process.stdout.write(s) });
9
+ * enc.beginArray('employees', ['id', 'name', 'department', 'salary']);
10
+ * enc.writeRow([1, 'Alice', 'Engineering', 95000]);
11
+ * enc.writeRow([2, 'Bob', 'Sales', 72000]);
12
+ * enc.endArray();
13
+ * enc.close();
14
+ * ```
15
+ */
16
+ export class GenericStreamEncoder {
17
+ writer;
18
+ sections = [];
19
+ current = null;
20
+ constructor(writer) {
21
+ this.writer = writer;
22
+ }
23
+ /** Start a tabular array section with deferred count [?]. */
24
+ beginArray(name, fields) {
25
+ if (this.current !== null) {
26
+ this.endArrayInternal();
27
+ }
28
+ this.writer.write(`## ${name} [?]{${fields.join(',')}}\n`);
29
+ this.current = { name, fields, count: 0 };
30
+ }
31
+ /** Emit a single pipe-separated row immediately. */
32
+ writeRow(values) {
33
+ if (this.current === null) {
34
+ return;
35
+ }
36
+ const parts = values.map(formatValue);
37
+ this.writer.write(`${parts.join('|')}\n`);
38
+ this.current.count++;
39
+ }
40
+ /** Close the current array section and record its count. */
41
+ endArray() {
42
+ this.endArrayInternal();
43
+ }
44
+ /** Emit a key=value line immediately. */
45
+ writeKV(key, value) {
46
+ this.writer.write(`${key}=${formatValue(value)}\n`);
47
+ }
48
+ /** Start a nested object section (## key). */
49
+ writeSection(name) {
50
+ if (this.current !== null) {
51
+ this.endArrayInternal();
52
+ }
53
+ this.writer.write(`## ${name}\n`);
54
+ }
55
+ /** Emit a primitive array inline: name[N]: val1,val2,val3 */
56
+ writeInlineArray(name, values) {
57
+ const parts = values.map(formatValue);
58
+ this.writer.write(`${name}[${values.length}]: ${parts.join(',')}\n`);
59
+ }
60
+ /** Emit the ## _summary trailer with final counts. Must be called after all data. */
61
+ close() {
62
+ if (this.current !== null) {
63
+ this.endArrayInternal();
64
+ }
65
+ if (this.sections.length === 0) {
66
+ return;
67
+ }
68
+ let totalRows = 0;
69
+ const sectionParts = [];
70
+ for (const s of this.sections) {
71
+ sectionParts.push(`${s.name}:${s.count}`);
72
+ totalRows += s.count;
73
+ }
74
+ this.writer.write(`## _summary rows=${totalRows} sections=${sectionParts.join(',')}\n`);
75
+ }
76
+ endArrayInternal() {
77
+ if (this.current === null) {
78
+ return;
79
+ }
80
+ this.sections.push({ name: this.current.name, count: this.current.count });
81
+ this.current = null;
82
+ }
83
+ }
84
+ function formatValue(v) {
85
+ if (v === null || v === undefined) {
86
+ return '-';
87
+ }
88
+ if (typeof v === 'boolean') {
89
+ return v ? 'true' : 'false';
90
+ }
91
+ if (typeof v === 'number') {
92
+ return String(v);
93
+ }
94
+ if (typeof v === 'string') {
95
+ if (v === '') {
96
+ return '""';
97
+ }
98
+ if (v.includes('|') || v.includes('\n')) {
99
+ return `"${v.replace(/"/g, '\\"')}"`;
100
+ }
101
+ return v;
102
+ }
103
+ return String(v);
104
+ }
105
+ //# sourceMappingURL=stream_generic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream_generic.js","sourceRoot":"","sources":["../src/stream_generic.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blackwell-systems/gcf",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "TypeScript implementation of GCF (Graph Compact Format) - token-optimized wire format for LLM tool responses",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/index.ts CHANGED
@@ -7,3 +7,4 @@ export { encodeDelta } from './delta.js';
7
7
  export { encodeGeneric } from './generic.js';
8
8
  export { decodeGeneric } from './decode_generic.js';
9
9
  export { StreamEncoder, type StreamWriter, type StreamOptions } from './stream.js';
10
+ export { GenericStreamEncoder } from './stream_generic.js';
@@ -0,0 +1,127 @@
1
+ import type { StreamWriter } from './stream.js';
2
+
3
+ interface SectionCount {
4
+ name: string;
5
+ count: number;
6
+ }
7
+
8
+ interface ActiveArray {
9
+ name: string;
10
+ fields: string[];
11
+ count: number;
12
+ }
13
+
14
+ /**
15
+ * GenericStreamEncoder writes GCF tabular output incrementally as rows arrive.
16
+ * Zero buffering: each row is written immediately. A trailer summary is
17
+ * emitted on close() with the final counts.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const enc = new GenericStreamEncoder({ write: (s) => process.stdout.write(s) });
22
+ * enc.beginArray('employees', ['id', 'name', 'department', 'salary']);
23
+ * enc.writeRow([1, 'Alice', 'Engineering', 95000]);
24
+ * enc.writeRow([2, 'Bob', 'Sales', 72000]);
25
+ * enc.endArray();
26
+ * enc.close();
27
+ * ```
28
+ */
29
+ export class GenericStreamEncoder {
30
+ private readonly writer: StreamWriter;
31
+ private sections: SectionCount[] = [];
32
+ private current: ActiveArray | null = null;
33
+
34
+ constructor(writer: StreamWriter) {
35
+ this.writer = writer;
36
+ }
37
+
38
+ /** Start a tabular array section with deferred count [?]. */
39
+ beginArray(name: string, fields: string[]): void {
40
+ if (this.current !== null) {
41
+ this.endArrayInternal();
42
+ }
43
+ this.writer.write(`## ${name} [?]{${fields.join(',')}}\n`);
44
+ this.current = { name, fields, count: 0 };
45
+ }
46
+
47
+ /** Emit a single pipe-separated row immediately. */
48
+ writeRow(values: unknown[]): void {
49
+ if (this.current === null) {
50
+ return;
51
+ }
52
+ const parts = values.map(formatValue);
53
+ this.writer.write(`${parts.join('|')}\n`);
54
+ this.current.count++;
55
+ }
56
+
57
+ /** Close the current array section and record its count. */
58
+ endArray(): void {
59
+ this.endArrayInternal();
60
+ }
61
+
62
+ /** Emit a key=value line immediately. */
63
+ writeKV(key: string, value: unknown): void {
64
+ this.writer.write(`${key}=${formatValue(value)}\n`);
65
+ }
66
+
67
+ /** Start a nested object section (## key). */
68
+ writeSection(name: string): void {
69
+ if (this.current !== null) {
70
+ this.endArrayInternal();
71
+ }
72
+ this.writer.write(`## ${name}\n`);
73
+ }
74
+
75
+ /** Emit a primitive array inline: name[N]: val1,val2,val3 */
76
+ writeInlineArray(name: string, values: unknown[]): void {
77
+ const parts = values.map(formatValue);
78
+ this.writer.write(`${name}[${values.length}]: ${parts.join(',')}\n`);
79
+ }
80
+
81
+ /** Emit the ## _summary trailer with final counts. Must be called after all data. */
82
+ close(): void {
83
+ if (this.current !== null) {
84
+ this.endArrayInternal();
85
+ }
86
+ if (this.sections.length === 0) {
87
+ return;
88
+ }
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`);
96
+ }
97
+
98
+ private endArrayInternal(): void {
99
+ if (this.current === null) {
100
+ return;
101
+ }
102
+ this.sections.push({ name: this.current.name, count: this.current.count });
103
+ this.current = null;
104
+ }
105
+ }
106
+
107
+ 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);
127
+ }