Skip to content

Commit 201f154

Browse files
fix: spec v3.1 conformance (tool optional, comma quoting, attachment fixes)
- tool field optional in graph profile header (v3.1) - Quote strings containing commas in needs_quote - Decode v2-format indented attachments in tabular rows - Reject duplicate attachments on the same row
1 parent d27ad06 commit 201f154

4 files changed

Lines changed: 35 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## v2.1.0 (2026-06-14)
4+
5+
### Spec v3.1
6+
7+
- `tool` field in graph profile header is now optional (SHOULD be present for MCP, not required)
8+
9+
### Bug Fixes
10+
11+
- Quote strings containing commas (conformance: `inline-schema/006_inline_with_quoted_values`)
12+
- Decode v2-format indented attachments in tabular rows (conformance: `decode/002_attachment`)
13+
- Reject duplicate attachments on the same row (conformance: `errors-v2/027_duplicate_attachment`)
14+
315
## v2.0.0 (2026-06-12)
416

517
### Breaking Changes

src/gcf/decode.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ def decode(input_text: str) -> Payload:
3434
raise DecodeError(f"invalid header, expected 'GCF ...' got {header!r}")
3535
_parse_header(header[4:], p)
3636

37-
if not p.tool:
38-
raise DecodeError("missing_tool: header missing required 'tool' field")
37+
# v3.1: tool field is optional (SHOULD be present for MCP tool responses, not required).
3938

4039
# Detect delta mode.
4140
is_delta = "delta=true" in header

src/gcf/decode_generic.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,10 @@ def _parse_tabular_body(
442442
if a_content is None:
443443
break
444444

445+
# Handle v2 indented attachments: strip one extra indent level.
446+
if not a_content.startswith(".") and a_content.startswith(" ."):
447+
a_content = a_content[2:]
448+
445449
# Line starts with ".": traditional or prefixed inline.
446450
if a_content.startswith("."):
447451
rest = a_content[1:]
@@ -459,6 +463,8 @@ def _parse_tabular_body(
459463
p = parse_scalar(inline_vals[k], tabular_context=True)
460464
if p is not MISSING:
461465
obj[inf] = p
466+
if att_name in attachment_values:
467+
raise ValueError(f"duplicate_attachment: {att_name}")
462468
attachment_values[att_name] = obj
463469
i += 1
464470
continue
@@ -469,6 +475,8 @@ def _parse_tabular_body(
469475
)
470476
if not rows and parsed_fields:
471477
shared_array_schemas[att_name_t] = parsed_fields
478+
if att_name_t in attachment_values:
479+
raise ValueError(f"duplicate_attachment: {att_name_t}")
472480
attachment_values[att_name_t] = att_val
473481
i += consumed
474482
continue
@@ -503,6 +511,19 @@ def _parse_tabular_body(
503511
if f not in attachment_values:
504512
raise ValueError(f"missing_attachment: {f}")
505513

514+
# Check for extra attachment lines after all fields resolved (duplicate).
515+
if i < len(lines):
516+
extra_line = lines[i]
517+
extra_content = ""
518+
if depth == 0 or extra_line.startswith(ind):
519+
extra_content = extra_line[len(ind):] if depth > 0 else extra_line
520+
if not extra_content.startswith(".") and extra_content.startswith(" ."):
521+
extra_content = extra_content[2:]
522+
if extra_content.startswith("."):
523+
extra_name, _ = _parse_attachment_name(extra_content[1:])
524+
if extra_name in attachment_values:
525+
raise ValueError(f"duplicate_attachment: {extra_name}")
526+
506527
if not row_has_id or not all_att_fields:
507528
att_indent = ind + " "
508529
if i < len(lines) and lines[i].startswith(att_indent):

src/gcf/scalar.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def needs_quote(s: str) -> bool:
3838
return True
3939
for c in s:
4040
o = ord(c)
41-
if c in ('"', "\\", "|") or o < 0x20 or c in ("\n", "\r"):
41+
if c in ('"', "\\", "|", ",") or o < 0x20 or c in ("\n", "\r"):
4242
return True
4343
# C1 control characters
4444
if 0x80 <= o <= 0x9F:

0 commit comments

Comments
 (0)