@blackwell-systems/gcf 0.1.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 +21 -0
- package/README.md +177 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +122 -0
- package/dist/cli.js.map +1 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +43 -0
- package/dist/constants.js.map +1 -0
- package/dist/decode.d.ts +6 -0
- package/dist/decode.d.ts.map +1 -0
- package/dist/decode.js +179 -0
- package/dist/decode.js.map +1 -0
- package/dist/delta.d.ts +6 -0
- package/dist/delta.d.ts.map +1 -0
- package/dist/delta.js +46 -0
- package/dist/delta.js.map +1 -0
- package/dist/encode.d.ts +6 -0
- package/dist/encode.d.ts.map +1 -0
- package/dist/encode.js +69 -0
- package/dist/encode.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/session.d.ts +31 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +127 -0
- package/dist/session.js.map +1 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +51 -0
- package/src/cli.ts +136 -0
- package/src/constants.ts +43 -0
- package/src/decode.ts +208 -0
- package/src/delta.ts +55 -0
- package/src/encode.ts +84 -0
- package/src/index.ts +6 -0
- package/src/session.ts +147 -0
- package/src/types.ts +79 -0
package/src/decode.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { Edge, Payload, Symbol } from './types.js';
|
|
2
|
+
import { KIND_EXPAND } from './constants.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Decode parses GCF text back into a Payload.
|
|
6
|
+
*/
|
|
7
|
+
export function decode(input: string): Payload {
|
|
8
|
+
const lines = input.split('\n');
|
|
9
|
+
if (lines.length === 0) {
|
|
10
|
+
throw new Error('gcf: empty input');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const header = lines[0];
|
|
14
|
+
if (!header.startsWith('GCF ')) {
|
|
15
|
+
throw new Error(`gcf: invalid header, expected 'GCF ...' got "${header}"`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const p: Payload = {
|
|
19
|
+
tool: '',
|
|
20
|
+
tokenBudget: 0,
|
|
21
|
+
tokensUsed: 0,
|
|
22
|
+
symbols: [],
|
|
23
|
+
edges: [],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Parse header fields.
|
|
27
|
+
parseHeader(header.slice(4), p);
|
|
28
|
+
|
|
29
|
+
// Parse body: symbols and edges.
|
|
30
|
+
const symbols: Symbol[] = [];
|
|
31
|
+
const symByID = new Map<number, Symbol>();
|
|
32
|
+
let currentDistance = 0;
|
|
33
|
+
let inEdges = false;
|
|
34
|
+
|
|
35
|
+
for (let i = 1; i < lines.length; i++) {
|
|
36
|
+
let line = lines[i].replace(/\r$/, '');
|
|
37
|
+
if (line === '') continue;
|
|
38
|
+
|
|
39
|
+
// Group header.
|
|
40
|
+
if (line.startsWith('## ')) {
|
|
41
|
+
const group = line.slice(3);
|
|
42
|
+
inEdges = group === 'edges';
|
|
43
|
+
if (!inEdges) {
|
|
44
|
+
switch (group) {
|
|
45
|
+
case 'targets':
|
|
46
|
+
currentDistance = 0;
|
|
47
|
+
break;
|
|
48
|
+
case 'related':
|
|
49
|
+
currentDistance = 1;
|
|
50
|
+
break;
|
|
51
|
+
case 'extended':
|
|
52
|
+
currentDistance = 2;
|
|
53
|
+
break;
|
|
54
|
+
default:
|
|
55
|
+
if (group.startsWith('distance_')) {
|
|
56
|
+
const d = parseInt(group.slice(9), 10);
|
|
57
|
+
if (!isNaN(d)) {
|
|
58
|
+
currentDistance = d;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Comment.
|
|
68
|
+
if (line.startsWith('# ')) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (inEdges) {
|
|
73
|
+
const edge = parseEdgeLine(line, symByID);
|
|
74
|
+
p.edges.push(edge);
|
|
75
|
+
} else {
|
|
76
|
+
const { symbol, id } = parseSymbolLine(line, currentDistance);
|
|
77
|
+
symbols.push(symbol);
|
|
78
|
+
symByID.set(id, symbol);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
p.symbols = symbols;
|
|
83
|
+
return p;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseHeader(fields: string, p: Payload): void {
|
|
87
|
+
const parts = fields.split(/\s+/);
|
|
88
|
+
for (const part of parts) {
|
|
89
|
+
const eqIdx = part.indexOf('=');
|
|
90
|
+
if (eqIdx < 0) continue;
|
|
91
|
+
const key = part.slice(0, eqIdx);
|
|
92
|
+
const value = part.slice(eqIdx + 1);
|
|
93
|
+
|
|
94
|
+
switch (key) {
|
|
95
|
+
case 'tool':
|
|
96
|
+
p.tool = value;
|
|
97
|
+
break;
|
|
98
|
+
case 'budget': {
|
|
99
|
+
const v = parseInt(value, 10);
|
|
100
|
+
if (isNaN(v)) throw new Error(`gcf: invalid budget "${value}"`);
|
|
101
|
+
p.tokenBudget = v;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case 'tokens': {
|
|
105
|
+
const v = parseInt(value, 10);
|
|
106
|
+
if (isNaN(v)) throw new Error(`gcf: invalid tokens "${value}"`);
|
|
107
|
+
p.tokensUsed = v;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case 'pack_root':
|
|
111
|
+
p.packRoot = value;
|
|
112
|
+
break;
|
|
113
|
+
case 'symbols':
|
|
114
|
+
// Informational, reconstructed from parsed symbols.
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseSymbolLine(
|
|
121
|
+
line: string,
|
|
122
|
+
distance: number
|
|
123
|
+
): { symbol: Symbol; id: number } {
|
|
124
|
+
if (!line.startsWith('@')) {
|
|
125
|
+
throw new Error(`gcf: expected symbol line starting with @, got "${line}"`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const parts = line.split(/\s+/);
|
|
129
|
+
if (parts.length < 5) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`gcf: symbol line needs at least 5 fields, got ${parts.length} in "${line}"`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const idStr = parts[0].slice(1); // strip @
|
|
136
|
+
const id = parseInt(idStr, 10);
|
|
137
|
+
if (isNaN(id)) {
|
|
138
|
+
throw new Error(`gcf: invalid symbol id "${idStr}"`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let kind = parts[1];
|
|
142
|
+
if (KIND_EXPAND[kind]) {
|
|
143
|
+
kind = KIND_EXPAND[kind];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const qname = parts[2];
|
|
147
|
+
|
|
148
|
+
const score = parseFloat(parts[3]);
|
|
149
|
+
if (isNaN(score)) {
|
|
150
|
+
throw new Error(`gcf: invalid score "${parts[3]}"`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const provenance = parts[4];
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
symbol: {
|
|
157
|
+
qualifiedName: qname,
|
|
158
|
+
kind,
|
|
159
|
+
score,
|
|
160
|
+
provenance,
|
|
161
|
+
distance,
|
|
162
|
+
},
|
|
163
|
+
id,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseEdgeLine(line: string, symByID: Map<number, Symbol>): Edge {
|
|
168
|
+
const parts = line.split(/\s+/);
|
|
169
|
+
if (parts.length < 2) {
|
|
170
|
+
throw new Error(`gcf: edge line needs at least 2 fields, got "${line}"`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const ref = parts[0];
|
|
174
|
+
const ltIdx = ref.indexOf('<');
|
|
175
|
+
if (ltIdx < 0) {
|
|
176
|
+
throw new Error(`gcf: edge line missing '<' separator in "${ref}"`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const targetIDStr = ref.slice(1, ltIdx); // strip leading @
|
|
180
|
+
const sourceIDStr = ref.slice(ltIdx + 2); // strip <@
|
|
181
|
+
|
|
182
|
+
const targetID = parseInt(targetIDStr, 10);
|
|
183
|
+
if (isNaN(targetID)) {
|
|
184
|
+
throw new Error(`gcf: invalid target id "${targetIDStr}"`);
|
|
185
|
+
}
|
|
186
|
+
const sourceID = parseInt(sourceIDStr, 10);
|
|
187
|
+
if (isNaN(sourceID)) {
|
|
188
|
+
throw new Error(`gcf: invalid source id "${sourceIDStr}"`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const targetSym = symByID.get(targetID);
|
|
192
|
+
const sourceSym = symByID.get(sourceID);
|
|
193
|
+
if (!targetSym || !sourceSym) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`gcf: edge references unknown symbol id(s): target=${targetID} source=${sourceID}`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const edgeType = parts[1];
|
|
200
|
+
const status = parts.length >= 3 ? parts[2] : undefined;
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
source: sourceSym.qualifiedName,
|
|
204
|
+
target: targetSym.qualifiedName,
|
|
205
|
+
edgeType,
|
|
206
|
+
status,
|
|
207
|
+
};
|
|
208
|
+
}
|
package/src/delta.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { DeltaPayload } from './types.js';
|
|
2
|
+
import { KIND_ABBREV } from './constants.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* EncodeDelta serializes a DeltaPayload into GCF delta format.
|
|
6
|
+
*/
|
|
7
|
+
export function encodeDelta(d: DeltaPayload): string {
|
|
8
|
+
const lines: string[] = [];
|
|
9
|
+
|
|
10
|
+
// Header.
|
|
11
|
+
let savings = 0;
|
|
12
|
+
if (d.fullTokens > 0) {
|
|
13
|
+
savings = Math.round(100 * (1 - d.deltaTokens / d.fullTokens));
|
|
14
|
+
}
|
|
15
|
+
lines.push(
|
|
16
|
+
`GCF tool=${d.tool} delta=true base_root=${d.baseRoot} new_root=${d.newRoot} tokens=${d.deltaTokens} savings=${savings}%`
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// Removed symbols: short references (consumer already has the full declaration).
|
|
20
|
+
if (d.removed.length > 0) {
|
|
21
|
+
lines.push('## removed');
|
|
22
|
+
for (const s of d.removed) {
|
|
23
|
+
const kind = KIND_ABBREV[s.kind] || s.kind;
|
|
24
|
+
lines.push(`${kind} ${s.qualifiedName}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Added symbols: full declarations (consumer doesn't have these).
|
|
29
|
+
if (d.added.length > 0) {
|
|
30
|
+
lines.push('## added');
|
|
31
|
+
for (let i = 0; i < d.added.length; i++) {
|
|
32
|
+
const s = d.added[i];
|
|
33
|
+
const kind = KIND_ABBREV[s.kind] || s.kind;
|
|
34
|
+
lines.push(`@${i} ${kind} ${s.qualifiedName} ${s.score.toFixed(2)} ${s.provenance}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Removed edges.
|
|
39
|
+
if (d.removedEdges.length > 0) {
|
|
40
|
+
lines.push('## edges_removed');
|
|
41
|
+
for (const e of d.removedEdges) {
|
|
42
|
+
lines.push(`${e.source} -> ${e.target} ${e.edgeType}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Added edges.
|
|
47
|
+
if (d.addedEdges.length > 0) {
|
|
48
|
+
lines.push('## edges_added');
|
|
49
|
+
for (const e of d.addedEdges) {
|
|
50
|
+
lines.push(`${e.source} -> ${e.target} ${e.edgeType}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return lines.join('\n') + '\n';
|
|
55
|
+
}
|
package/src/encode.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Payload, Symbol } from './types.js';
|
|
2
|
+
import { KIND_ABBREV } from './constants.js';
|
|
3
|
+
|
|
4
|
+
interface DistanceGroup {
|
|
5
|
+
distance: number;
|
|
6
|
+
symbols: Symbol[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function groupByDistance(symbols: Symbol[]): DistanceGroup[] {
|
|
10
|
+
if (symbols.length === 0) return [];
|
|
11
|
+
|
|
12
|
+
const groups: DistanceGroup[] = [];
|
|
13
|
+
let current: DistanceGroup | null = null;
|
|
14
|
+
|
|
15
|
+
for (const s of symbols) {
|
|
16
|
+
if (current === null || current.distance !== s.distance) {
|
|
17
|
+
current = { distance: s.distance, symbols: [] };
|
|
18
|
+
groups.push(current);
|
|
19
|
+
}
|
|
20
|
+
current.symbols.push(s);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return groups;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Encode serializes a Payload into GCF text format.
|
|
28
|
+
*/
|
|
29
|
+
export function encode(p: Payload): string {
|
|
30
|
+
const lines: string[] = [];
|
|
31
|
+
|
|
32
|
+
// Header line.
|
|
33
|
+
let header = `GCF tool=${p.tool} budget=${p.tokenBudget} tokens=${p.tokensUsed} symbols=${p.symbols.length}`;
|
|
34
|
+
if (p.packRoot) {
|
|
35
|
+
header += ` pack_root=${p.packRoot}`;
|
|
36
|
+
}
|
|
37
|
+
lines.push(header);
|
|
38
|
+
|
|
39
|
+
// Build symbol index for edge references.
|
|
40
|
+
const symIndex = new Map<string, number>();
|
|
41
|
+
for (let i = 0; i < p.symbols.length; i++) {
|
|
42
|
+
symIndex.set(p.symbols[i].qualifiedName, i);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Group symbols by distance.
|
|
46
|
+
const groups = groupByDistance(p.symbols);
|
|
47
|
+
const groupNames = ['targets', 'related', 'extended'];
|
|
48
|
+
|
|
49
|
+
for (const g of groups) {
|
|
50
|
+
if (g.symbols.length === 0) continue;
|
|
51
|
+
|
|
52
|
+
let name: string;
|
|
53
|
+
if (g.distance < groupNames.length) {
|
|
54
|
+
name = groupNames[g.distance];
|
|
55
|
+
} else {
|
|
56
|
+
name = `distance_${g.distance}`;
|
|
57
|
+
}
|
|
58
|
+
lines.push(`## ${name}`);
|
|
59
|
+
|
|
60
|
+
for (const s of g.symbols) {
|
|
61
|
+
const idx = symIndex.get(s.qualifiedName)!;
|
|
62
|
+
const kind = KIND_ABBREV[s.kind] || s.kind;
|
|
63
|
+
lines.push(`@${idx} ${kind} ${s.qualifiedName} ${s.score.toFixed(2)} ${s.provenance}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Edges section.
|
|
68
|
+
if (p.edges.length > 0) {
|
|
69
|
+
lines.push('## edges');
|
|
70
|
+
for (const e of p.edges) {
|
|
71
|
+
const srcIdx = symIndex.get(e.source);
|
|
72
|
+
const tgtIdx = symIndex.get(e.target);
|
|
73
|
+
if (srcIdx === undefined || tgtIdx === undefined) continue;
|
|
74
|
+
|
|
75
|
+
let line = `@${tgtIdx}<@${srcIdx} ${e.edgeType}`;
|
|
76
|
+
if (e.status && e.status !== 'unchanged') {
|
|
77
|
+
line += ` ${e.status}`;
|
|
78
|
+
}
|
|
79
|
+
lines.push(line);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return lines.join('\n') + '\n';
|
|
84
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { Symbol, Edge, Payload, DeltaPayload, Components } from './types.js';
|
|
2
|
+
export { KIND_ABBREV, KIND_EXPAND } from './constants.js';
|
|
3
|
+
export { encode } from './encode.js';
|
|
4
|
+
export { decode } from './decode.js';
|
|
5
|
+
export { Session, encodeWithSession } from './session.js';
|
|
6
|
+
export { encodeDelta } from './delta.js';
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { Payload, Symbol } from './types.js';
|
|
2
|
+
import { KIND_ABBREV } from './constants.js';
|
|
3
|
+
import { encode } from './encode.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Session tracks symbols that have been transmitted to a client, enabling
|
|
7
|
+
* subsequent responses to reference them by ID without full retransmission.
|
|
8
|
+
* This makes multi-call workflows progressively cheaper.
|
|
9
|
+
*/
|
|
10
|
+
export class Session {
|
|
11
|
+
private symbols: Map<string, number> = new Map();
|
|
12
|
+
private nextID: number = 0;
|
|
13
|
+
|
|
14
|
+
/** Returns true if the symbol has been sent in a previous response. */
|
|
15
|
+
transmitted(qname: string): boolean {
|
|
16
|
+
return this.symbols.has(qname);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Returns the session-global ID for a previously transmitted symbol, or -1 if not found. */
|
|
20
|
+
getID(qname: string): number {
|
|
21
|
+
const id = this.symbols.get(qname);
|
|
22
|
+
return id !== undefined ? id : -1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Record marks symbols as transmitted and assigns session-global IDs.
|
|
27
|
+
* Call this after a successful encode to register newly-sent symbols.
|
|
28
|
+
*/
|
|
29
|
+
record(symbols: Symbol[]): void {
|
|
30
|
+
for (const sym of symbols) {
|
|
31
|
+
if (!this.symbols.has(sym.qualifiedName)) {
|
|
32
|
+
this.symbols.set(sym.qualifiedName, this.nextID);
|
|
33
|
+
this.nextID++;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Returns the number of symbols tracked in this session. */
|
|
39
|
+
size(): number {
|
|
40
|
+
return this.symbols.size;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Clears the session state. */
|
|
44
|
+
reset(): void {
|
|
45
|
+
this.symbols.clear();
|
|
46
|
+
this.nextID = 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface DistanceGroup {
|
|
51
|
+
distance: number;
|
|
52
|
+
symbols: Symbol[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function groupByDistance(symbols: Symbol[]): DistanceGroup[] {
|
|
56
|
+
if (symbols.length === 0) return [];
|
|
57
|
+
const groups: DistanceGroup[] = [];
|
|
58
|
+
let current: DistanceGroup | null = null;
|
|
59
|
+
for (const s of symbols) {
|
|
60
|
+
if (current === null || current.distance !== s.distance) {
|
|
61
|
+
current = { distance: s.distance, symbols: [] };
|
|
62
|
+
groups.push(current);
|
|
63
|
+
}
|
|
64
|
+
current.symbols.push(s);
|
|
65
|
+
}
|
|
66
|
+
return groups;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Encode a payload using GCF with session deduplication.
|
|
71
|
+
* Symbols that were already transmitted in prior responses are emitted as
|
|
72
|
+
* bare references (`@N # previously transmitted`) instead of full declarations.
|
|
73
|
+
* After encoding, newly-sent symbols are recorded in the session.
|
|
74
|
+
*/
|
|
75
|
+
export function encodeWithSession(p: Payload, sess: Session | null): string {
|
|
76
|
+
if (!sess) {
|
|
77
|
+
return encode(p);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const lines: string[] = [];
|
|
81
|
+
|
|
82
|
+
// Header with session=true marker.
|
|
83
|
+
let header = `GCF tool=${p.tool} budget=${p.tokenBudget} tokens=${p.tokensUsed} symbols=${p.symbols.length} session=true`;
|
|
84
|
+
if (p.packRoot) {
|
|
85
|
+
header += ` pack_root=${p.packRoot}`;
|
|
86
|
+
}
|
|
87
|
+
lines.push(header);
|
|
88
|
+
|
|
89
|
+
// Build local ID mapping for this response.
|
|
90
|
+
const localIndex = new Map<string, number>();
|
|
91
|
+
for (let i = 0; i < p.symbols.length; i++) {
|
|
92
|
+
localIndex.set(p.symbols[i].qualifiedName, i);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Track which symbols are new for recording after encode.
|
|
96
|
+
const newSymbols: Symbol[] = [];
|
|
97
|
+
|
|
98
|
+
// Group by distance.
|
|
99
|
+
const groups = groupByDistance(p.symbols);
|
|
100
|
+
const groupNames = ['targets', 'related', 'extended'];
|
|
101
|
+
|
|
102
|
+
for (const g of groups) {
|
|
103
|
+
if (g.symbols.length === 0) continue;
|
|
104
|
+
|
|
105
|
+
let name: string;
|
|
106
|
+
if (g.distance < groupNames.length) {
|
|
107
|
+
name = groupNames[g.distance];
|
|
108
|
+
} else {
|
|
109
|
+
name = `distance_${g.distance}`;
|
|
110
|
+
}
|
|
111
|
+
lines.push(`## ${name}`);
|
|
112
|
+
|
|
113
|
+
for (const s of g.symbols) {
|
|
114
|
+
const idx = localIndex.get(s.qualifiedName)!;
|
|
115
|
+
if (sess.transmitted(s.qualifiedName)) {
|
|
116
|
+
// Bare reference: symbol was sent in a prior response.
|
|
117
|
+
lines.push(`@${idx} # previously transmitted`);
|
|
118
|
+
} else {
|
|
119
|
+
// Full declaration.
|
|
120
|
+
const kind = KIND_ABBREV[s.kind] || s.kind;
|
|
121
|
+
lines.push(`@${idx} ${kind} ${s.qualifiedName} ${s.score.toFixed(2)} ${s.provenance}`);
|
|
122
|
+
newSymbols.push(s);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Edges section.
|
|
128
|
+
if (p.edges.length > 0) {
|
|
129
|
+
lines.push('## edges');
|
|
130
|
+
for (const e of p.edges) {
|
|
131
|
+
const srcIdx = localIndex.get(e.source);
|
|
132
|
+
const tgtIdx = localIndex.get(e.target);
|
|
133
|
+
if (srcIdx === undefined || tgtIdx === undefined) continue;
|
|
134
|
+
|
|
135
|
+
let line = `@${tgtIdx}<@${srcIdx} ${e.edgeType}`;
|
|
136
|
+
if (e.status && e.status !== 'unchanged') {
|
|
137
|
+
line += ` ${e.status}`;
|
|
138
|
+
}
|
|
139
|
+
lines.push(line);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Record all new symbols in the session.
|
|
144
|
+
sess.record(newSymbols);
|
|
145
|
+
|
|
146
|
+
return lines.join('\n') + '\n';
|
|
147
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol represents a node in a GCF payload.
|
|
3
|
+
*/
|
|
4
|
+
export interface Symbol {
|
|
5
|
+
/** Fully qualified identifier (e.g., "pkg/auth.Middleware") */
|
|
6
|
+
qualifiedName: string;
|
|
7
|
+
/** Node type: "function", "type", "method", etc. */
|
|
8
|
+
kind: string;
|
|
9
|
+
/** Relevance score (0.0 to 1.0) */
|
|
10
|
+
score: number;
|
|
11
|
+
/** Discovery method: "lsp_resolved", "ast_inferred", etc. */
|
|
12
|
+
provenance: string;
|
|
13
|
+
/** Hops from query center (0=target, 1=related, 2+=extended) */
|
|
14
|
+
distance: number;
|
|
15
|
+
/** Optional: function/method signature */
|
|
16
|
+
signature?: string;
|
|
17
|
+
/** Optional: score breakdown */
|
|
18
|
+
components?: Components;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Components holds the score breakdown for a symbol.
|
|
23
|
+
*/
|
|
24
|
+
export interface Components {
|
|
25
|
+
blastRadius: number;
|
|
26
|
+
confidence: number;
|
|
27
|
+
recency: number;
|
|
28
|
+
distance: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Edge represents a directed relationship in a GCF payload.
|
|
33
|
+
*/
|
|
34
|
+
export interface Edge {
|
|
35
|
+
/** Qualified name of source symbol */
|
|
36
|
+
source: string;
|
|
37
|
+
/** Qualified name of target symbol */
|
|
38
|
+
target: string;
|
|
39
|
+
/** Relationship type (e.g., "calls", "imports", "implements") */
|
|
40
|
+
edgeType: string;
|
|
41
|
+
/** Optional: "added", "removed", "unchanged" (for diff responses) */
|
|
42
|
+
status?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Payload is the input/output structure for GCF encoding/decoding.
|
|
47
|
+
*/
|
|
48
|
+
export interface Payload {
|
|
49
|
+
/** Producing tool name (e.g., "context_for_task") */
|
|
50
|
+
tool: string;
|
|
51
|
+
/** Token budget requested by the consumer */
|
|
52
|
+
tokenBudget: number;
|
|
53
|
+
/** Actual tokens consumed by this payload */
|
|
54
|
+
tokensUsed: number;
|
|
55
|
+
/** Content-addressed identity (hex SHA-256), enables delta encoding */
|
|
56
|
+
packRoot?: string;
|
|
57
|
+
/** Ordered by score descending within each distance group */
|
|
58
|
+
symbols: Symbol[];
|
|
59
|
+
/** Directed relationships between symbols */
|
|
60
|
+
edges: Edge[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* DeltaPayload represents the diff between a prior context pack and the
|
|
65
|
+
* current result. Used for incremental context delivery.
|
|
66
|
+
*/
|
|
67
|
+
export interface DeltaPayload {
|
|
68
|
+
tool: string;
|
|
69
|
+
/** pack_root the consumer has */
|
|
70
|
+
baseRoot: string;
|
|
71
|
+
/** pack_root of the current result */
|
|
72
|
+
newRoot: string;
|
|
73
|
+
removed: Symbol[];
|
|
74
|
+
added: Symbol[];
|
|
75
|
+
removedEdges: Edge[];
|
|
76
|
+
addedEdges: Edge[];
|
|
77
|
+
deltaTokens: number;
|
|
78
|
+
fullTokens: number;
|
|
79
|
+
}
|