@@ -7,58 +7,67 @@ function indent(depth: number): string {
77 return ' ' . repeat ( depth ) ;
88}
99
10- export function encodeGeneric ( data : unknown ) : string {
10+ /** Options for controlling generic encoding behavior. */
11+ export interface GenericOptions {
12+ /** When true, disables promotion of fixed-shape nested objects to path
13+ * columns (e.g. "customer>name"). Nested objects use attachment syntax
14+ * instead. Set when targeting open-weight models that show lower
15+ * comprehension on flattened encoding. */
16+ noFlatten ?: boolean ;
17+ }
18+
19+ export function encodeGeneric ( data : unknown , opts ?: GenericOptions ) : string {
1120 let out = 'GCF profile=generic\n' ;
12- out += encodeRootValue ( data ) ;
21+ out += encodeRootValue ( data , opts ) ;
1322 return out ;
1423}
1524
16- function encodeRootValue ( v : unknown ) : string {
25+ function encodeRootValue ( v : unknown , opts ?: GenericOptions ) : string {
1726 if ( v === null || v === undefined ) return '=-\n' ;
18- if ( Array . isArray ( v ) ) return encodeRootArray ( v ) ;
19- if ( typeof v === 'object' ) return encodeObject ( v as Record < string , unknown > , 0 ) ;
27+ if ( Array . isArray ( v ) ) return encodeRootArray ( v , opts ) ;
28+ if ( typeof v === 'object' ) return encodeObject ( v as Record < string , unknown > , 0 , opts ) ;
2029 return `=${ formatScalar ( v , 0 ) } \n` ;
2130}
2231
23- function encodeObject ( obj : Record < string , unknown > , depth : number ) : string {
32+ function encodeObject ( obj : Record < string , unknown > , depth : number , opts ?: GenericOptions ) : string {
2433 const prefix = indent ( depth ) ;
2534 let out = '' ;
2635 for ( const key of Object . keys ( obj ) ) {
2736 const value = obj [ key ] ;
2837 const fk = formatKey ( key ) ;
2938 if ( Array . isArray ( value ) ) {
30- out += encodeNamedArray ( fk , value , depth ) ;
39+ out += encodeNamedArray ( fk , value , depth , opts ) ;
3140 } else if ( typeof value === 'object' && value !== null ) {
3241 out += `${ prefix } ## ${ fk } \n` ;
33- out += encodeObject ( value as Record < string , unknown > , depth + 1 ) ;
42+ out += encodeObject ( value as Record < string , unknown > , depth + 1 , opts ) ;
3443 } else {
3544 out += `${ prefix } ${ fk } =${ formatScalar ( value , 0 ) } \n` ;
3645 }
3746 }
3847 return out ;
3948}
4049
41- function encodeRootArray ( arr : unknown [ ] ) : string {
50+ function encodeRootArray ( arr : unknown [ ] , opts ?: GenericOptions ) : string {
4251 if ( arr . length === 0 ) return '## [0]\n' ;
4352 if ( allPrimitives ( arr ) ) {
4453 const vals = arr . map ( v => formatScalar ( v , 0x2c ) ) ;
4554 return `## [${ arr . length } ]: ${ vals . join ( ',' ) } \n` ;
4655 }
4756 const fields = tabularFields ( arr ) ;
48- if ( fields ) return encodeTabular ( '## ' , arr , fields , 0 ) ;
49- return encodeExpanded ( '## ' , arr , 0 ) ;
57+ if ( fields ) return encodeTabular ( '## ' , arr , fields , 0 , opts ) ;
58+ return encodeExpanded ( '## ' , arr , 0 , opts ) ;
5059}
5160
52- function encodeNamedArray ( name : string , arr : unknown [ ] , depth : number ) : string {
61+ function encodeNamedArray ( name : string , arr : unknown [ ] , depth : number , opts ?: GenericOptions ) : string {
5362 const prefix = indent ( depth ) ;
5463 if ( arr . length === 0 ) return `${ prefix } ## ${ name } [0]\n` ;
5564 if ( allPrimitives ( arr ) ) {
5665 const vals = arr . map ( v => formatScalar ( v , 0x2c ) ) ;
5766 return `${ prefix } ${ name } [${ arr . length } ]: ${ vals . join ( ',' ) } \n` ;
5867 }
5968 const fields = tabularFields ( arr ) ;
60- if ( fields ) return encodeTabular ( `${ prefix } ## ${ name } ` , arr , fields , depth ) ;
61- return encodeExpanded ( `${ prefix } ## ${ name } ` , arr , depth ) ;
69+ if ( fields ) return encodeTabular ( `${ prefix } ## ${ name } ` , arr , fields , depth , opts ) ;
70+ return encodeExpanded ( `${ prefix } ## ${ name } ` , arr , depth , opts ) ;
6271}
6372
6473function tabularFields ( arr : unknown [ ] ) : string [ ] | null {
@@ -149,6 +158,8 @@ interface FlatLeaf {
149158}
150159
151160function analyzeFlattenable ( arr : unknown [ ] , fieldName : string , parentPath : string ) : FlatLeaf [ ] | null {
161+ // Field names containing ">" cannot be flattened (would create ambiguous paths).
162+ if ( fieldName . includes ( '>' ) ) return null ;
152163 let canonicalShape : Record < string , 'scalar' | 'nested' > | null = null ;
153164
154165 for ( const item of arr ) {
@@ -244,15 +255,27 @@ function resolveKeyChain(item: unknown, keys: string[]): { value: unknown; exist
244255
245256// ── End flattening helpers ───────────────────────────────────────────────
246257
247- function encodeTabular ( headerPrefix : string , arr : unknown [ ] , fields : string [ ] , depth : number ) : string {
258+ function encodeTabular ( headerPrefix : string , arr : unknown [ ] , fields : string [ ] , depth : number , opts ?: GenericOptions ) : string {
248259 const prefix = indent ( depth ) ;
249260
250261 // Phase 0: Analyze fields for flattening.
251262 const flattenMap = new Map < string , FlatLeaf [ ] > ( ) ;
263+ if ( ! opts ?. noFlatten ) {
264+ for ( const f of fields ) {
265+ const leaves = analyzeFlattenable ( arr , f , '' ) ;
266+ if ( leaves && leaves . length > 0 ) {
267+ flattenMap . set ( f , leaves ) ;
268+ }
269+ }
270+ }
271+
272+ // Fields whose names contain ">" must not appear as tabular columns
273+ // because the decoder would interpret them as flattened path columns.
274+ // Track them for per-row attachment emission (spec rule 7.4.6.1.4).
275+ const gtFields = new Set < string > ( ) ;
252276 for ( const f of fields ) {
253- const leaves = analyzeFlattenable ( arr , f , '' ) ;
254- if ( leaves && leaves . length > 0 ) {
255- flattenMap . set ( f , leaves ) ;
277+ if ( ! flattenMap . has ( f ) && f . includes ( '>' ) ) {
278+ gtFields . add ( f ) ;
256279 }
257280 }
258281
@@ -261,6 +284,7 @@ function encodeTabular(headerPrefix: string, arr: unknown[], fields: string[], d
261284 interface FlatColumn { headerName : string ; colType : ColType ; field : string ; keys : string [ ] ; }
262285 const columns : FlatColumn [ ] = [ ] ;
263286 for ( const f of fields ) {
287+ if ( gtFields . has ( f ) ) continue ;
264288 const leaves = flattenMap . get ( f ) ;
265289 if ( leaves ) {
266290 for ( const leaf of leaves ) {
@@ -271,6 +295,11 @@ function encodeTabular(headerPrefix: string, arr: unknown[], fields: string[], d
271295 }
272296 }
273297
298+ // If all fields were excluded (all contain ">"), fall back to expanded.
299+ if ( columns . length === 0 ) {
300+ return encodeExpanded ( headerPrefix , arr , depth , opts ) ;
301+ }
302+
274303 // Pre-compute inline schemas and shared array schemas (skip flattened fields).
275304 const inlineSchemas = new Map < string , string [ ] > ( ) ;
276305 const sharedArrSchemas = new Map < string , string [ ] > ( ) ;
@@ -340,6 +369,15 @@ function encodeTabular(headerPrefix: string, arr: unknown[], fields: string[], d
340369 }
341370 }
342371
372+ // Emit fields with ">" in their names as per-row attachments.
373+ for ( const f of fields ) {
374+ if ( ! gtFields . has ( f ) ) continue ;
375+ const obj = arr [ i ] as Record < string , unknown > ;
376+ if ( ! ( f in obj ) ) continue ;
377+ rowHasAttachment = true ;
378+ attachments . push ( { name : f , value : obj [ f ] , inline : false } ) ;
379+ }
380+
343381 const row = cells . join ( '|' ) ;
344382 if ( rowHasAttachment ) {
345383 out += `${ prefix } @${ i } ${ row } \n` ;
@@ -361,31 +399,38 @@ function encodeTabular(headerPrefix: string, arr: unknown[], fields: string[], d
361399 // Shared array schema: omit {fields} on subsequent rows.
362400 const sas = sharedArrSchemas . get ( att . name ) ;
363401 if ( sas && i > 0 ) {
364- out += encodeAttachmentArrayShared ( prefix , fk , att . value as unknown [ ] , depth + 2 , sas ) ;
402+ out += encodeAttachmentArrayShared ( prefix , fk , att . value as unknown [ ] , depth + 2 , sas , opts ) ;
365403 } else {
366- out += encodeAttachmentArray ( prefix , fk , att . value as unknown [ ] , depth + 2 ) ;
404+ out += encodeAttachmentArray ( prefix , fk , att . value as unknown [ ] , depth + 2 , opts ) ;
367405 }
368- } else {
406+ } else if ( typeof att . value === 'object' && att . value !== null ) {
369407 out += `${ prefix } .${ fk } {}\n` ;
370- out += encodeObject ( att . value as Record < string , unknown > , depth + 2 ) ;
408+ out += encodeObject ( att . value as Record < string , unknown > , depth + 2 , opts ) ;
409+ } else {
410+ // Scalar attachment (e.g. field names containing ">").
411+ if ( att . value === null || att . value === undefined ) {
412+ out += `${ prefix } .${ fk } =-\n` ;
413+ } else {
414+ out += `${ prefix } .${ fk } =${ formatScalar ( att . value , 0 ) } \n` ;
415+ }
371416 }
372417 }
373418 }
374419 return out ;
375420}
376421
377- function encodeAttachmentArray ( attPrefix : string , fk : string , arr : unknown [ ] , depth : number ) : string {
422+ function encodeAttachmentArray ( attPrefix : string , fk : string , arr : unknown [ ] , depth : number , opts ?: GenericOptions ) : string {
378423 if ( arr . length === 0 ) return `${ attPrefix } .${ fk } [0]\n` ;
379424 if ( allPrimitives ( arr ) ) {
380425 const vals = arr . map ( v => formatScalar ( v , 0x2c ) ) ;
381426 return `${ attPrefix } .${ fk } [${ arr . length } ]: ${ vals . join ( ',' ) } \n` ;
382427 }
383428 const fields = tabularFields ( arr ) ;
384- if ( fields ) return encodeTabular ( `${ attPrefix } .${ fk } ` , arr , fields , depth ) ;
385- return encodeExpanded ( `${ attPrefix } .${ fk } ` , arr , depth ) ;
429+ if ( fields ) return encodeTabular ( `${ attPrefix } .${ fk } ` , arr , fields , depth , opts ) ;
430+ return encodeExpanded ( `${ attPrefix } .${ fk } ` , arr , depth , opts ) ;
386431}
387432
388- function encodeAttachmentArrayShared ( attPrefix : string , fk : string , arr : unknown [ ] , depth : number , sharedFields : string [ ] ) : string {
433+ function encodeAttachmentArrayShared ( attPrefix : string , fk : string , arr : unknown [ ] , depth : number , sharedFields : string [ ] , opts ?: GenericOptions ) : string {
389434 if ( arr . length === 0 ) return `${ attPrefix } .${ fk } [0]\n` ;
390435 if ( allPrimitives ( arr ) ) {
391436 const vals = arr . map ( v => formatScalar ( v , 0x2c ) ) ;
@@ -409,35 +454,35 @@ function encodeAttachmentArrayShared(attPrefix: string, fk: string, arr: unknown
409454 return out ;
410455 }
411456 // Fields don't match: fall back to full encoding.
412- return encodeAttachmentArray ( attPrefix , fk , arr , depth ) ;
457+ return encodeAttachmentArray ( attPrefix , fk , arr , depth , opts ) ;
413458}
414459
415- function encodeExpanded ( headerPrefix : string , arr : unknown [ ] , depth : number ) : string {
460+ function encodeExpanded ( headerPrefix : string , arr : unknown [ ] , depth : number , opts ?: GenericOptions ) : string {
416461 const prefix = indent ( depth ) ;
417462 let out = `${ headerPrefix } [${ arr . length } ]\n` ;
418463 for ( let i = 0 ; i < arr . length ; i ++ ) {
419464 const item = arr [ i ] ;
420465 if ( Array . isArray ( item ) ) {
421- out += encodeExpandedArrayItem ( prefix , i , item , depth ) ;
466+ out += encodeExpandedArrayItem ( prefix , i , item , depth , opts ) ;
422467 } else if ( typeof item === 'object' && item !== null ) {
423468 out += `${ prefix } @${ i } {}\n` ;
424- out += encodeObject ( item as Record < string , unknown > , depth + 1 ) ;
469+ out += encodeObject ( item as Record < string , unknown > , depth + 1 , opts ) ;
425470 } else {
426471 out += `${ prefix } @${ i } =${ formatScalar ( item , 0 ) } \n` ;
427472 }
428473 }
429474 return out ;
430475}
431476
432- function encodeExpandedArrayItem ( prefix : string , idx : number , arr : unknown [ ] , depth : number ) : string {
477+ function encodeExpandedArrayItem ( prefix : string , idx : number , arr : unknown [ ] , depth : number , opts ?: GenericOptions ) : string {
433478 if ( arr . length === 0 ) return `${ prefix } @${ idx } [0]\n` ;
434479 if ( allPrimitives ( arr ) ) {
435480 const vals = arr . map ( v => formatScalar ( v , 0x2c ) ) ;
436481 return `${ prefix } @${ idx } [${ arr . length } ]: ${ vals . join ( ',' ) } \n` ;
437482 }
438483 const fields = tabularFields ( arr ) ;
439- if ( fields ) return encodeTabular ( `${ prefix } @${ idx } ` , arr , fields , depth + 1 ) ;
440- return encodeExpanded ( `${ prefix } @${ idx } ` , arr , depth + 1 ) ;
484+ if ( fields ) return encodeTabular ( `${ prefix } @${ idx } ` , arr , fields , depth + 1 , opts ) ;
485+ return encodeExpanded ( `${ prefix } @${ idx } ` , arr , depth + 1 , opts ) ;
441486}
442487
443488function allPrimitives ( arr : unknown [ ] ) : boolean {
0 commit comments