Skip to content

Commit 94c8e96

Browse files
authored
Merge pull request DeusData#1154 from DeusData/feat/toon-round2
Tree-format tool output: prefix-grouped rows, exactly-once pagination, real change impact, cycle detection
2 parents e678b2b + cc5f71b commit 94c8e96

15 files changed

Lines changed: 3378 additions & 714 deletions

‎scripts/smoke-test.sh‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,9 @@ echo "OK: search_graph found $TOTAL result(s) for 'compute'"
195195
if ! TRACE=$(cli trace_path --project "$PROJECT" --function-name compute --direction inbound --depth 1); then
196196
echo "FAIL: trace_path (flag form) exited non-zero"; cat "$CLI_STDERR"; exit 1
197197
fi
198-
# trace_path default output is TOON: the callers[N]{...} header carries the count
199-
CALLERS=$(echo "$TRACE" | sed -n 's/^callers\[\([0-9]*\)\].*/\1/p' | head -1)
198+
# trace_path default output is the tree format: callers_total carries the
199+
# exact reachable count (test files excluded by default)
200+
CALLERS=$(echo "$TRACE" | sed -n 's/^callers_total: \([0-9]*\).*/\1/p' | head -1)
200201
CALLERS=${CALLERS:-0}
201202
if [ "$CALLERS" -lt 1 ]; then
202203
echo "FAIL: trace_path found 0 callers for 'compute'"
@@ -278,7 +279,7 @@ cyp_first_cell() {
278279
# argv token, so string-literal args (e.g. replace(f.name,"a","A")) and Cypher
279280
# metacharacters {}|=~<>" need no JSON escaping.
280281
cli query_graph --project "$PROJECT" --query "$1" |
281-
sed -n '/^rows\[/{n;p;}' | sed 's/^ //' | sed 's/^"//;s/"$//;s/\\"/"/g'
282+
sed -n '/^rows: /{n;p;}' | sed 's/^ //' | sed 's/^"//;s/"$//;s/\\"/"/g'
282283
}
283284

284285
# labels(n) → JSON list like ["Function"]
@@ -366,7 +367,7 @@ LEFTV=$(cyp_first_cell 'MATCH (f:Function) RETURN left(f.name, 3) AS l LIMIT 1')
366367

367368
# NOT EXISTS dead-code query (functions with no caller)
368369
CYPHER_NX=$(cli query_graph --project "$PROJECT" --query "MATCH (f:Function) WHERE NOT EXISTS { (f)<-[:CALLS]-() } RETURN f.name")
369-
NX_OK=$(echo "$CYPHER_NX" | grep -qE '^rows\[[0-9]+\]\{' && echo "True" || echo "False")
370+
NX_OK=$(echo "$CYPHER_NX" | grep -qE '^rows: [0-9]+' && echo "True" || echo "False")
370371
[ "$NX_OK" = "True" ] && echo "OK: query_graph NOT EXISTS dead-code query executed" || { echo "FAIL: NOT EXISTS query"; echo "$CYPHER_NX" | head -c 300; exit 1; }
371372

372373
# CASE expression in RETURN
@@ -390,7 +391,7 @@ if ! ARCH=$(cli get_architecture --project "$PROJECT" --aspects clusters); then
390391
echo "FAIL: get_architecture (flag form) exited non-zero"; cat "$CLI_STDERR"; exit 1
391392
fi
392393
# get_architecture default output is TOON: clusters[N]{...} header carries the count
393-
NCLUST=$(echo "$ARCH" | sed -n 's/^clusters\[\([0-9]*\)\].*/\1/p' | head -1)
394+
NCLUST=$(echo "$ARCH" | sed -n 's/^clusters: \([0-9]*\).*/\1/p' | head -1)
394395
NCLUST=${NCLUST:-0}
395396
if [ "$NCLUST" -lt 1 ]; then
396397
echo "FAIL: get_architecture returned 0 community clusters"; echo "$ARCH" | head -c 400; exit 1
@@ -431,7 +432,7 @@ echo "=== Phase 3h: CLI input-mode guards (flags / stdin / --args-file / --help
431432
assert_json_obj() { python3 -c "import json,sys; d=json.loads(sys.stdin.read()); sys.exit(0 if isinstance(d,dict) else 1)" 2>/dev/null; }
432433
# search_graph emits TOON by default: a results/semantic table header proves
433434
# the tool parsed its typed flags and produced a well-formed response.
434-
assert_toon_table() { grep -qE '^(results|semantic)\[[0-9]+\]\{'; }
435+
assert_toon_table() { grep -qE '^(results|semantic): [0-9]+'; }
435436

436437
# B1: INTEGER flag — --limit is schema-typed integer; must parse and answer.
437438
if ! IM_INT=$(cli search_graph --project "$PROJECT" --name-pattern compute --limit 5); then
@@ -458,7 +459,7 @@ fi
458459
if ! IM_ARR=$(cli search_graph --project "$PROJECT" --semantic-query send --semantic-query publish); then
459460
echo "FAIL B3: search_graph repeated --semantic-query exited non-zero"; cat "$CLI_STDERR"; exit 1
460461
fi
461-
if echo "$IM_ARR" | grep -qE '^semantic\[[0-9]+\]\{'; then
462+
if echo "$IM_ARR" | grep -qE '^semantic: [0-9]+'; then
462463
echo "OK B3: ARRAY flag (repeated --semantic-query) → semantic TOON table"
463464
else
464465
echo "FAIL B3: repeated --semantic-query did not produce a semantic table"; echo "$IM_ARR" | head -c 300; exit 1

‎src/cli/cli.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ int cbm_resolve_claude_hook_command_for_testing(const char *script_name, bool wi
190190
char *command, size_t command_size);
191191
bool cbm_optional_hook_supported_for_testing(const char *agent_name, bool windows);
192192
void cbm_hook_sanitize_metadata_for_testing(const char *input, char *output, size_t output_size);
193+
char *cbm_hook_augment_format_context_for_testing(const char *envelope, const char *token,
194+
bool *is_error);
193195
int cbm_upsert_qwen_lifecycle_hooks_for_testing(const char *settings_path, const char *binary_path,
194196
bool windows);
195197
int cbm_upsert_qoder_context_hooks_for_testing(const char *settings_path, const char *binary_path);

‎src/cli/hook_augment.c‎

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -368,9 +368,23 @@ static char *ha_format_context(const char *envelope, const char *token, bool *is
368368
yyjson_doc_free(edoc);
369369
return NULL;
370370
}
371+
/* json-tree shape: {total, cols, groups:[{qn_prefix, file,
372+
* rows:[[name,label,lines,in,out],...]}]}. The full qualified name is
373+
* qn_prefix + "." + row[0]; label is row[1]; file lives on the group. */
371374
yyjson_val *iroot = yyjson_doc_get_root(idoc);
372-
yyjson_val *results = yyjson_obj_get(iroot, "results");
373-
size_t nres = (results && yyjson_is_arr(results)) ? yyjson_arr_size(results) : 0;
375+
yyjson_val *groups = yyjson_obj_get(iroot, "groups");
376+
size_t nres = 0;
377+
size_t gidx;
378+
size_t gmax;
379+
yyjson_val *g;
380+
if (groups && yyjson_is_arr(groups)) {
381+
yyjson_arr_foreach(groups, gidx, gmax, g) {
382+
yyjson_val *rows = yyjson_obj_get(g, "rows");
383+
if (rows && yyjson_is_arr(rows)) {
384+
nres += yyjson_arr_size(rows);
385+
}
386+
}
387+
}
374388
if (nres == 0) {
375389
yyjson_doc_free(idoc);
376390
yyjson_doc_free(edoc);
@@ -389,26 +403,37 @@ static char *ha_format_context(const char *envelope, const char *token, bool *is
389403
"(structured context; your search results below are "
390404
"unaffected):",
391405
nres, token);
392-
size_t idx;
393-
size_t maxn;
394-
yyjson_val *r;
395-
yyjson_arr_foreach(results, idx, maxn, r) {
396-
if (off < 0 || off >= 3900) {
397-
break;
406+
yyjson_arr_foreach(groups, gidx, gmax, g) {
407+
const char *prefix = ha_obj_str(g, "qn_prefix");
408+
const char *fp = ha_obj_str(g, "file");
409+
yyjson_val *rows = yyjson_obj_get(g, "rows");
410+
if (!rows || !yyjson_is_arr(rows)) {
411+
continue;
412+
}
413+
size_t ridx;
414+
size_t rmax;
415+
yyjson_val *row;
416+
yyjson_arr_foreach(rows, ridx, rmax, row) {
417+
if (off < 0 || off >= 3900) {
418+
break;
419+
}
420+
const char *nm = yyjson_get_str(yyjson_arr_get(row, 0));
421+
const char *lb = yyjson_get_str(yyjson_arr_get(row, 1));
422+
char disp[HA_METADATA_CAP];
423+
if (prefix && prefix[0] && nm && nm[0]) {
424+
snprintf(disp, sizeof(disp), "%s.%s", prefix, nm);
425+
} else {
426+
snprintf(disp, sizeof(disp), "%s", nm ? nm : "");
427+
}
428+
char safe_disp[HA_METADATA_CAP];
429+
char safe_path[HA_METADATA_CAP];
430+
char safe_label[HA_METADATA_CAP];
431+
ha_sanitize_metadata(disp, safe_disp, sizeof(safe_disp));
432+
ha_sanitize_metadata(fp, safe_path, sizeof(safe_path));
433+
ha_sanitize_metadata(lb, safe_label, sizeof(safe_label));
434+
off += snprintf(text + off, (size_t)(4096 - off), "\n- %s %s%s%s", safe_disp,
435+
safe_path, safe_label[0] ? " " : "", safe_label);
398436
}
399-
const char *qn = ha_obj_str(r, "qualified_name");
400-
const char *nm = ha_obj_str(r, "name");
401-
const char *fp = ha_obj_str(r, "file_path");
402-
const char *lb = ha_obj_str(r, "label");
403-
const char *disp = (qn && qn[0]) ? qn : (nm ? nm : "");
404-
char safe_disp[HA_METADATA_CAP];
405-
char safe_path[HA_METADATA_CAP];
406-
char safe_label[HA_METADATA_CAP];
407-
ha_sanitize_metadata(disp, safe_disp, sizeof(safe_disp));
408-
ha_sanitize_metadata(fp, safe_path, sizeof(safe_path));
409-
ha_sanitize_metadata(lb, safe_label, sizeof(safe_label));
410-
off += snprintf(text + off, (size_t)(4096 - off), "\n- %s %s%s%s", safe_disp, safe_path,
411-
safe_label[0] ? " " : "", safe_label);
412437
}
413438

414439
yyjson_doc_free(idoc);
@@ -1222,6 +1247,15 @@ char *cbm_hook_augment_lifecycle_json_for_dialect(const char *input, const char
12221247
return json;
12231248
}
12241249

1250+
/* Test seam: run the real envelope->additionalContext formatter. Couples the
1251+
* test suite to the ACTUAL search_graph response shape — a format change that
1252+
* breaks this parser breaks a test locally, not just the Windows CI guard. */
1253+
char *cbm_hook_augment_format_context_for_testing(const char *envelope, const char *token,
1254+
bool *is_error) {
1255+
bool dummy = false;
1256+
return ha_format_context(envelope, token, is_error ? is_error : &dummy);
1257+
}
1258+
12251259
char *cbm_hook_augment_tool_json_for_testing(const char *input, const char *dialect_name,
12261260
const char *context, char *path, size_t path_size) {
12271261
if (!input || !context || !path || path_size == 0U || strlen(input) > HA_STDIN_CAP) {

‎src/cypher/cypher.c‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2163,8 +2163,13 @@ static const char *node_string_field(const cbm_node_t *n, const char *prop) {
21632163
} fields[] = {
21642164
{"name", offsetof(cbm_node_t, name)},
21652165
{"qualified_name", offsetof(cbm_node_t, qualified_name)},
2166+
/* Aliases: field-eval agents reach for the short names, and a miss
2167+
* used to return a silent empty column costing a round-trip. */
2168+
{"qn", offsetof(cbm_node_t, qualified_name)},
21662169
{"label", offsetof(cbm_node_t, label)},
21672170
{"file_path", offsetof(cbm_node_t, file_path)},
2171+
{"file", offsetof(cbm_node_t, file_path)},
2172+
{"path", offsetof(cbm_node_t, file_path)},
21682173
};
21692174
for (size_t i = 0; i < sizeof(fields) / sizeof(fields[0]); i++) {
21702175
if (strcmp(prop, fields[i].key) == 0) {

‎src/mcp/compact_out.c‎

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* compact_out.c — TOON emission helpers. See compact_out.h for the contract. */
1+
/* compact_out.c — tree-format emission helpers. See compact_out.h for the contract. */
22
#include "mcp/compact_out.h"
33

44
#include <ctype.h>
@@ -72,7 +72,7 @@ void cbm_sb_free(cbm_sb_t *sb) {
7272
cbm_sb_init(sb);
7373
}
7474

75-
/* ── TOON quoting ───────────────────────────────────────────────── */
75+
/* ── Tree quoting ────────────────────────────────────────────────── */
7676

7777
/* True when `s` parses as an integer or real literal (sign, digits, one dot). */
7878
static bool looks_numeric(const char *s) {
@@ -99,17 +99,17 @@ static bool looks_numeric(const char *s) {
9999

100100
static bool needs_quotes(const char *s) {
101101
if (!s || !*s) {
102-
return true; /* empty cell must be visible as "" */
103-
}
104-
if (isspace((unsigned char)s[0]) || isspace((unsigned char)s[strlen(s) - 1])) {
105-
return true;
102+
return false; /* empty cells emit as the "-" placeholder, not quotes */
106103
}
107104
for (const char *p = s; *p; p++) {
108-
if (*p == ',' || *p == '"' || *p == '\n' || *p == '\r') {
105+
/* Space-delimited rows: any internal whitespace or quote forces
106+
* quoting so column positions stay parseable. */
107+
if (isspace((unsigned char)*p) || *p == '"' || *p == '\r') {
109108
return true;
110109
}
111110
}
112-
if (strcmp(s, "true") == 0 || strcmp(s, "false") == 0 || strcmp(s, "null") == 0) {
111+
if (strcmp(s, "true") == 0 || strcmp(s, "false") == 0 || strcmp(s, "null") == 0 ||
112+
strcmp(s, "-") == 0) {
113113
return true;
114114
}
115115
return looks_numeric(s);
@@ -140,6 +140,10 @@ static void append_quoted(cbm_sb_t *sb, const char *s) {
140140
}
141141

142142
static void append_value(cbm_sb_t *sb, const char *s) {
143+
if (!s || !*s) {
144+
cbm_sb_append_n(sb, "-", 1); /* stable column positions for empties */
145+
return;
146+
}
143147
if (needs_quotes(s)) {
144148
append_quoted(sb, s);
145149
} else {
@@ -149,14 +153,14 @@ static void append_value(cbm_sb_t *sb, const char *s) {
149153

150154
/* ── Scalars ────────────────────────────────────────────────────── */
151155

152-
void cbm_toon_scalar_str(cbm_sb_t *sb, const char *key, const char *val) {
156+
void cbm_tree_scalar_str(cbm_sb_t *sb, const char *key, const char *val) {
153157
cbm_sb_append(sb, key);
154158
cbm_sb_append_n(sb, ": ", 2);
155159
append_value(sb, val);
156160
cbm_sb_append_n(sb, "\n", 1);
157161
}
158162

159-
void cbm_toon_scalar_int(cbm_sb_t *sb, const char *key, long long v) {
163+
void cbm_tree_scalar_int(cbm_sb_t *sb, const char *key, long long v) {
160164
char num[32];
161165
snprintf(num, sizeof(num), "%lld", v);
162166
cbm_sb_append(sb, key);
@@ -165,7 +169,7 @@ void cbm_toon_scalar_int(cbm_sb_t *sb, const char *key, long long v) {
165169
cbm_sb_append_n(sb, "\n", 1);
166170
}
167171

168-
void cbm_toon_scalar_bool(cbm_sb_t *sb, const char *key, bool v) {
172+
void cbm_tree_scalar_bool(cbm_sb_t *sb, const char *key, bool v) {
169173
cbm_sb_append(sb, key);
170174
cbm_sb_append_n(sb, ": ", 2);
171175
cbm_sb_append(sb, v ? "true" : "false");
@@ -174,57 +178,57 @@ void cbm_toon_scalar_bool(cbm_sb_t *sb, const char *key, bool v) {
174178

175179
/* ── Tables ─────────────────────────────────────────────────────── */
176180

177-
void cbm_toon_table_header(cbm_sb_t *sb, const char *key, int n, const char *const *cols,
181+
/* Tree-syntax table header: `key: N (cols: a b c)` — count first (agents
182+
* read scale before rows), column names once, rows indented beneath. */
183+
void cbm_tree_table_header(cbm_sb_t *sb, const char *key, int n, const char *const *cols,
178184
int ncols) {
179185
char num[32];
180-
snprintf(num, sizeof(num), "[%d]{", n);
186+
snprintf(num, sizeof(num), ": %d (cols:", n);
181187
cbm_sb_append(sb, key);
182188
cbm_sb_append(sb, num);
183189
for (int i = 0; i < ncols; i++) {
184-
if (i > 0) {
185-
cbm_sb_append_n(sb, ",", 1);
186-
}
190+
cbm_sb_append_n(sb, " ", 1);
187191
cbm_sb_append(sb, cols[i]);
188192
}
189-
cbm_sb_append_n(sb, "}:\n", 3);
193+
cbm_sb_append_n(sb, ")\n", 2);
190194
}
191195

192-
void cbm_toon_row_begin(cbm_sb_t *sb) {
196+
void cbm_tree_row_begin(cbm_sb_t *sb) {
193197
cbm_sb_append_n(sb, " ", 2);
194198
}
195199

196-
void cbm_toon_cell_str(cbm_sb_t *sb, const char *val, bool first) {
200+
void cbm_tree_cell_str(cbm_sb_t *sb, const char *val, bool first) {
197201
if (!first) {
198-
cbm_sb_append_n(sb, ",", 1);
202+
cbm_sb_append_n(sb, " ", 1);
199203
}
200204
append_value(sb, val ? val : "");
201205
}
202206

203-
void cbm_toon_cell_int(cbm_sb_t *sb, long long v, bool first) {
207+
void cbm_tree_cell_int(cbm_sb_t *sb, long long v, bool first) {
204208
char num[32];
205209
snprintf(num, sizeof(num), "%lld", v);
206210
if (!first) {
207-
cbm_sb_append_n(sb, ",", 1);
211+
cbm_sb_append_n(sb, " ", 1);
208212
}
209213
cbm_sb_append(sb, num);
210214
}
211215

212-
void cbm_toon_cell_real(cbm_sb_t *sb, double v, bool first) {
216+
void cbm_tree_cell_real(cbm_sb_t *sb, double v, bool first) {
213217
char num[48];
214218
snprintf(num, sizeof(num), "%.4g", v);
215219
if (!first) {
216-
cbm_sb_append_n(sb, ",", 1);
220+
cbm_sb_append_n(sb, " ", 1);
217221
}
218222
cbm_sb_append(sb, num);
219223
}
220224

221-
void cbm_toon_cell_bool(cbm_sb_t *sb, bool v, bool first) {
225+
void cbm_tree_cell_bool(cbm_sb_t *sb, bool v, bool first) {
222226
if (!first) {
223-
cbm_sb_append_n(sb, ",", 1);
227+
cbm_sb_append_n(sb, " ", 1);
224228
}
225229
cbm_sb_append(sb, v ? "true" : "false");
226230
}
227231

228-
void cbm_toon_row_end(cbm_sb_t *sb) {
232+
void cbm_tree_row_end(cbm_sb_t *sb) {
229233
cbm_sb_append_n(sb, "\n", 1);
230234
}

‎src/mcp/compact_out.h‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,21 @@ char *cbm_sb_finish(cbm_sb_t *sb);
3434
void cbm_sb_free(cbm_sb_t *sb);
3535

3636
/* `key: value` scalar lines (top-level, no indent). */
37-
void cbm_toon_scalar_str(cbm_sb_t *sb, const char *key, const char *val);
38-
void cbm_toon_scalar_int(cbm_sb_t *sb, const char *key, long long v);
39-
void cbm_toon_scalar_bool(cbm_sb_t *sb, const char *key, bool v);
37+
void cbm_tree_scalar_str(cbm_sb_t *sb, const char *key, const char *val);
38+
void cbm_tree_scalar_int(cbm_sb_t *sb, const char *key, long long v);
39+
void cbm_tree_scalar_bool(cbm_sb_t *sb, const char *key, bool v);
4040

4141
/* `key[n]{col1,col2,...}:` table header; rows follow at 2-space indent. */
42-
void cbm_toon_table_header(cbm_sb_t *sb, const char *key, int n, const char *const *cols,
42+
void cbm_tree_table_header(cbm_sb_t *sb, const char *key, int n, const char *const *cols,
4343
int ncols);
4444

4545
/* Row cells: call row_begin, then cell_* per column (first=true for the
4646
* first cell), then row_end. Empty/NULL strings emit as empty cells. */
47-
void cbm_toon_row_begin(cbm_sb_t *sb);
48-
void cbm_toon_cell_str(cbm_sb_t *sb, const char *val, bool first);
49-
void cbm_toon_cell_int(cbm_sb_t *sb, long long v, bool first);
50-
void cbm_toon_cell_real(cbm_sb_t *sb, double v, bool first);
51-
void cbm_toon_cell_bool(cbm_sb_t *sb, bool v, bool first);
52-
void cbm_toon_row_end(cbm_sb_t *sb);
47+
void cbm_tree_row_begin(cbm_sb_t *sb);
48+
void cbm_tree_cell_str(cbm_sb_t *sb, const char *val, bool first);
49+
void cbm_tree_cell_int(cbm_sb_t *sb, long long v, bool first);
50+
void cbm_tree_cell_real(cbm_sb_t *sb, double v, bool first);
51+
void cbm_tree_cell_bool(cbm_sb_t *sb, bool v, bool first);
52+
void cbm_tree_row_end(cbm_sb_t *sb);
5353

5454
#endif /* CBM_MCP_COMPACT_OUT_H */

0 commit comments

Comments
 (0)