Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions scripts/smoke-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,9 @@ echo "OK: search_graph found $TOTAL result(s) for 'compute'"
if ! TRACE=$(cli trace_path --project "$PROJECT" --function-name compute --direction inbound --depth 1); then
echo "FAIL: trace_path (flag form) exited non-zero"; cat "$CLI_STDERR"; exit 1
fi
# trace_path default output is TOON: the callers[N]{...} header carries the count
CALLERS=$(echo "$TRACE" | sed -n 's/^callers\[\([0-9]*\)\].*/\1/p' | head -1)
# trace_path default output is the tree format: callers_total carries the
# exact reachable count (test files excluded by default)
CALLERS=$(echo "$TRACE" | sed -n 's/^callers_total: \([0-9]*\).*/\1/p' | head -1)
CALLERS=${CALLERS:-0}
if [ "$CALLERS" -lt 1 ]; then
echo "FAIL: trace_path found 0 callers for 'compute'"
Expand Down Expand Up @@ -278,7 +279,7 @@ cyp_first_cell() {
# argv token, so string-literal args (e.g. replace(f.name,"a","A")) and Cypher
# metacharacters {}|=~<>" need no JSON escaping.
cli query_graph --project "$PROJECT" --query "$1" |
sed -n '/^rows\[/{n;p;}' | sed 's/^ //' | sed 's/^"//;s/"$//;s/\\"/"/g'
sed -n '/^rows: /{n;p;}' | sed 's/^ //' | sed 's/^"//;s/"$//;s/\\"/"/g'
}

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

# NOT EXISTS dead-code query (functions with no caller)
CYPHER_NX=$(cli query_graph --project "$PROJECT" --query "MATCH (f:Function) WHERE NOT EXISTS { (f)<-[:CALLS]-() } RETURN f.name")
NX_OK=$(echo "$CYPHER_NX" | grep -qE '^rows\[[0-9]+\]\{' && echo "True" || echo "False")
NX_OK=$(echo "$CYPHER_NX" | grep -qE '^rows: [0-9]+' && echo "True" || echo "False")
[ "$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; }

# CASE expression in RETURN
Expand All @@ -390,7 +391,7 @@ if ! ARCH=$(cli get_architecture --project "$PROJECT" --aspects clusters); then
echo "FAIL: get_architecture (flag form) exited non-zero"; cat "$CLI_STDERR"; exit 1
fi
# get_architecture default output is TOON: clusters[N]{...} header carries the count
NCLUST=$(echo "$ARCH" | sed -n 's/^clusters\[\([0-9]*\)\].*/\1/p' | head -1)
NCLUST=$(echo "$ARCH" | sed -n 's/^clusters: \([0-9]*\).*/\1/p' | head -1)
NCLUST=${NCLUST:-0}
if [ "$NCLUST" -lt 1 ]; then
echo "FAIL: get_architecture returned 0 community clusters"; echo "$ARCH" | head -c 400; exit 1
Expand Down Expand Up @@ -431,7 +432,7 @@ echo "=== Phase 3h: CLI input-mode guards (flags / stdin / --args-file / --help
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; }
# search_graph emits TOON by default: a results/semantic table header proves
# the tool parsed its typed flags and produced a well-formed response.
assert_toon_table() { grep -qE '^(results|semantic)\[[0-9]+\]\{'; }
assert_toon_table() { grep -qE '^(results|semantic): [0-9]+'; }

# B1: INTEGER flag — --limit is schema-typed integer; must parse and answer.
if ! IM_INT=$(cli search_graph --project "$PROJECT" --name-pattern compute --limit 5); then
Expand All @@ -458,7 +459,7 @@ fi
if ! IM_ARR=$(cli search_graph --project "$PROJECT" --semantic-query send --semantic-query publish); then
echo "FAIL B3: search_graph repeated --semantic-query exited non-zero"; cat "$CLI_STDERR"; exit 1
fi
if echo "$IM_ARR" | grep -qE '^semantic\[[0-9]+\]\{'; then
if echo "$IM_ARR" | grep -qE '^semantic: [0-9]+'; then
echo "OK B3: ARRAY flag (repeated --semantic-query) → semantic TOON table"
else
echo "FAIL B3: repeated --semantic-query did not produce a semantic table"; echo "$IM_ARR" | head -c 300; exit 1
Expand Down
2 changes: 2 additions & 0 deletions src/cli/cli.h
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ int cbm_resolve_claude_hook_command_for_testing(const char *script_name, bool wi
char *command, size_t command_size);
bool cbm_optional_hook_supported_for_testing(const char *agent_name, bool windows);
void cbm_hook_sanitize_metadata_for_testing(const char *input, char *output, size_t output_size);
char *cbm_hook_augment_format_context_for_testing(const char *envelope, const char *token,
bool *is_error);
int cbm_upsert_qwen_lifecycle_hooks_for_testing(const char *settings_path, const char *binary_path,
bool windows);
int cbm_upsert_qoder_context_hooks_for_testing(const char *settings_path, const char *binary_path);
Expand Down
76 changes: 55 additions & 21 deletions src/cli/hook_augment.c
Original file line number Diff line number Diff line change
Expand Up @@ -368,9 +368,23 @@ static char *ha_format_context(const char *envelope, const char *token, bool *is
yyjson_doc_free(edoc);
return NULL;
}
/* json-tree shape: {total, cols, groups:[{qn_prefix, file,
* rows:[[name,label,lines,in,out],...]}]}. The full qualified name is
* qn_prefix + "." + row[0]; label is row[1]; file lives on the group. */
yyjson_val *iroot = yyjson_doc_get_root(idoc);
yyjson_val *results = yyjson_obj_get(iroot, "results");
size_t nres = (results && yyjson_is_arr(results)) ? yyjson_arr_size(results) : 0;
yyjson_val *groups = yyjson_obj_get(iroot, "groups");
size_t nres = 0;
size_t gidx;
size_t gmax;
yyjson_val *g;
if (groups && yyjson_is_arr(groups)) {
yyjson_arr_foreach(groups, gidx, gmax, g) {
yyjson_val *rows = yyjson_obj_get(g, "rows");
if (rows && yyjson_is_arr(rows)) {
nres += yyjson_arr_size(rows);
}
}
}
if (nres == 0) {
yyjson_doc_free(idoc);
yyjson_doc_free(edoc);
Expand All @@ -389,26 +403,37 @@ static char *ha_format_context(const char *envelope, const char *token, bool *is
"(structured context; your search results below are "
"unaffected):",
nres, token);
size_t idx;
size_t maxn;
yyjson_val *r;
yyjson_arr_foreach(results, idx, maxn, r) {
if (off < 0 || off >= 3900) {
break;
yyjson_arr_foreach(groups, gidx, gmax, g) {
const char *prefix = ha_obj_str(g, "qn_prefix");
const char *fp = ha_obj_str(g, "file");
yyjson_val *rows = yyjson_obj_get(g, "rows");
if (!rows || !yyjson_is_arr(rows)) {
continue;
}
size_t ridx;
size_t rmax;
yyjson_val *row;
yyjson_arr_foreach(rows, ridx, rmax, row) {
if (off < 0 || off >= 3900) {
break;
}
const char *nm = yyjson_get_str(yyjson_arr_get(row, 0));
const char *lb = yyjson_get_str(yyjson_arr_get(row, 1));
char disp[HA_METADATA_CAP];
if (prefix && prefix[0] && nm && nm[0]) {
snprintf(disp, sizeof(disp), "%s.%s", prefix, nm);
} else {
snprintf(disp, sizeof(disp), "%s", nm ? nm : "");
}
char safe_disp[HA_METADATA_CAP];
char safe_path[HA_METADATA_CAP];
char safe_label[HA_METADATA_CAP];
ha_sanitize_metadata(disp, safe_disp, sizeof(safe_disp));
ha_sanitize_metadata(fp, safe_path, sizeof(safe_path));
ha_sanitize_metadata(lb, safe_label, sizeof(safe_label));
off += snprintf(text + off, (size_t)(4096 - off), "\n- %s %s%s%s", safe_disp,
safe_path, safe_label[0] ? " " : "", safe_label);
}
const char *qn = ha_obj_str(r, "qualified_name");
const char *nm = ha_obj_str(r, "name");
const char *fp = ha_obj_str(r, "file_path");
const char *lb = ha_obj_str(r, "label");
const char *disp = (qn && qn[0]) ? qn : (nm ? nm : "");
char safe_disp[HA_METADATA_CAP];
char safe_path[HA_METADATA_CAP];
char safe_label[HA_METADATA_CAP];
ha_sanitize_metadata(disp, safe_disp, sizeof(safe_disp));
ha_sanitize_metadata(fp, safe_path, sizeof(safe_path));
ha_sanitize_metadata(lb, safe_label, sizeof(safe_label));
off += snprintf(text + off, (size_t)(4096 - off), "\n- %s %s%s%s", safe_disp, safe_path,
safe_label[0] ? " " : "", safe_label);
}

yyjson_doc_free(idoc);
Expand Down Expand Up @@ -1222,6 +1247,15 @@ char *cbm_hook_augment_lifecycle_json_for_dialect(const char *input, const char
return json;
}

/* Test seam: run the real envelope->additionalContext formatter. Couples the
* test suite to the ACTUAL search_graph response shape — a format change that
* breaks this parser breaks a test locally, not just the Windows CI guard. */
char *cbm_hook_augment_format_context_for_testing(const char *envelope, const char *token,
bool *is_error) {
bool dummy = false;
return ha_format_context(envelope, token, is_error ? is_error : &dummy);
}

char *cbm_hook_augment_tool_json_for_testing(const char *input, const char *dialect_name,
const char *context, char *path, size_t path_size) {
if (!input || !context || !path || path_size == 0U || strlen(input) > HA_STDIN_CAP) {
Expand Down
5 changes: 5 additions & 0 deletions src/cypher/cypher.c
Original file line number Diff line number Diff line change
Expand Up @@ -2163,8 +2163,13 @@ static const char *node_string_field(const cbm_node_t *n, const char *prop) {
} fields[] = {
{"name", offsetof(cbm_node_t, name)},
{"qualified_name", offsetof(cbm_node_t, qualified_name)},
/* Aliases: field-eval agents reach for the short names, and a miss
* used to return a silent empty column costing a round-trip. */
{"qn", offsetof(cbm_node_t, qualified_name)},
{"label", offsetof(cbm_node_t, label)},
{"file_path", offsetof(cbm_node_t, file_path)},
{"file", offsetof(cbm_node_t, file_path)},
{"path", offsetof(cbm_node_t, file_path)},
};
for (size_t i = 0; i < sizeof(fields) / sizeof(fields[0]); i++) {
if (strcmp(prop, fields[i].key) == 0) {
Expand Down
58 changes: 31 additions & 27 deletions src/mcp/compact_out.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* compact_out.c — TOON emission helpers. See compact_out.h for the contract. */
/* compact_out.c — tree-format emission helpers. See compact_out.h for the contract. */
#include "mcp/compact_out.h"

#include <ctype.h>
Expand Down Expand Up @@ -72,7 +72,7 @@ void cbm_sb_free(cbm_sb_t *sb) {
cbm_sb_init(sb);
}

/* ── TOON quoting ───────────────────────────────────────────────── */
/* ── Tree quoting ───────────────────────────────────────────────── */

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

static bool needs_quotes(const char *s) {
if (!s || !*s) {
return true; /* empty cell must be visible as "" */
}
if (isspace((unsigned char)s[0]) || isspace((unsigned char)s[strlen(s) - 1])) {
return true;
return false; /* empty cells emit as the "-" placeholder, not quotes */
}
for (const char *p = s; *p; p++) {
if (*p == ',' || *p == '"' || *p == '\n' || *p == '\r') {
/* Space-delimited rows: any internal whitespace or quote forces
* quoting so column positions stay parseable. */
if (isspace((unsigned char)*p) || *p == '"' || *p == '\r') {
return true;
}
}
if (strcmp(s, "true") == 0 || strcmp(s, "false") == 0 || strcmp(s, "null") == 0) {
if (strcmp(s, "true") == 0 || strcmp(s, "false") == 0 || strcmp(s, "null") == 0 ||
strcmp(s, "-") == 0) {
return true;
}
return looks_numeric(s);
Expand Down Expand Up @@ -140,6 +140,10 @@ static void append_quoted(cbm_sb_t *sb, const char *s) {
}

static void append_value(cbm_sb_t *sb, const char *s) {
if (!s || !*s) {
cbm_sb_append_n(sb, "-", 1); /* stable column positions for empties */
return;
}
if (needs_quotes(s)) {
append_quoted(sb, s);
} else {
Expand All @@ -149,14 +153,14 @@ static void append_value(cbm_sb_t *sb, const char *s) {

/* ── Scalars ────────────────────────────────────────────────────── */

void cbm_toon_scalar_str(cbm_sb_t *sb, const char *key, const char *val) {
void cbm_tree_scalar_str(cbm_sb_t *sb, const char *key, const char *val) {
cbm_sb_append(sb, key);
cbm_sb_append_n(sb, ": ", 2);
append_value(sb, val);
cbm_sb_append_n(sb, "\n", 1);
}

void cbm_toon_scalar_int(cbm_sb_t *sb, const char *key, long long v) {
void cbm_tree_scalar_int(cbm_sb_t *sb, const char *key, long long v) {
char num[32];
snprintf(num, sizeof(num), "%lld", v);
cbm_sb_append(sb, key);
Expand All @@ -165,7 +169,7 @@ void cbm_toon_scalar_int(cbm_sb_t *sb, const char *key, long long v) {
cbm_sb_append_n(sb, "\n", 1);
}

void cbm_toon_scalar_bool(cbm_sb_t *sb, const char *key, bool v) {
void cbm_tree_scalar_bool(cbm_sb_t *sb, const char *key, bool v) {
cbm_sb_append(sb, key);
cbm_sb_append_n(sb, ": ", 2);
cbm_sb_append(sb, v ? "true" : "false");
Expand All @@ -174,57 +178,57 @@ void cbm_toon_scalar_bool(cbm_sb_t *sb, const char *key, bool v) {

/* ── Tables ─────────────────────────────────────────────────────── */

void cbm_toon_table_header(cbm_sb_t *sb, const char *key, int n, const char *const *cols,
/* Tree-syntax table header: `key: N (cols: a b c)` — count first (agents
* read scale before rows), column names once, rows indented beneath. */
void cbm_tree_table_header(cbm_sb_t *sb, const char *key, int n, const char *const *cols,
int ncols) {
char num[32];
snprintf(num, sizeof(num), "[%d]{", n);
snprintf(num, sizeof(num), ": %d (cols:", n);
cbm_sb_append(sb, key);
cbm_sb_append(sb, num);
for (int i = 0; i < ncols; i++) {
if (i > 0) {
cbm_sb_append_n(sb, ",", 1);
}
cbm_sb_append_n(sb, " ", 1);
cbm_sb_append(sb, cols[i]);
}
cbm_sb_append_n(sb, "}:\n", 3);
cbm_sb_append_n(sb, ")\n", 2);
}

void cbm_toon_row_begin(cbm_sb_t *sb) {
void cbm_tree_row_begin(cbm_sb_t *sb) {
cbm_sb_append_n(sb, " ", 2);
}

void cbm_toon_cell_str(cbm_sb_t *sb, const char *val, bool first) {
void cbm_tree_cell_str(cbm_sb_t *sb, const char *val, bool first) {
if (!first) {
cbm_sb_append_n(sb, ",", 1);
cbm_sb_append_n(sb, " ", 1);
}
append_value(sb, val ? val : "");
}

void cbm_toon_cell_int(cbm_sb_t *sb, long long v, bool first) {
void cbm_tree_cell_int(cbm_sb_t *sb, long long v, bool first) {
char num[32];
snprintf(num, sizeof(num), "%lld", v);
if (!first) {
cbm_sb_append_n(sb, ",", 1);
cbm_sb_append_n(sb, " ", 1);
}
cbm_sb_append(sb, num);
}

void cbm_toon_cell_real(cbm_sb_t *sb, double v, bool first) {
void cbm_tree_cell_real(cbm_sb_t *sb, double v, bool first) {
char num[48];
snprintf(num, sizeof(num), "%.4g", v);
if (!first) {
cbm_sb_append_n(sb, ",", 1);
cbm_sb_append_n(sb, " ", 1);
}
cbm_sb_append(sb, num);
}

void cbm_toon_cell_bool(cbm_sb_t *sb, bool v, bool first) {
void cbm_tree_cell_bool(cbm_sb_t *sb, bool v, bool first) {
if (!first) {
cbm_sb_append_n(sb, ",", 1);
cbm_sb_append_n(sb, " ", 1);
}
cbm_sb_append(sb, v ? "true" : "false");
}

void cbm_toon_row_end(cbm_sb_t *sb) {
void cbm_tree_row_end(cbm_sb_t *sb) {
cbm_sb_append_n(sb, "\n", 1);
}
20 changes: 10 additions & 10 deletions src/mcp/compact_out.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,21 @@ char *cbm_sb_finish(cbm_sb_t *sb);
void cbm_sb_free(cbm_sb_t *sb);

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

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

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

#endif /* CBM_MCP_COMPACT_OUT_H */
Loading
Loading