@blackwell-systems/gcf 0.3.1 → 0.5.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/README.md CHANGED
@@ -44,33 +44,21 @@ Or install globally: `npm install -g @blackwell-systems/gcf` then use `gcf` dire
44
44
  ### Quick Start
45
45
 
46
46
  ```typescript
47
- import { encode, type Payload } from '@blackwell-systems/gcf';
47
+ import { encodeGeneric } from '@blackwell-systems/gcf';
48
48
 
49
- const p: Payload = {
50
- tool: 'context_for_task',
51
- tokenBudget: 5000,
52
- tokensUsed: 1847,
53
- symbols: [
54
- { qualifiedName: 'pkg.AuthMiddleware', kind: 'function', score: 0.78, provenance: 'lsp_resolved', distance: 0 },
55
- { qualifiedName: 'pkg.NewServer', kind: 'function', score: 0.54, provenance: 'lsp_resolved', distance: 1 },
56
- ],
57
- edges: [
58
- { source: 'pkg.NewServer', target: 'pkg.AuthMiddleware', edgeType: 'calls' },
49
+ const output = encodeGeneric({
50
+ employees: [
51
+ { id: 1, name: 'Alice', department: 'Engineering', salary: 95000 },
52
+ { id: 2, name: 'Bob', department: 'Sales', salary: 72000 },
59
53
  ],
60
- };
61
-
62
- const output = encode(p);
54
+ });
63
55
  ```
64
56
 
65
57
  Output:
66
58
  ```
67
- GCF tool=context_for_task budget=5000 tokens=1847 symbols=2 edges=1
68
- ## targets
69
- @0 fn pkg.AuthMiddleware 0.78 lsp_resolved
70
- ## related
71
- @1 fn pkg.NewServer 0.54 lsp_resolved
72
- ## edges [1]
73
- @0<@1 calls
59
+ ## employees [2]{id,name,department,salary}
60
+ 1|Alice|Engineering|95000
61
+ 2|Bob|Sales|72000
74
62
  ```
75
63
 
76
64
  ## Decode
@@ -97,6 +85,40 @@ const out2 = encodeWithSession(payload2, sess); // reused symbols as "@N # prev
97
85
 
98
86
  By the 5th call in a session: 92.7% token savings vs JSON.
99
87
 
88
+ ## Streaming Encode
89
+
90
+ Write GCF output incrementally as symbols and edges arrive. Zero buffering, O(1) memory per row. Ideal for MCP servers that walk large graphs or paginate results:
91
+
92
+ ```typescript
93
+ import { StreamEncoder } from '@blackwell-systems/gcf';
94
+
95
+ const enc = new StreamEncoder(writer, 'context_for_task', { tokenBudget: 5000 });
96
+
97
+ // Symbols emit immediately as they're discovered.
98
+ enc.writeSymbol({ qualifiedName: 'pkg.Auth', kind: 'function', score: 0.95, provenance: 'lsp', distance: 0 });
99
+ enc.writeSymbol({ qualifiedName: 'pkg.Server', kind: 'function', score: 0.60, provenance: 'lsp', distance: 1 });
100
+
101
+ // Edges emit immediately too.
102
+ enc.writeEdge({ source: 'pkg.Server', target: 'pkg.Auth', edgeType: 'calls' });
103
+
104
+ // Close emits the ## _summary trailer with final counts.
105
+ enc.close();
106
+ ```
107
+
108
+ Output:
109
+ ```
110
+ GCF tool=context_for_task budget=5000
111
+ ## targets
112
+ @0 fn pkg.Auth 0.95 lsp
113
+ ## related
114
+ @1 fn pkg.Server 0.60 lsp
115
+ ## edges [?]
116
+ @0<@1 calls
117
+ ## _summary symbols=2 edges=1 sections=targets:1,related:1,edges:1
118
+ ```
119
+
120
+ The `writer` is any object with a `write(s: string)` method (Node.js streams, web WritableStreams, or a simple callback). Standard `decode()` handles streaming output with no changes.
121
+
100
122
  ## Delta Encoding
101
123
 
102
124
  When the consumer already has a prior context pack, send only what changed:
@@ -153,6 +175,7 @@ Works on objects, arrays, and primitives. Arrays of uniform objects get tabular
153
175
  | `encodeGeneric(data: unknown): string` | Encode any value to GCF tabular format |
154
176
  | `decode(input: string): Payload` | Parse GCF text back to a Payload |
155
177
  | `encodeWithSession(p: Payload, s: Session): string` | Encode with session deduplication |
178
+ | `new StreamEncoder(w, tool, opts)` | Create a streaming encoder (zero-buffering) |
156
179
  | `encodeDelta(d: DeltaPayload): string` | Encode a delta (added/removed only) |
157
180
  | `new Session()` | Create a new session tracker |
158
181
 
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Decode any GCF text (tabular or graph profile) back into a JS value.
3
+ * Returns objects, arrays, and primitives matching the original structure.
4
+ *
5
+ * If the input starts with "GCF " (graph profile), falls back to decode()
6
+ * and returns the Payload as a plain object.
7
+ */
8
+ export declare function decodeGeneric(input: string): any;
9
+ //# sourceMappingURL=decode_generic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode_generic.d.ts","sourceRoot":"","sources":["../src/decode_generic.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAiChD"}
@@ -0,0 +1,236 @@
1
+ import { decode } from './decode.js';
2
+ /**
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.
8
+ */
9
+ export function decodeGeneric(input) {
10
+ input = input.trimEnd();
11
+ if (!input)
12
+ return null;
13
+ const lines = input.split('\n');
14
+ // Graph profile fallback.
15
+ if (lines[0].startsWith('GCF ')) {
16
+ const p = decode(input);
17
+ return {
18
+ tool: p.tool,
19
+ tokenBudget: p.tokenBudget,
20
+ tokensUsed: p.tokensUsed,
21
+ packRoot: p.packRoot ?? '',
22
+ symbols: p.symbols.map(s => ({
23
+ qualifiedName: s.qualifiedName,
24
+ kind: s.kind,
25
+ score: s.score,
26
+ provenance: s.provenance,
27
+ distance: s.distance,
28
+ })),
29
+ edges: p.edges.map(e => ({
30
+ source: e.source,
31
+ target: e.target,
32
+ edgeType: e.edgeType,
33
+ ...(e.status ? { status: e.status } : {}),
34
+ })),
35
+ };
36
+ }
37
+ const result = {};
38
+ parseObject(lines, 0, 0, result);
39
+ return result;
40
+ }
41
+ function parseObject(lines, start, depth, out) {
42
+ const indent = ' '.repeat(depth);
43
+ let i = start;
44
+ 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))
51
+ break;
52
+ const content = depth > 0 ? raw.slice(indent.length) : raw;
53
+ // Skip _summary.
54
+ if (content.startsWith('## _summary')) {
55
+ i++;
56
+ continue;
57
+ }
58
+ // Tabular array or section header.
59
+ 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
+ }
95
+ }
96
+ // Plain section header.
97
+ let name = header;
98
+ const bi = name.indexOf(' [');
99
+ if (bi >= 0)
100
+ name = name.slice(0, bi);
101
+ i++;
102
+ const nested = {};
103
+ const consumed = parseObject(lines, i, depth + 1, nested);
104
+ out[name] = nested;
105
+ i += consumed;
106
+ continue;
107
+ }
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;
118
+ }
119
+ }
120
+ // Key=value.
121
+ const eqIdx = content.indexOf('=');
122
+ if (eqIdx > 0) {
123
+ const key = content.slice(0, eqIdx);
124
+ const val = content.slice(eqIdx + 1);
125
+ out[key] = parseValue(val);
126
+ i++;
127
+ continue;
128
+ }
129
+ i++;
130
+ }
131
+ return i - start;
132
+ }
133
+ function parseTabularRows(lines, start, depth, fields) {
134
+ const indent = ' '.repeat(depth);
135
+ const rows = [];
136
+ let i = start;
137
+ 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;
144
+ if (content === null)
145
+ break;
146
+ if (content.startsWith('## '))
147
+ break;
148
+ if (content.startsWith('# ')) {
149
+ i++;
150
+ continue;
151
+ }
152
+ let rowData = content;
153
+ let hasNested = false;
154
+ if (rowData.startsWith('@')) {
155
+ const sp = rowData.indexOf(' ');
156
+ if (sp > 0) {
157
+ rowData = rowData.slice(sp + 1);
158
+ hasNested = true;
159
+ }
160
+ }
161
+ const vals = rowData.split('|');
162
+ const row = {};
163
+ for (let j = 0; j < fields.length; j++) {
164
+ row[fields[j]] = j < vals.length ? parseValue(vals[j]) : null;
165
+ }
166
+ i++;
167
+ if (hasNested) {
168
+ const nestedIndent = indent + ' ';
169
+ while (i < lines.length) {
170
+ const nl = lines[i].replace(/\r$/, '');
171
+ if (!nl.startsWith(nestedIndent))
172
+ 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 {
183
+ break;
184
+ }
185
+ }
186
+ }
187
+ rows.push(row);
188
+ }
189
+ return [rows, i - start];
190
+ }
191
+ function parseNonUniformArray(lines, start, depth) {
192
+ const indent = ' '.repeat(depth);
193
+ const items = [];
194
+ let i = start;
195
+ 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;
202
+ if (content === null)
203
+ break;
204
+ if (content.startsWith('## '))
205
+ break;
206
+ if (content.startsWith('@')) {
207
+ const sp = content.indexOf(' ');
208
+ if (sp > 0) {
209
+ items.push(parseValue(content.slice(sp + 1)));
210
+ }
211
+ i++;
212
+ }
213
+ else {
214
+ break;
215
+ }
216
+ }
217
+ return [items, i - start];
218
+ }
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;
235
+ }
236
+ //# sourceMappingURL=decode_generic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode_generic.js","sourceRoot":"","sources":["../src/decode_generic.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -5,4 +5,6 @@ export { decode } from './decode.js';
5
5
  export { Session, encodeWithSession } from './session.js';
6
6
  export { encodeDelta } from './delta.js';
7
7
  export { encodeGeneric } from './generic.js';
8
+ export { decodeGeneric } from './decode_generic.js';
9
+ export { StreamEncoder, type StreamWriter, type StreamOptions } from './stream.js';
8
10
  //# 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"}
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"}
package/dist/index.js CHANGED
@@ -4,4 +4,6 @@ export { decode } from './decode.js';
4
4
  export { Session, encodeWithSession } from './session.js';
5
5
  export { encodeDelta } from './delta.js';
6
6
  export { encodeGeneric } from './generic.js';
7
+ export { decodeGeneric } from './decode_generic.js';
8
+ export { StreamEncoder } from './stream.js';
7
9
  //# 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"}
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"}
@@ -0,0 +1,67 @@
1
+ import type { Symbol, Edge } from './types.js';
2
+ /**
3
+ * Options for the streaming encoder.
4
+ */
5
+ export interface StreamOptions {
6
+ tokenBudget?: number;
7
+ tokensUsed?: number;
8
+ packRoot?: string;
9
+ session?: boolean;
10
+ }
11
+ /**
12
+ * A writable sink for streaming output. Accepts string chunks.
13
+ * Compatible with Node.js streams, web WritableStreams, or simple callbacks.
14
+ */
15
+ export interface StreamWriter {
16
+ write(chunk: string): void;
17
+ }
18
+ /**
19
+ * StreamEncoder writes GCF output incrementally as symbols and edges arrive.
20
+ * Zero buffering: each symbol/edge is written immediately. A trailer summary
21
+ * is emitted on close() with the final counts.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * const chunks: string[] = [];
26
+ * const enc = new StreamEncoder({ write: (s) => chunks.push(s) }, 'context_for_task', { tokenBudget: 5000 });
27
+ * enc.writeSymbol({ qualifiedName: 'pkg.Auth', kind: 'function', score: 0.95, provenance: 'lsp', distance: 0 });
28
+ * enc.writeEdge({ source: 'pkg.Server', target: 'pkg.Auth', edgeType: 'calls' });
29
+ * enc.close();
30
+ * ```
31
+ */
32
+ export declare class StreamEncoder {
33
+ private w;
34
+ private symIndex;
35
+ private nextID;
36
+ private currentGroup;
37
+ private groupCounts;
38
+ private edgeCount;
39
+ private edgesStarted;
40
+ constructor(w: StreamWriter, tool: string, opts?: StreamOptions);
41
+ private writeHeader;
42
+ /**
43
+ * Emit a symbol line immediately. Group headers are emitted automatically
44
+ * when the distance changes.
45
+ */
46
+ writeSymbol(s: Symbol): void;
47
+ /**
48
+ * Emit an edge line immediately. The edges section header is emitted
49
+ * automatically on the first edge (with [?] deferred count).
50
+ * Source and target must reference previously-written symbols.
51
+ */
52
+ writeEdge(e: Edge): void;
53
+ /**
54
+ * Emit a bare reference for a previously-transmitted symbol (session mode).
55
+ */
56
+ writeBareRef(qname: string, distance: number): void;
57
+ /**
58
+ * Emit the ## _summary trailer with final counts. Must be called after all
59
+ * symbols and edges have been written.
60
+ */
61
+ close(): void;
62
+ /** Number of symbols written so far. */
63
+ get symbolCount(): number;
64
+ /** Number of edges written so far. */
65
+ get edgeCount_(): number;
66
+ }
67
+ //# sourceMappingURL=stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,CAAC,CAAe;IACxB,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,YAAY,CAAM;IAC1B,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,YAAY,CAAS;gBAEjB,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB;IAKnE,OAAO,CAAC,WAAW;IASnB;;;OAGG;IACH,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAoB5B;;;;OAIG;IACH,SAAS,CAAC,CAAC,EAAE,IAAI,GAAG,IAAI;IAkBxB;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAkBnD;;;OAGG;IACH,KAAK,IAAI,IAAI;IAkBb,wCAAwC;IACxC,IAAI,WAAW,IAAI,MAAM,CAAwB;IAEjD,sCAAsC;IACtC,IAAI,UAAU,IAAI,MAAM,CAA2B;CACpD"}
package/dist/stream.js ADDED
@@ -0,0 +1,123 @@
1
+ import { KIND_ABBREV } from './constants.js';
2
+ /**
3
+ * StreamEncoder writes GCF output incrementally as symbols and edges arrive.
4
+ * Zero buffering: each symbol/edge is written immediately. A trailer summary
5
+ * is emitted on close() with the final counts.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const chunks: string[] = [];
10
+ * const enc = new StreamEncoder({ write: (s) => chunks.push(s) }, 'context_for_task', { tokenBudget: 5000 });
11
+ * enc.writeSymbol({ qualifiedName: 'pkg.Auth', kind: 'function', score: 0.95, provenance: 'lsp', distance: 0 });
12
+ * enc.writeEdge({ source: 'pkg.Server', target: 'pkg.Auth', edgeType: 'calls' });
13
+ * enc.close();
14
+ * ```
15
+ */
16
+ export class StreamEncoder {
17
+ w;
18
+ symIndex = new Map();
19
+ nextID = 0;
20
+ currentGroup = '';
21
+ groupCounts = new Map();
22
+ edgeCount = 0;
23
+ edgesStarted = false;
24
+ constructor(w, tool, opts = {}) {
25
+ this.w = w;
26
+ this.writeHeader(tool, opts);
27
+ }
28
+ writeHeader(tool, opts) {
29
+ const parts = [`GCF tool=${tool}`];
30
+ if (opts.tokenBudget)
31
+ parts.push(`budget=${opts.tokenBudget}`);
32
+ if (opts.tokensUsed)
33
+ parts.push(`tokens=${opts.tokensUsed}`);
34
+ if (opts.packRoot)
35
+ parts.push(`pack_root=${opts.packRoot}`);
36
+ if (opts.session)
37
+ parts.push('session=true');
38
+ this.w.write(parts.join(' ') + '\n');
39
+ }
40
+ /**
41
+ * Emit a symbol line immediately. Group headers are emitted automatically
42
+ * when the distance changes.
43
+ */
44
+ writeSymbol(s) {
45
+ const groupNames = ['targets', 'related', 'extended'];
46
+ const groupName = s.distance < groupNames.length
47
+ ? groupNames[s.distance]
48
+ : `distance_${s.distance}`;
49
+ if (groupName !== this.currentGroup) {
50
+ this.w.write(`## ${groupName}\n`);
51
+ this.currentGroup = groupName;
52
+ }
53
+ const id = this.nextID++;
54
+ this.symIndex.set(s.qualifiedName, id);
55
+ const kind = KIND_ABBREV[s.kind] || s.kind;
56
+ this.w.write(`@${id} ${kind} ${s.qualifiedName} ${s.score.toFixed(2)} ${s.provenance}\n`);
57
+ this.groupCounts.set(groupName, (this.groupCounts.get(groupName) || 0) + 1);
58
+ }
59
+ /**
60
+ * Emit an edge line immediately. The edges section header is emitted
61
+ * automatically on the first edge (with [?] deferred count).
62
+ * Source and target must reference previously-written symbols.
63
+ */
64
+ writeEdge(e) {
65
+ const srcIdx = this.symIndex.get(e.source);
66
+ const tgtIdx = this.symIndex.get(e.target);
67
+ if (srcIdx === undefined || tgtIdx === undefined)
68
+ return;
69
+ if (!this.edgesStarted) {
70
+ this.w.write('## edges [?]\n');
71
+ this.edgesStarted = true;
72
+ }
73
+ let line = `@${tgtIdx}<@${srcIdx} ${e.edgeType}`;
74
+ if (e.status && e.status !== 'unchanged') {
75
+ line += ` ${e.status}`;
76
+ }
77
+ this.w.write(line + '\n');
78
+ this.edgeCount++;
79
+ }
80
+ /**
81
+ * Emit a bare reference for a previously-transmitted symbol (session mode).
82
+ */
83
+ writeBareRef(qname, distance) {
84
+ const groupNames = ['targets', 'related', 'extended'];
85
+ const groupName = distance < groupNames.length
86
+ ? groupNames[distance]
87
+ : `distance_${distance}`;
88
+ if (groupName !== this.currentGroup) {
89
+ this.w.write(`## ${groupName}\n`);
90
+ this.currentGroup = groupName;
91
+ }
92
+ const id = this.nextID++;
93
+ this.symIndex.set(qname, id);
94
+ this.w.write(`@${id} # previously transmitted\n`);
95
+ this.groupCounts.set(groupName, (this.groupCounts.get(groupName) || 0) + 1);
96
+ }
97
+ /**
98
+ * Emit the ## _summary trailer with final counts. Must be called after all
99
+ * symbols and edges have been written.
100
+ */
101
+ close() {
102
+ const sections = [];
103
+ const groupOrder = ['targets', 'related', 'extended'];
104
+ for (const g of groupOrder) {
105
+ const c = this.groupCounts.get(g);
106
+ if (c && c > 0)
107
+ sections.push(`${g}:${c}`);
108
+ }
109
+ for (const [g, c] of this.groupCounts) {
110
+ if (!groupOrder.includes(g) && c > 0)
111
+ sections.push(`${g}:${c}`);
112
+ }
113
+ if (this.edgeCount > 0) {
114
+ sections.push(`edges:${this.edgeCount}`);
115
+ }
116
+ this.w.write(`## _summary symbols=${this.nextID} edges=${this.edgeCount} sections=${sections.join(',')}\n`);
117
+ }
118
+ /** Number of symbols written so far. */
119
+ get symbolCount() { return this.nextID; }
120
+ /** Number of edges written so far. */
121
+ get edgeCount_() { return this.edgeCount; }
122
+ }
123
+ //# sourceMappingURL=stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../src/stream.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.3.1",
3
+ "version": "0.5.0",
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",
@@ -0,0 +1,237 @@
1
+ import { decode } from './decode.js';
2
+
3
+ /**
4
+ * Decode any GCF text (tabular or graph profile) back into a JS value.
5
+ * Returns objects, arrays, and primitives matching the original structure.
6
+ *
7
+ * If the input starts with "GCF " (graph profile), falls back to decode()
8
+ * and returns the Payload as a plain object.
9
+ */
10
+ export function decodeGeneric(input: string): any {
11
+ input = input.trimEnd();
12
+ if (!input) return null;
13
+
14
+ const lines = input.split('\n');
15
+
16
+ // Graph profile fallback.
17
+ if (lines[0].startsWith('GCF ')) {
18
+ const p = decode(input);
19
+ return {
20
+ tool: p.tool,
21
+ tokenBudget: p.tokenBudget,
22
+ tokensUsed: p.tokensUsed,
23
+ packRoot: p.packRoot ?? '',
24
+ symbols: p.symbols.map(s => ({
25
+ qualifiedName: s.qualifiedName,
26
+ kind: s.kind,
27
+ score: s.score,
28
+ provenance: s.provenance,
29
+ distance: s.distance,
30
+ })),
31
+ edges: p.edges.map(e => ({
32
+ source: e.source,
33
+ target: e.target,
34
+ edgeType: e.edgeType,
35
+ ...(e.status ? { status: e.status } : {}),
36
+ })),
37
+ };
38
+ }
39
+
40
+ const result: Record<string, any> = {};
41
+ parseObject(lines, 0, 0, result);
42
+ return result;
43
+ }
44
+
45
+ function parseObject(lines: string[], start: number, depth: number, out: Record<string, any>): number {
46
+ const indent = ' '.repeat(depth);
47
+ let i = start;
48
+
49
+ while (i < lines.length) {
50
+ const raw = lines[i].replace(/\r$/, '');
51
+ if (raw === '' || raw.startsWith('# ')) { i++; continue; }
52
+
53
+ if (depth > 0 && !raw.startsWith(indent)) break;
54
+
55
+ const content = depth > 0 ? raw.slice(indent.length) : raw;
56
+
57
+ // Skip _summary.
58
+ if (content.startsWith('## _summary')) { i++; continue; }
59
+
60
+ // Tabular array or section header.
61
+ if (content.startsWith('## ')) {
62
+ const header = content.slice(3);
63
+ const bracketIdx = header.indexOf(' [');
64
+
65
+ if (bracketIdx >= 0) {
66
+ const name = header.slice(0, bracketIdx);
67
+ const rest = header.slice(bracketIdx + 2);
68
+ const closeBracket = rest.indexOf(']');
69
+
70
+ if (closeBracket >= 0) {
71
+ const afterBracket = rest.slice(closeBracket + 1);
72
+
73
+ if (afterBracket.startsWith('{')) {
74
+ // Tabular with fields.
75
+ const fieldEnd = afterBracket.indexOf('}');
76
+ if (fieldEnd >= 0) {
77
+ const fields = afterBracket.slice(1, fieldEnd).split(',');
78
+ i++;
79
+ const [rows, consumed] = parseTabularRows(lines, i, depth, fields);
80
+ out[name] = rows;
81
+ i += consumed;
82
+ continue;
83
+ }
84
+ } else {
85
+ const countStr = rest.slice(0, closeBracket);
86
+ if (countStr === '0') {
87
+ out[name] = [];
88
+ i++;
89
+ continue;
90
+ }
91
+ // Non-uniform array.
92
+ i++;
93
+ const [items, consumed] = parseNonUniformArray(lines, i, depth);
94
+ out[name] = items;
95
+ i += consumed;
96
+ continue;
97
+ }
98
+ }
99
+ }
100
+
101
+ // Plain section header.
102
+ let name = header;
103
+ const bi = name.indexOf(' [');
104
+ if (bi >= 0) name = name.slice(0, bi);
105
+ i++;
106
+ const nested: Record<string, any> = {};
107
+ const consumed = parseObject(lines, i, depth + 1, nested);
108
+ out[name] = nested;
109
+ i += consumed;
110
+ continue;
111
+ }
112
+
113
+ // Inline primitive array: name[N]: val1,val2,...
114
+ const bracketIdx = content.indexOf('[');
115
+ if (bracketIdx > 0) {
116
+ const colonIdx = content.indexOf(']: ');
117
+ if (colonIdx > bracketIdx) {
118
+ const name = content.slice(0, bracketIdx);
119
+ const valsStr = content.slice(colonIdx + 3);
120
+ out[name] = valsStr.split(',').map(v => parseValue(v.trim()));
121
+ i++;
122
+ continue;
123
+ }
124
+ }
125
+
126
+ // Key=value.
127
+ const eqIdx = content.indexOf('=');
128
+ if (eqIdx > 0) {
129
+ const key = content.slice(0, eqIdx);
130
+ const val = content.slice(eqIdx + 1);
131
+ out[key] = parseValue(val);
132
+ i++;
133
+ continue;
134
+ }
135
+
136
+ i++;
137
+ }
138
+
139
+ return i - start;
140
+ }
141
+
142
+ function parseTabularRows(lines: string[], start: number, depth: number, fields: string[]): [any[], number] {
143
+ const indent = ' '.repeat(depth);
144
+ const rows: any[] = [];
145
+ let i = start;
146
+
147
+ while (i < lines.length) {
148
+ const raw = lines[i].replace(/\r$/, '');
149
+ if (raw === '') { i++; continue; }
150
+
151
+ const content = depth > 0 ? (raw.startsWith(indent) ? raw.slice(indent.length) : null) : raw;
152
+ if (content === null) break;
153
+ if (content.startsWith('## ')) break;
154
+ if (content.startsWith('# ')) { i++; continue; }
155
+
156
+ let rowData = content;
157
+ let hasNested = false;
158
+ if (rowData.startsWith('@')) {
159
+ const sp = rowData.indexOf(' ');
160
+ if (sp > 0) {
161
+ rowData = rowData.slice(sp + 1);
162
+ hasNested = true;
163
+ }
164
+ }
165
+
166
+ const vals = rowData.split('|');
167
+ const row: Record<string, any> = {};
168
+ for (let j = 0; j < fields.length; j++) {
169
+ row[fields[j]] = j < vals.length ? parseValue(vals[j]) : null;
170
+ }
171
+
172
+ i++;
173
+
174
+ if (hasNested) {
175
+ const nestedIndent = indent + ' ';
176
+ while (i < lines.length) {
177
+ const nl = lines[i].replace(/\r$/, '');
178
+ if (!nl.startsWith(nestedIndent)) break;
179
+ const nc = nl.slice(nestedIndent.length);
180
+
181
+ if (nc.startsWith('.')) {
182
+ const fieldName = nc.slice(1);
183
+ i++;
184
+ const nested: Record<string, any> = {};
185
+ const consumed = parseObject(lines, i, depth + 2, nested);
186
+ row[fieldName] = nested;
187
+ i += consumed;
188
+ } else {
189
+ break;
190
+ }
191
+ }
192
+ }
193
+
194
+ rows.push(row);
195
+ }
196
+
197
+ return [rows, i - start];
198
+ }
199
+
200
+ function parseNonUniformArray(lines: string[], start: number, depth: number): [any[], number] {
201
+ const indent = ' '.repeat(depth);
202
+ const items: any[] = [];
203
+ let i = start;
204
+
205
+ while (i < lines.length) {
206
+ const raw = lines[i].replace(/\r$/, '');
207
+ if (raw === '') { i++; continue; }
208
+ const content = depth > 0 ? (raw.startsWith(indent) ? raw.slice(indent.length) : null) : raw;
209
+ if (content === null) break;
210
+ if (content.startsWith('## ')) break;
211
+
212
+ if (content.startsWith('@')) {
213
+ const sp = content.indexOf(' ');
214
+ if (sp > 0) {
215
+ items.push(parseValue(content.slice(sp + 1)));
216
+ }
217
+ i++;
218
+ } else {
219
+ break;
220
+ }
221
+ }
222
+
223
+ return [items, i - start];
224
+ }
225
+
226
+ function parseValue(s: string): any {
227
+ if (s === '-') return null;
228
+ if (s === 'true') return true;
229
+ if (s === 'false') return false;
230
+ if (s === '""') return '';
231
+ if (s.length >= 2 && s[0] === '"' && s[s.length - 1] === '"') {
232
+ return s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
233
+ }
234
+ const n = Number(s);
235
+ if (!isNaN(n) && s !== '') return n;
236
+ return s;
237
+ }
package/src/index.ts CHANGED
@@ -5,3 +5,5 @@ export { decode } from './decode.js';
5
5
  export { Session, encodeWithSession } from './session.js';
6
6
  export { encodeDelta } from './delta.js';
7
7
  export { encodeGeneric } from './generic.js';
8
+ export { decodeGeneric } from './decode_generic.js';
9
+ export { StreamEncoder, type StreamWriter, type StreamOptions } from './stream.js';
package/src/stream.ts ADDED
@@ -0,0 +1,154 @@
1
+ import { KIND_ABBREV } from './constants.js';
2
+ import type { Symbol, Edge } from './types.js';
3
+
4
+ /**
5
+ * Options for the streaming encoder.
6
+ */
7
+ export interface StreamOptions {
8
+ tokenBudget?: number;
9
+ tokensUsed?: number;
10
+ packRoot?: string;
11
+ session?: boolean;
12
+ }
13
+
14
+ /**
15
+ * A writable sink for streaming output. Accepts string chunks.
16
+ * Compatible with Node.js streams, web WritableStreams, or simple callbacks.
17
+ */
18
+ export interface StreamWriter {
19
+ write(chunk: string): void;
20
+ }
21
+
22
+ /**
23
+ * StreamEncoder writes GCF output incrementally as symbols and edges arrive.
24
+ * Zero buffering: each symbol/edge is written immediately. A trailer summary
25
+ * is emitted on close() with the final counts.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const chunks: string[] = [];
30
+ * const enc = new StreamEncoder({ write: (s) => chunks.push(s) }, 'context_for_task', { tokenBudget: 5000 });
31
+ * enc.writeSymbol({ qualifiedName: 'pkg.Auth', kind: 'function', score: 0.95, provenance: 'lsp', distance: 0 });
32
+ * enc.writeEdge({ source: 'pkg.Server', target: 'pkg.Auth', edgeType: 'calls' });
33
+ * enc.close();
34
+ * ```
35
+ */
36
+ export class StreamEncoder {
37
+ private w: StreamWriter;
38
+ private symIndex: Map<string, number> = new Map();
39
+ private nextID = 0;
40
+ private currentGroup = '';
41
+ private groupCounts: Map<string, number> = new Map();
42
+ private edgeCount = 0;
43
+ private edgesStarted = false;
44
+
45
+ constructor(w: StreamWriter, tool: string, opts: StreamOptions = {}) {
46
+ this.w = w;
47
+ this.writeHeader(tool, opts);
48
+ }
49
+
50
+ private writeHeader(tool: string, opts: StreamOptions): void {
51
+ const parts = [`GCF tool=${tool}`];
52
+ if (opts.tokenBudget) parts.push(`budget=${opts.tokenBudget}`);
53
+ if (opts.tokensUsed) parts.push(`tokens=${opts.tokensUsed}`);
54
+ if (opts.packRoot) parts.push(`pack_root=${opts.packRoot}`);
55
+ if (opts.session) parts.push('session=true');
56
+ this.w.write(parts.join(' ') + '\n');
57
+ }
58
+
59
+ /**
60
+ * Emit a symbol line immediately. Group headers are emitted automatically
61
+ * when the distance changes.
62
+ */
63
+ writeSymbol(s: Symbol): void {
64
+ const groupNames = ['targets', 'related', 'extended'];
65
+ const groupName = s.distance < groupNames.length
66
+ ? groupNames[s.distance]
67
+ : `distance_${s.distance}`;
68
+
69
+ if (groupName !== this.currentGroup) {
70
+ this.w.write(`## ${groupName}\n`);
71
+ this.currentGroup = groupName;
72
+ }
73
+
74
+ const id = this.nextID++;
75
+ this.symIndex.set(s.qualifiedName, id);
76
+
77
+ const kind = KIND_ABBREV[s.kind] || s.kind;
78
+ this.w.write(`@${id} ${kind} ${s.qualifiedName} ${s.score.toFixed(2)} ${s.provenance}\n`);
79
+
80
+ this.groupCounts.set(groupName, (this.groupCounts.get(groupName) || 0) + 1);
81
+ }
82
+
83
+ /**
84
+ * Emit an edge line immediately. The edges section header is emitted
85
+ * automatically on the first edge (with [?] deferred count).
86
+ * Source and target must reference previously-written symbols.
87
+ */
88
+ writeEdge(e: Edge): void {
89
+ const srcIdx = this.symIndex.get(e.source);
90
+ const tgtIdx = this.symIndex.get(e.target);
91
+ if (srcIdx === undefined || tgtIdx === undefined) return;
92
+
93
+ if (!this.edgesStarted) {
94
+ this.w.write('## edges [?]\n');
95
+ this.edgesStarted = true;
96
+ }
97
+
98
+ let line = `@${tgtIdx}<@${srcIdx} ${e.edgeType}`;
99
+ if (e.status && e.status !== 'unchanged') {
100
+ line += ` ${e.status}`;
101
+ }
102
+ this.w.write(line + '\n');
103
+ this.edgeCount++;
104
+ }
105
+
106
+ /**
107
+ * Emit a bare reference for a previously-transmitted symbol (session mode).
108
+ */
109
+ writeBareRef(qname: string, distance: number): void {
110
+ const groupNames = ['targets', 'related', 'extended'];
111
+ const groupName = distance < groupNames.length
112
+ ? groupNames[distance]
113
+ : `distance_${distance}`;
114
+
115
+ if (groupName !== this.currentGroup) {
116
+ this.w.write(`## ${groupName}\n`);
117
+ this.currentGroup = groupName;
118
+ }
119
+
120
+ const id = this.nextID++;
121
+ this.symIndex.set(qname, id);
122
+ this.w.write(`@${id} # previously transmitted\n`);
123
+
124
+ this.groupCounts.set(groupName, (this.groupCounts.get(groupName) || 0) + 1);
125
+ }
126
+
127
+ /**
128
+ * Emit the ## _summary trailer with final counts. Must be called after all
129
+ * symbols and edges have been written.
130
+ */
131
+ close(): void {
132
+ const sections: string[] = [];
133
+ const groupOrder = ['targets', 'related', 'extended'];
134
+
135
+ for (const g of groupOrder) {
136
+ const c = this.groupCounts.get(g);
137
+ if (c && c > 0) sections.push(`${g}:${c}`);
138
+ }
139
+ for (const [g, c] of this.groupCounts) {
140
+ if (!groupOrder.includes(g) && c > 0) sections.push(`${g}:${c}`);
141
+ }
142
+ if (this.edgeCount > 0) {
143
+ sections.push(`edges:${this.edgeCount}`);
144
+ }
145
+
146
+ this.w.write(`## _summary symbols=${this.nextID} edges=${this.edgeCount} sections=${sections.join(',')}\n`);
147
+ }
148
+
149
+ /** Number of symbols written so far. */
150
+ get symbolCount(): number { return this.nextID; }
151
+
152
+ /** Number of edges written so far. */
153
+ get edgeCount_(): number { return this.edgeCount; }
154
+ }