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