@@ -35,7 +35,11 @@ def decode(input_text: str) -> Payload:
3535 _parse_header (header [4 :], p )
3636
3737 if not p .tool :
38- raise DecodeError ("header missing required 'tool' field" )
38+ raise DecodeError ("missing_tool: header missing required 'tool' field" )
39+
40+ # Detect delta mode.
41+ is_delta = "delta=true" in header
42+ valid_delta_sections = {"removed" , "added" , "edges_removed" , "edges_added" }
3943
4044 # Parse body: symbols and edges.
4145 symbols : list [Symbol ] = []
@@ -48,13 +52,19 @@ def decode(input_text: str) -> Payload:
4852 if not line :
4953 continue
5054
55+ # Skip ##! summary trailer.
56+ if line .startswith ("##! " ):
57+ continue
58+
5159 # Group header.
5260 if line .startswith ("## " ):
5361 group = line [3 :]
5462 # Strip bracket suffix: "edges [200]" -> "edges"
5563 bracket_idx = group .find (" [" )
5664 if bracket_idx >= 0 :
5765 group = group [:bracket_idx ]
66+ if is_delta and group not in valid_delta_sections :
67+ raise DecodeError (f"malformed_delta: invalid delta section { group !r} " )
5868 in_edges = group == "edges"
5969 if not in_edges :
6070 if group == "targets" :
@@ -113,19 +123,19 @@ def _parse_header(fields: str, p: Payload) -> None:
113123def _parse_symbol_line (line : str , distance : int ) -> tuple [Symbol , int ]:
114124 """Parse a symbol line into a Symbol and its local ID."""
115125 if not line .startswith ("@" ):
116- raise DecodeError (f"expected symbol line starting with @, got { line !r} " )
126+ raise DecodeError (f"invalid_node_line: expected symbol line starting with @, got { line !r} " )
117127
118128 parts = line .split ()
119129 if len (parts ) < 5 :
120130 raise DecodeError (
121- f"symbol line needs at least 5 fields, got { len (parts )} in { line !r} "
131+ f"invalid_node_line: symbol line needs at least 5 fields, got { len (parts )} in { line !r} "
122132 )
123133
124134 id_str = parts [0 ][1 :] # strip @
125135 try :
126136 sym_id = int (id_str )
127137 except ValueError as e :
128- raise DecodeError (f"invalid symbol id { id_str !r} : { e } " ) from e
138+ raise DecodeError (f"invalid_symbol_id: invalid symbol id { id_str !r} : { e } " ) from e
129139
130140 kind = parts [1 ]
131141 kind = KIND_EXPAND .get (kind , kind )
@@ -135,7 +145,7 @@ def _parse_symbol_line(line: str, distance: int) -> tuple[Symbol, int]:
135145 try :
136146 score = float (parts [3 ])
137147 except ValueError as e :
138- raise DecodeError (f"invalid score { parts [3 ]!r} : { e } " ) from e
148+ raise DecodeError (f"invalid_score: invalid score { parts [3 ]!r} : { e } " ) from e
139149
140150 provenance = parts [4 ]
141151
@@ -157,7 +167,7 @@ def _parse_edge_line(line: str, sym_by_id: dict[int, Symbol]) -> Edge:
157167 ref = parts [0 ]
158168 lt_idx = ref .find ("<" )
159169 if lt_idx < 0 :
160- raise DecodeError (f"edge line missing '<' separator in { ref !r} " )
170+ raise DecodeError (f"invalid_edge_syntax: edge line missing '<' separator in { ref !r} " )
161171
162172 target_id_str = ref [1 :lt_idx ] # strip leading @
163173 source_id_str = ref [lt_idx + 2 :] # strip <@
@@ -176,7 +186,7 @@ def _parse_edge_line(line: str, sym_by_id: dict[int, Symbol]) -> Edge:
176186 source_sym = sym_by_id .get (source_id )
177187 if target_sym is None or source_sym is None :
178188 raise DecodeError (
179- f"edge references unknown symbol id(s): target={ target_id } source={ source_id } "
189+ f"unknown_edge_reference: edge references unknown symbol id(s): target={ target_id } source={ source_id } "
180190 )
181191
182192 edge_type = parts [1 ]
0 commit comments